Python Closures and Decorators

qte77 · July 8, 2022

Python Closures and Decorators

Basics

Functions are treated as objects and can be used as arguments or return values for other higher functions. This paradigm is called first-class functions. A built-in example in python would be map() which takes a function as an argument.
In the following example the higher function f2 gets f1 as an argument and executes it.

def f1() -> None:
    print('inside f1: foobar runs')

def f2(f: object) -> None:
    print('inside f2: f1 gets executed')
    print(f'which is object {str(f)} of type {type(f)}')
    f()

f2(f1)
inside f2: f1 gets executed
which is object <function f1 at 0x000001AA553B7C10> of type <class 'function'>
inside f1: foobar runs

Closures

Closures store the function and its environment for later usage.
Meaning free variables, i.e. non-local variables, are referenced even if the function is called outside of the closures scopes.

def outer(f_arg: object) -> object:

    # free variable
    msg_outer = 'This is \'msg_outer\', a free variable used by inner'

    def inner(msg_inner: str = None) -> None:
        print(f'inner: start')
        print(msg_outer)
        f_arg(msg_inner) # gets called either way
        print(f'inner: end')

    return inner

def f(msg: str = None) -> None:
    print(f'This is \'f\' passed as an argument with args[0] equal \'{msg}\'')

closure = outer(f)
closure
closure()
closure("hello world")
 
Expand for Output

>>> closure
<function outer.<locals>.inner at 0x000002344DAF6B80>
>>> closure()
This is 'msg_outer', a free variable used by inner
This is f passed as an argument with args[0] equal 'None'
inner: end
>>> closure("hello world")
inner: start
This is 'msg_outer', a free variable used by inner
This is 'f' passed as an argument with args[0] equal 'hello world'
inner: end

 

Wrapper function

The decorating function outer gets called with the decorated function f as an argument. The decorating outer adds functionality to f.
In the following case printing some messages like the (from the point of view of f) free variable msg_outer. If bool argument deco is provided, then f will be evaluated and not returned. Function f_arg gets called inside inner either way.

from typing import Union
def outer(f_arg: object, deco: bool = None) -> Union[object,None]:

    # free variable
    msg_outer = 'This is \'msg_outer\', a free variable used by inner'

    def inner():
        print(f'inner: start')
        print(msg_outer)
        f_arg() # gets called either way
        print(f'inner: end')

    return inner if deco else inner()

def f():
    print('This is f passed as an argument')

closure_1 = outer(f, False)
closure_2 = outer(f, True)() # () as a decoration
closure_1
closure_2

Output with deco not true calls inner ad hoc.

inner: start
This 'msg_outer', a free variable used by inner
This is f passed as an argument
inner: end

Output with deco equal True returns inner as an object witch gets called by trailing ().

inner: start
This is f
inner: end

Decorater Annotation

The decorator annotation @outer replaces the notation outer(f, True).
*args and **kwargs can be provided.

from typing import Any
def outer_deco(f_arg: object) -> object:

    def inner(*args: Any) -> None:
        f_arg(*args)
    
    return inner

@outer_deco
def f(*args):
    out = str(args[0]) if args else 'no *args given'
    print(f'This is decorated f called by decorator annotation saying \'{out}\'')

f()
f('Hello World!')
>>> f()
This is decorated f called by decorator annotation saying 'no *args given'
>>> f('Hello World!')
This is decorated f called by decorator annotation saying 'Hello World!'

Decorator with return values

def outer_deco(f_arg: object) -> object:

    def inner() -> str:
        ret = f_arg()
        print(f'inner WILL return: {ret}')
        return ret
    
    return inner # does not get executed

@outer_deco
def f():
    return 'This is decorated \'f\''

ret = f() # gets executed
print(f'inner DID return: {ret}')
>>> ret = f()
inner WILL return: This is decorated 'f'
>>> print(f'inner DID return: {ret}')
inner DID return: This is decorated 'f'

Twitter, Facebook