Decorators¶
The Ominous “@
Ӧ
Special syntax ⟶ only syntactic sugar
Used to wrap existing functions/methods
Without modifying them
Examples
@functools.total_ordering
(see here)Debugging: printing function name and arguments
Timing information: time spent inside a function
Adding a D-Bus interface to a plain Python class
Defining Flask URL routes to call Python functions
…
Decorators Overview¶
Only syntactic sugar for inner functions/closures (see Closures)
Can be implemented as functions
@debug
def bar(a, b, c):
...
Can be implemented as classes (usually when decorator is parameterized ⟶ state needs to be remembered)
@debug(prefix)
def bar(a, b, c):
...
A Simple Minded Function¶
def f():
print('f called (inside the function)')
return 42
f()
f called (inside the function)
42
Decorator Basics¶
Something that creates a function to wrap another function
Wrapped function passed as argument
That is the whole point
def debug(func):
def wrapper():
print(func.__name__, 'called (says wrapper)')
return func()
return wrapper
Decorators are Syntactic Sugar¶
Lets decorate
f()
f = debug(f)
f()
f called (says wrapper)
f called (inside the function)
42
This is too much typing
Why explicitly replace a function?
Want decadence!
⟶
@debug
is the same asf = debug(f)
!
@debug
def f():
return 42
f()
f called (says wrapper)
42
Problem: Arbitrary Function Arguments¶
Currently,
wrapper()
cannot does not take any arguments⟶ cannot wrap any function
@debug
def add(a, b): # <--- takes 2 arguments
return a+b
add(1,2)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 5
1 @debug
2 def add(a, b): # <--- takes 2 arguments
3 return a+b
----> 5 add(1,2)
TypeError: debug.<locals>.wrapper() takes 0 positional arguments but 2 were given
*args
, **kwargs
To The Rescue¶
A wrapper needs to accept anything that the wrapped function can take
As generic as possible
⟶ starargs
def debug(func):
def wrapper(*args, **kwargs): # <--- accept anything
print(func.__name__, 'called:', args, kwargs)
return func(*args, **kwargs) # <--- pass anything
return wrapper
wrapper()
can wrap anythingCan
@debug
anything now
@debug
def add(a, b):
return a+b
add(1,2)
add called: (1, 2) {}
3
Sideways: functools.wraps
¶
Ugly: wrapper’s
__name__
attribute unreadableIt’s just
wrapper
inside local scope
add.__name__
'wrapper'
Fix: copy wrapped function’s metadata over
⟶
@functools.wraps
: a decorator for decorators
import functools
def debug(func):
@functools.wraps(func) # <--- copy func metadata over to wrapper
def wrapper(*args, **kwargs):
print(func.__name__, 'called:', args, kwargs)
return func(*args, **kwargs)
return wrapper
Now any decorated/wrapped function has all it needs
@debug
def add(a, b):
return a+b
add.__name__
'add'
Class Decorator: debug()
with prefix¶
import functools
class debug:
def __init__(self, msg):
self.msg = msg
def __call__(self, func): # <--- __call__() implements () (calls) on objects of "class debug"
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'{self.msg}: func = {func.__name__}, {args}, {kwargs}')
return func(*args, **kwargs)
return wrapper
@debug('wtf') # <--- here
def add(l, r):
return l+r
@debug('gosh') # <--- and here
def sub(l, r):
return l-r
print('add(1,2) = ', add(1,2))
print('sub(1,2) = ', sub(1,2))
wtf: func = add, (1, 2), {}
add(1,2) = 3
gosh: func = sub, (1, 2), {}
sub(1,2) = -1