Decorators Notes – 1 from Fluent Python

Decorators 101

@deco
def target():
    print("Running target()")

target # <function deco.<locals>.inner at 0x10063b598>
# 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()

"""

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
"""

Closure

# 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

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 ?

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

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
"""    
import clockdeco_demo
clockdeco_demo.factorial.__name__
"""
Output
------
'clocked'
"""

Common Decorators in the standard library through functools

#notes #Python #FluentPython #Decorators #Closure