-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathc9_04_06_decorator_with_args.py
More file actions
294 lines (242 loc) · 5.64 KB
/
c9_04_06_decorator_with_args.py
File metadata and controls
294 lines (242 loc) · 5.64 KB
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
# %%
"""
Decorator with args
Modify a decorator at run time with nonlocal
Manually set decorator vars
Optional args
Inject arg
"""
#%%
from functools import partial, wraps
def print1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(1)
result = func(*args, **kwargs)
return result
return wrapper
@print1
def passer():
pass
passer()
# %%
from functools import wraps
def print2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(2)
result = func(*args, **kwargs)
return result
return wrapper
@print2
def passer():
pass
passer()
# %% Do we have to create a decorator for each value ?
# No, you can pass args to a decorator ! Did you notice @wraps(func) ?
def printn(n):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(n)
result = func(*args, **kwargs)
return result
return wrapper
return decorate
@printn(8)
def passer():
pass
passer()
# %% Lets decompose
def passer2():
pass
printn(8) # returns a function (decorate)
printn(8)(passer2) # is the decorated function
printn(8)(passer2)()
# %% Now lets create a decorator which counts the number of times a function is called
def countcall(n):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal n
n = n+1
print(n)
result = func(*args, **kwargs)
return result
return wrapper
return decorate
@countcall(0)
def passer():
pass
# %%
passer()
# %%
# Lets look at this func
def logged(logmsg="info"):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(logmsg)
return func(*args, **kwargs)
def get_msg():
return logmsg
wrapper.get_msg_attr = get_msg
def set_msg(newmsg):
nonlocal logmsg
logmsg = newmsg
wrapper.set_msg_attr = set_msg
return wrapper
return decorate
@logged("warning")
def passer():
pass
passer()
# %%
passer.get_msg_attr() # it works because we attached the get_msg_attr to the wrapper
# %%
passer.set_msg_attr("error")
passer()
# %% Note that set and get still work even if you stack decorators : remember we updated the __dict__ with @wraps(func)
@printn(5)
@logged("warning")
def passer():
pass
passer()
# %%
passer.get_msg_attr()
# %%
passer.set_msg_attr("info")
passer.get_msg_attr()
# %% So what we saw is that you can create decorators with specific args and modify them at run time when needed. Below is the same idea but with a general pattern you can reuse
from functools import partial
def attach_wrapper(obj, f=None):
if f is None:
return partial(attach_wrapper,obj)
setattr(obj, f.__name__, f)
return f
def logged(logmsg="info"):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(logmsg)
return func(*args, **kwargs)
@attach_wrapper(wrapper)
def get_msg():
return logmsg
@attach_wrapper(wrapper)
def set_msg(newmsg):
nonlocal logmsg
logmsg = newmsg
return wrapper
return decorate
@logged("warning")
def passer():
pass
passer.set_msg("startup")
passer()
# %% the idea is similar to @wraps(func)
# first attach_wrapper(wrapper) returns partial(attach_wrapper,wrapper)
# then set_msg is decorated, which means it is the same as writing
set_msg = partial(attach_wrapper,wrapper)(set_msg)
# which is equivalent to
set_msg = attach_wrapper(wrapper, set_msg)
# wich is the same as writing
set_msg = set_msg
wrapper.set_msg = set_msg
# %% Why is it so complicated ?
# Why not do that for basic access ?
def logged(logmsg="info"):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(wrapper.logmsg)
return func(*args, **kwargs)
wrapper.logmsg = logmsg
return wrapper
return decorate
@logged("warning")
def passer():
pass
passer()
passer.logmsg # it seems to work !
# %%
passer.logmsg = "info"
passer()
passer.logmsg
# huh, so it works ?
# %%
@print2
@logged("error")
def passer():
pass
# %%
passer()
passer.logmsg
# %%
passer.logmsg = "debug"
passer() # because we were fored to use wrapper.logmsg, it is shadowed if we stack decorator. Therefore, if you modify the attribute, it is not propagated to the code. You are better of using the accessor and setter methods shown above
# %%
# * Optional args
# Problem : you want to create a decorator with only optional args. When no args is passed, you want the decorator to work even with no parenthesis
@logged() # this works
def passer():
pass
passer()
# %%
@logged # this does not work
def passer():
pass
passer()
# %%
# solution : you dont need the decorate(func) anymore because func in passed as an arg
def logged(func=None, logmsg="info"):
if func is None:
return partial(logged, logmsg=logmsg)
@wraps(func)
def wrapper(*args, **kwargs):
print(logmsg)
return func(*args, **kwargs)
return wrapper
@logged(logmsg="warning") # this works
def spam():
print('spam')
spam()
# %%
@logged() # this also works
def spam():
print('spam')
spam()
# %%
@logged # this works too
def spam():
print('spam')
spam()
# %%
# %%
from functools import wraps
import inspect
def optional_debug(func):
# prevents debug from being an arg of the function
if "debug" in inspect.getfullargspec(func).args:
raise TypeError("Debug arg already defined")
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
# do debug stuff
print("Calling", func.__name__)
return func(*args, **kwargs)
# modifies signature so that it adds debug arg to the func
sig = inspect.signature(func)
parms = list(sig.parameters.values())
parms.append(
inspect.Parameter("debug", inspect.Parameter.KEYWORD_ONLY, default=False)
)
wrapper.__signature__ = sig.replace(parameters=parms)
return wrapper
# %%
@optional_debug
def passer():
pass
passer.__signature__
# %%
passer(debug=True)