Python Decorators
Good read: https://realpython.com/primer-on-python-decorators/
A decorator is just a regular Python function
You can also define classes as decorators. In this case, such classes must be callable (i.e. need to override __call__
method).
Syntactic sugar with @
Python allows to use decorators in a simpler way with the @
symbol, sometimes called the pie syntax.
Following code shows example our_decorator
which wraps a function func
passed as input.
def our_decorator(func):
def function_wrapper(x):
print("Before calling " + func.__name__)
func(x)
print("After calling " + func.__name__)
return function_wrapper
def foo_inner(x):
print("Hi, foo has been called with " + str(x))
foo = our_decorator(foo_inner)
@our_decorator
def foo2(x):
print("Hi, foo2 has been called with " + str(x))
After inspecting the environment variables using inspect.getmembers(sys.modules[__name__], inspect.isfunction)
, we will get following output
('our_decorator', <function __main__.our_decorator(func)>),
('foo_inner', <function __main__.foo_inner(x)>),
('foo', <function __main__.our_decorator.<locals>.function_wrapper(x)>),
('foo2', <function __main__.our_decorator.<locals>.function_wrapper(x)>),
our_decorator
and foo_inner
are regular python functions
While both foo
and foo2
are of same type i.e. an instance of function our_decorator.function_wrapper
This shows that using @our_decorator
on foo2
is same as running foo2 = our_decorator(foo2)
But here’s a problem, we expected foo2 to be a function of its own type foo2
, and not of type function_wrapper
.
By decorating this (default) way we lose the identity of the wrapped function.
Sidenote: Its just a syntactic sugar
Consider following lines of code
def wrapper(func):
print("Inside wrapper with func: ", func)
return 10
@wrapper # HINT: same as wrapped = wrapper(wrapped)
def wrapped():
print("Inside wrapped")
Q. If the above code block is executed in a jupyter cell, what will be the output to stdout?
Answer:
Inside wrapper with func: <function wrapped at 0x125f86b60>
Exaplaination: Since wrapped
function is not called yet, only the print statement from wrapper is executed.
Q. After executing above code block, if we executed following code block, what will be the output?
print(wrapped)
print(wrapped())
Answer:
10
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[47], line 2
1 print(wrapped) # outout 1?
----> 2 print(wrapped()) # output 2?
TypeError: 'int' object is not callable
Explaination:
wrapper
returned 10 and it was assigned to wrapped
. So wrapped, instead of a function, became an integer variable with value 10. Thus print(wrapped)
returned 10.
Since integer is not callable, wrapped()
throws an error
functools wrap
Python functools provides a convinient way to wrap function in functools
module called wrap
and partial
The advantage of using functools.wraps
is that it preserves the information of the wrapped function, such as name, documentation.
It simply changes the __name__
, __doc__
and other relevent variables of the wrapper function to be equal to the wrapped (inner) function
The decorator has to be modified like this:
from functools import wraps
def our_decorator(func):
@wraps(func) # same effect as function_wrapper = wraps(func)(function_wrapper)
def function_wrapper(x):
print("Before calling " + func.__name__)
func(x)
print("After calling " + func.__name__)
return function_wrapper
So now the inspection output is
('foo', <function __main__.foo_inner(x)>),
('foo2', <function __main__.foo2(x)>),
No that even foo
is not an instance of the foo_inner
functools partial
If the decorator function requires some arguments, then we can’t directly use @decorator
annotation.
In such cases, decorator can be wrapped inside @partial
from functools import wraps, partial
def our_decorator(func, id):
# id has a local scope. can be accesed inside function_wrapper.
@wraps(func)
def function_wrapper(x):
print("Before calling " + func.__name__)
func(x)
print("After calling " + func.__name__)
return function_wrapper
# @our_decorator(2) # will fail
@partial(our_decorator, id = 2)
def foo2(x):
print("Hi, foo2 has been called with " + str(x))