closure

The concept of closures

An inner function is defined in an outer function, the inner function uses temporary variables of the outer function, and the return value of the outer function is a reference to the inner function. This forms a closure. [1]

Here is an example closure:

def outer():
    a = 10
    def inner():
        b= 10
        print(b)
        print(a)
    return inner
    
if __name__ == '__main__':
    inner_func = outer()
    inner_func()
  
  >> 10
Copy the code

In this case, a is the local variable of outer, and normally the memory allocated for a is freed at the end of the function. In closures, however, if the outer function ends up with a temporary variable of its own that will be used in the inner function in the future, it binds the temporary variable to the inner function and terminates itself. [1]

In inner, a is a free variable. This is a technical term for a variable that is not bound locally in scope. [2]

In Python, the __code__ attribute holds the names of local and free variables. For inner, the local and free variables are:

inner_func = outer()
inner_func.__code__.co_freevars
>> ('a',)
inner_func.__code__.co_varnames
>> ('b'.)Copy the code

So, since the outer function will bind the variables used by the inner variable (i.e. the free variables of the inner function) to the inner function, where is the binding of A? The binding of A is in the inner __closure__ property of the return function, where cell_contents holds the real value. [2]

inner_func.__closure__[0].cell_contents
>> 10
Copy the code

To sum up, a closure is a function that preserves the binding of the free variables that existed when the function was defined, so that when a function is called, those bindings can still be used even though the definition scope is no longer available. [2]

further

When we try to change the value of a in inner

def outer():
    a = 10

    def inner():
        # nonlocal a
        b = 10
        print(b)
        a += 1
        print(a)
    return inner


if __name__ == '__main__':
    inner_func = outer()
    inner_func()
    
>> UnboundLocalError: local variable 'a' referenced before assignment
Copy the code

The key to this error is that when a is a number or any immutable type, a+ = 1 is equivalent to a = a+1, which makes a local variable. For immutable types such as numbers, strings, and tuples, they can only be read, not updated. In a += 1, which is equivalent to a = a + 1, we implicitly create a local variable a, so that a is no longer a free variable, and no longer exists in the closure.

Solution: Add nonlocal declaration. It marks a variable as a free variable so that it becomes a free variable even if you assign a new value to it in the function.

def outer():
    a = 10

    def inner():
        nonlocal a
        b = 10
        print(b)
        a += 1
        print(a)
    return inner

if __name__ == '__main__':
    inner_func = outer()
    inner_func()
>> 10 
   11
Copy the code

BINGO!

A decorator

In decorator this part mainly explains the following situations:

  • Function decorator

  • Class decorator

  • A decorator chain

  • Decorator with parameters

The role of decorators

A decorator is essentially a Python function or class that allows other functions or classes to add additional functionality without making any code changes, and the return value of the decorator is also a function/class object. It is often used in scenarios with faceted requirements, such as logging insertion, performance testing, transaction processing, caching, permission verification, etc. Decorators are an excellent design for solving these problems. With decorators, we can extract a lot of code that is similar to the function itself and continue to reuse it. [3]

One of the things that’s used a lot on the web is, if we have a lot of functions, we now want to count the running time of each function and print its arguments.

The method of comparison is retarded: modify the original content of the function, add the statistical time code and print the parameter code. But with the knowledge of closures above, we should be able to come up with such an approach

def count_time(func):
    def wrapper(*args, **kwargs):
        tic = time.clock()
        func(*args, **kwargs)
        toc = time.clock()
        print('Function %s runs at %.4f' %(func.__name__, toc-tic))
        print('Parameter is :'+str(args)+str(kwargs))
    return wrapper


def test_func(*args, **kwargs):
    time.sleep(1)


if __name__ == '__main__':
    f = count_time(test_func)
    f(['hello'.'world'], hello=1, world=2)
Copy the code

Here func is bound to the Wrapper function, so even if count_time ends, the func passed in will be bound to the Wrapper. The following code can be verified.

f.__code__.co_freevars
>> ('func',)
f.__closure__[0].cell_contents
>> <function test_func at 0x0000014234165AE8>
Copy the code

The code above is how python decorators work, except that we can simplify the code by using the @ syntax sugar.

def count_time(func):
    def wrapper(*args, **kwargs):
        tic = time.clock()
        func(*args, **kwargs)
        toc = time.clock()
        print('Function %s runs at %.4f' %(func.__name__, toc-tic))
        print('Parameter is :'+str(args)+str(kwargs))
    return wrapper

@count_time
def test_func(*args, **kwargs):
    time.sleep(1)


if __name__ == '__main__':
    test_func(['hello'.'world'], hello=1, world=2) >> test_func'hello'.'world'],){'hello': 1, 'world'2} :Copy the code

A function can also be decorated with multiple decorators in the same way as a single decorator. It is important to understand the order in which decorators are actually called.

def count_time(func):
    print('count_time_func')

    def wrapper_in_count_time(*args, **kwargs):
        tic = time.clock()
        func(*args, **kwargs)
        toc = time.clock()
        running_time = toc - tic
        print('Function %s runtime %f'% (func.__name__, running_time))
    return wrapper_in_count_time


def show_args(func):
    print('show_args func')

    def wrapper_in_show_args(*args, **kwargs):
        print('Function parameters are'+str(args)+str(kwargs))
        return func()
    return wrapper_in_show_args


@count_time
@show_args
def test_func(*args, **kwargs):
    print('test_func')


if __name__ == '__main__':
    f = test_func(['hello'.'world'], hello=1, world=2) >> show_args func count_time_func'hello'.'world'],){'hello': 1, 'world': 2} test_func wrapper_in_show_args run time 0.000025Copy the code

Ignore the @count_time decorator for now, if you only have the @show_args decorator. So, the back of the decorator actually looks like this:

f = show_args(test func)
f(...)

# plus @count_time
f = show_args(test func)
g = count_time(f)
g(...)
Copy the code

We can print f,g and see what comes back.

f.__name__
>> wrapper_in_show_args
g.__name__
>> wrapper_in_count_time
Copy the code

Show_args prints ‘show_args_func’ and returns wrapper_in_show_args. Then call count_time and pass wrapper_in_show_args to count_time, printing ‘count_time_func’ and returning wrapper_in_count_time. Finally, the user calls wrapper_in_count_time and passes in the parameters. In the wrapper_in_count_time function, the func function is first called, where func is a free variable, wrapper_in_show_args passed in earlier, so the function argument is printed. In wrapper_in_show_args, func() is called, which in turn is test_func passed in earlier, so print ‘test_func’. Finally, print the running time of the function, and the whole call process ends.

In short, the core of a decorator is a closure, and once you understand closures, you can understand decorators thoroughly.

In addition, decorators can not only be functions, but also can be classes. Compared with function decorators, class decorators have the advantages of large flexibility, high cohesion and encapsulation. Using class decorators relies heavily on the class’s __call__ method, which is called when a decorator is attached to a function using the @ form. [3]

class deco_class(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Initialize decorator')
        self.func(*args, **kwargs)
        print('Abort decorator')


@deco_class
def klass(*args, **kwargs):
    print(args, kwargs)


if __name__ == '__main__':
    klass(['hello'.'world'], hello=1, world=2) >> Initialize decorator (['hello'.'world']) {'hello': 1, 'world': 2} Abort the decoratorCopy the code

The resources

[1] www.cnblogs.com/Lin-Yi/p/73…

[2] Fluent Python, Luciano Ramalho

[3] foofish.net/python-deco…

[4] blog.apcelent.com/python-deco…