Decorators Notes – 1 from Fluent Python
Decorators 101
A decorator may perform some processing with the decorated function, and returns it or replaces it with another function or callable object.
Inspection reveals that target is a now a reference to inner
@deco
def target():
print("Running target()")
target # <function deco.<locals>.inner at 0x10063b598>
- Decorators have the power to replace the decorated function with a different one.
- Decorators are executed immediately when a module is loaded.
# Program to check how decorators are executed
registry = []
def register(func):
print(f"running register({func})")
registry.append(func)
return func
@register
def f1():
print("running f1()")
@register
def f2():
print("running f2()")
def f3():
print(f"running f3()")
def main():
print("running main()")
print("registry ->", registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
"""
OUTPUT
------
running register(<function f1 at 0x107a25c10>)
running register(<function f2 at 0x1079b0280>)
running main()
registry -> [<function f1 at 0x107a25c10>, <function f2 at 0x1079b0280>]
running f1()
running f2()
running f3()
"""
Register runs (twice) before any other function in the module.
When register is called, it receives as an argument the function object being decorated—for example,
<function f1 at 0x100631bf8>
.- After the module is loaded, the registry list holds references to the two decorated functions: f1 and f2.
A real decorator is usually defined in one module and applied to functions in other modules.Unlike the example above.
Variable Scoping
b = 6
def smart_function(a):
print(a)
print(b)
b = 9
smart_function(3)
"""
OUTPUT
------
3 def smart_function(a):
4 print(a)
----> 5 print(b)
6 b = 9
7
UnboundLocalError: local variable 'b' referenced before assignment
"""
Python does not require you to declare variables, but assumes that a variable assigned in the body of a function is local.
To make the above program work with
b=6
you would need to use the keywordglobal
.
Closure
A closure is a function with an extended scope that encompasses nonglobal variables referenced in the body of the function but not defined there.
It does not matter whether the function is anonymous or not; what matters is that it can access nonglobal variables that are defined outside of its body.
# Closure Example
def make_averager():
series = [] # Free Variable
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/sum(series)
return averager # Returns a function object
avg = make_averager()
avg(10) # 10.0
avg(11) # 10.5
avg(12) # 1.0
series
is a local variable ofmake_averager
because the assignmentseries = []
happens in the body of that function. But whenavg(10)
is called,make_averager
has already returned, and its local scope is long gone.Within
averager()
, series is a free variable i.e a variable that is not bound in the local scope.
__code__
attribute keeps the names of the local and free variables.The value for series is kept in the
__closure__
attribute of the returned function avg.
avg.__code__.co_varnames # ('new_value', 'total')
avg.__code__.co_freevars # ('series',)
avg.__closure__ # (<cell at 0x107a44f78: list object at 0x107a91a48>,)
avg.__closure__[0].cell_contents # [10, 11, 12]
Formal Definition
A closure is a function that retains the bindings of the free variables that exist when the function is defined, so that they can be used later when the function is invoked and the defining scope is no longer available.
Refactoring averager
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
avg(10)
"""
OUTPUT
------
UnboundLocalError: local variable 'count' referenced before assignment
"""
Why did count
not behave like series
i.e free variable ?
- For
series
we took advantage of the fact that lists are mutable and we never assigned to the series name. We only called series.append and invoked sum and len on it. count
is an immutable types and all you can do is read, never update. If you try to rebind them, as in count = count + 1, then you are implicitly creating a local variable count. It is no longer a free variable, and therefore it is not saved in the closure.
Refactoring averager
with nonlocal
def make_averager():
count = 0
total = 0
def average(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
nonlocal
lets you declare a variable as a free variable even when it is assigned within the function.If a new value is assigned to a nonlocal variable, the binding stored in the closure is changed.
# Code example here!
Understanding a simple decorator
# clockdeco.py
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
# closure for clocked encompasses the func free variable
result = func(*args)
elapsed = time.perf_counter()
name = func.__name__
arg_str = ','.join(repr(args) for arg in args)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked
# SimpleDeco.py
import time
from clockdeco import clock
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
"""
OUTPUT
------
**************************************** Calling snooze(.123)
[14549.25258126s] snooze((0.123,)) -> None
**************************************** Calling factorial(6)
[14549.25291446s] factorial((1,)) -> 1
[14549.25296426s] factorial((2,)) -> 2
[14549.25301028s] factorial((3,)) -> 6
[14549.25305792s] factorial((4,)) -> 24
[14549.25310024s] factorial((5,)) -> 120
[14549.25314435s] factorial((6,)) -> 720
6! = 720
"""
@clock
onfactorial(n)
is just the syntatic sugar forfactorial = clock(factorial)
clock(func)
gets the factorial function as its func argument.- It then creates and returns the clocked function, which the Python interpreter assigns to factorial behind the scenes.
- The
__name__
offactorial(n)
would give youclocked
import clockdeco_demo
clockdeco_demo.factorial.__name__
"""
Output
------
'clocked'
"""
- Each time
factorial(n)
is called,clocked(n)
gets executed.
Common Decorators in the standard library through functools
- @wraps
- @lru_cache
- @singledispatch