-
Notifications
You must be signed in to change notification settings - Fork 1
/
c9_09_descriptor_property_class_static.py
355 lines (297 loc) · 8.47 KB
/
c9_09_descriptor_property_class_static.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#%%
"""
How property works
The right way to use property
class and static methods
stack decorator with class and static method
lazy property
"""
#%% [markdown]
# # Classic accessor : BAD
# Note : only do this if you want to add extra funcs to get, set and delete
#%%
class Tester:
def __init__(self, val):
self.val = val
def getval(self):
print("GET")
return self.val
def setval(self, newval):
print("SET")
self.val = newval
def delval(self):
print("DEL")
del self.val
t = Tester(5)
# %%
t.getval()
# %%
t.setval(8)
t.getval()
# %%
t.delval()
t.getval()
# %% [markdown]
# # Use of property (class decorator)
# Not the standard way
# %%
class Tester2:
def __init__(self, val):
self.val = val
def getval(self):
print("GET")
return self._val
def setval(self, newval):
print("SET")
self._val = newval
def delval(self):
print("DEL")
del self._val
val = property(fget=getval, fset=setval, fdel=delval, doc="val property")
# %%
tt = Tester2(45) # setter is called
# %%
tt.val
# %%
tt.val = 5
tt.val
# %%
del tt.val
tt.val
# %%
Tester2.val
# %%
Tester2.val.__doc__
# %%
# with property - alternate
class Tester3:
def __init__(self, val):
self.val = val
def getval(self):
print("GET")
return self._val
def setval(self, newval):
print("SET")
self._val = newval
def delval(self):
print("DEL")
del self._val
"""val = property(fget=getval, fset=setval, fdel=delval, doc="val property")
is the same as the 4 lines above where i instantiate property and then define the getter, setter and deleter.
"""
val = property()
val = val.getter(getval)
# I can also replace the two lines above with val = property(getval)
val = val.setter(setval)
val = val.deleter(delval)
# %%
t3 = Tester3(45)
# %%
t3.val
# %%
t3.val = 5
t3.val
# %%
del t3.val
# %%
Tester3.val
# %%
Tester3.val.__doc__
# %%
Tester3.val.__get__(t3)
# %%
Tester3.val.__set__(t3,5)
# %%
t3.val
# %%
Tester3.val.__delete__(t3)
# %% [markdown]
# # Use of property the right way
class Tester4:
def __init__(self, val):
self.val = val # no _val so that init calls the setter
@property
def val(self):
print("GET")
return self._val
# after this the method val was transformed to property(val)
# when i try to access instance.val, it calls the __get__ of the property class. The __get__ of the property class calls the initial method which returns instance._val : so val went from method of a Tester4 instance to instance of property and returns an attribute of the Tester4 instance
# this is why you write .val (which calls the __get__) and not .val() as val is no longer a function
@val.setter
def val(self, newval): #not the same val (called val' in the comments)
print("SET")
self._val = newval
# after this, val is the instance of property. val has access to the setter method. A new method called val' is defined; after decoration val' = val.setter(val'). So val' is the same instance of property but now has a setter.
# so now if i do instance.val = 5, it calls the __set__ of our property which calls val'
# same logic applies here with __delete__
@val.deleter
def val(self):
print("DEL")
del self._val
#%%
t4 = Tester4(8)
#%%
Tester4.val.__get__(t4)
Tester4.val.__set__(t4, 9)
print(t4.val)
Tester4.val.__delete__(t4)
t4.val
"""Lots of work is needed to fully understand __get__, __set__, __delete__, __getattr__, etc. More work needed on descriptor protocol if you want something highly customizable. The difficulty lies in the fact that we will probably need to dive into the internals of python which are written in C
However, from what I understand, You should use @property if you dont want to risk breaking something
https://docs.python.org/3/howto/descriptor.html
https://realpython.com/python-descriptors/
"""
# %% [markdown]
# # Static method and Class Method
# @staticmethod and @classmethod are used to specify that some methods are part of the Class and not the instance. Note that they can be used by the instance as well. A classmethod is exactly the same as a staticmethod in python with the only difference that it takes the class as a first argument
class StaticMethod:
"""Emulate PyStaticMethod_Type() in Objects/funcobject.c
and adds print of instance and owner"""
def __init__(self, f):
print(self)
self.f = f
def __get__(self, instance, owner=None):
print("instance", instance)
print("owner", owner)
return self.f
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c and adds print of instance and owner"""
def __init__(self, f):
self.f = f
def __get__(self, instance, owner=None):
print("instance", instance)
print("owner", owner)
if owner is None:
owner = type(instance)
def newfunc(*args):
return self.f(owner, *args)
return newfunc
class PDF:
def standard(self):
print(41)
@StaticMethod
def static():
print(42)
@ClassMethod
def classmeth(cls):
print(cls, 43)
# %%
PDF.standard()
# %%
PDF.static()
# %%
PDF.classmeth()
# %%
PDF().standard()
# %%
PDF().static()
# %%
PDF().classmeth()
# %%
"""
static becomes StaticMethod(static)
same as having
class PDF:
static = ...
So static is now an attribute of PDF. Each time it is called, it goes through the __get__
Note that, in Python, a class method is just a static method that takes the class reference as the first argument of the argument list
"""
# %%
# When a function decorator is applied to a function, it transforms it into a function.
# When a class decorator is applied to a function, it transforms it into a callable instance.
# Which is why you can stack decorators.
# On the other hand, when you apply property, you transform your method into a class attribute which is not directly callable. classmethod and staticmethod transforms the method into a class instance where the initial method is an attribute, so not directly callable either. If you try to pass it to a decorator, it crashes
def printer(func):
def wrapper(*args, **kwargs):
print(42)
return func(*args, *kwargs)
return wrapper
class Crash:
@printer
@staticmethod
def passer():
pass
Crash.passer()
# %%
class Ok:
@staticmethod
@printer
def passer():
pass
Ok.passer()
# %%
def lazy_property(fn):
'''Decorator that makes a property lazy-evaluated.
'''
attr_name = '_lazy_' + fn.__name__
@property
def _lazy_property(self):
if not hasattr(self, attr_name):
setattr(self, attr_name, fn(self))
return getattr(self, attr_name)
return _lazy_property
class Person:
def __init__(self, name, occupation):
self.name = name
self.occupation = occupation
@lazy_property
def relatives(self):
# Get all relatives
for _ in range(100000000):
pass
return 5
# %%
a = Person("a","d")
# %%
a.relatives
# %%
dir(a)
# %%
class User1:
def __init__(self, username):
self.username = username
self._profile_data = None
print(f"{self.__class__.__name__} instance created")
@property
def profile_data(self):
if self._profile_data is None:
print("self._profile_data is None")
self._profile_data = self._get_profile_data()
else:
print("self._profile_data is set")
return self._profile_data
def _get_profile_data(self):
# get the data from the server and load it to memory
print("Run the expensive operation")
fetched_data = "The mock data of a large size"
return fetched_data
# %%
u = User1(5)
# %%
u.profile_data
# %%
u.profile_data
# %%
import json
from json import JSONEncoder
class MyJSONEncoder(JSONEncoder):
def default(self, o):
if hasattr(o, 'toJSON'):
return o.toJSON()
return super().default(o)
class C:
def __init__(self, name):
self.name = name
def toJSON(self):
return {"name":self.name}
c = C("jon")
class D:
def __init__(self, name, cc):
self.name = name
self.cc = cc
def toJSON(self):
return {"name":self.name, "cc":self.cc}
d = D("ff", c)
print(json.dumps(d, cls=MyJSONEncoder))
with open("gg.json", "w+") as fp:
json.dump(d, fp, indent=4, cls=MyJSONEncoder)