The introduction of

Defining functions is similar to defining variables. Variable names bind the memory address of values, whereas function names bind the memory address of code blocks. Functions can also be used like variables.

The function object

The essence of function objects is that functions can be used as variables. Function objects can be used as follows:

  • The function name is called in parentheses

A parenthesized call to a function name runs the function body code and can use variables to receive the function return value.

def func() :
    print('func')
    return 'func'
res = func()
print(res)
Copy the code
  • You can assign

The memory address of the function body code after the function definition is complete, without parentheses.

def func() :
    print('func')
print(func)  The memory address of the function body code
foo = func  # refer the name foo to the memory address of the func function body code
foo()  # equivalent to func()
Copy the code
  • Can be used as arguments to a function
def func(x) :
    print(x)
    x()
def foo() :
    print('from foo')

func(foo) 
# pass foo as an argument to func
Copy the code
  • Can be used as the return value of the function
def func(x) :
    x()
    return x
def foo() :
    print('from foo')
res = func(foo)
print(res)
Copy the code
  • Can be used as an element of container type data
def func() :
    print('from func')
def foo() :
    print('from foo')
    
func_list = [func, foo]
for function in func_list:
    function() 
Copy the code

Nested function

Function nesting is understood in two ways:

Nested definitions of functions

Define other functions within a function.

def func() :
    print('from func')
    def foo() :
        print('from foo')
Copy the code

Nested calls to a function

Call another function in the process of calling one function.

# Define a function: compare two values
def max2(x,y) :
    if x > y:
        return x
    else:
        return y# Define function: compare the size of four values
    
def max4(a,b,c,d) :
# Step 1, compare a and B
    res = max(a,b)
# 2, compare RES and C
    res1 = max(res,c)
Step 3, compare RES1 with D
    res2 = max(res1,d)
    return res2
res = max(2.3.5.1)
print(res)
Copy the code

The closure function

What is a closure function

A closed function is a function that is inside a function, that is, nested inside a function.

Package functions refer to references to scope words of inner functions and outer (non-global) functions.

Closure functions are based on function objects and can be called at any location, but the scope relationship is determined at the time the function is defined, regardless of where the function is called.

An inline function is a closure if it contains a reference to the name of an outer function’s scope.

def func() :
    x = 100
    def foo() :
        print(x)  # A reference to an external function scope word
    foo()
    return foo
res = func()
Copy the code

Closure functions

Closure functions provide an alternative way of passing parameters to functions, and decorators are also based on closure functions.

The parameters required by the function body are directly defined as parameters
def func(x, y) :
    print(x, y)

# function pass parameter method 2: closure function, embedded function name reference outer layer function
def func(x) :
    def foo() :
        print(x)
Copy the code

A decorator

Why decorators

The design of software should follow the principle of open and closed, that is, open to extension and closed to modification of code. This means that you can extend existing code as new requirements or features come in, but don’t modify code once it’s designed to fulfill a requirement on its own.

It seems to be the opposite, because all the code contained in a program can change some of it, you have the butterfly effect, you have to change everything wrong, but you have to provide a way for the program to extend, and that’s where decorators come in.

What is a decorator

Decorator refers to adding something extra to embellish other transactions, and an agent refers to a tool, which can be defined as a function. Therefore, decorator refers to defining a function that is used to add extra functionality to other functions.

In the abstract, decorators add additional functionality to decorated objects without modifying their source code or how they are called. Decorators are often used for login verification, permission verification, etc. Decorators can be used to extract a lot of code irrelevant to the function itself and reuse it.

No argument decorator implementation

Decorators can be divided into parameterless decorators and parameterless decorators. The implementation principle is the same, based on closure functions, function nesting and function objects.

To derive the implementation of a no-argument decorator, use a simple example: calculate the execution time of a function.

Let’s start with the simple use of the time module, which can be used to calculate time.

import time  # import module
start_time = time.time()  # current time
end_time = time.time()  
res = end_time - start_time  # Subtract to get the time difference
Copy the code

Now to derive how to implement the no-argument decorator, calculate the execution time of the following functions.

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
Copy the code

Solution 1

import time

def func(x, y) :
    start_time = time.time()
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    end_time = time.time()
    print(end_time - start_time)  # Function runtime
     return x, y
func(1.2)
Copy the code

Although this scheme solves the need to calculate the execution time of functions, it violates the open and closed principle. Although the calling method is not changed, the source code of func functions is changed.

Solution 2

Add time before and after the function call, and subtract to get the running time

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y

start_time = time.time()  # current time
func(1.2)
end_time = time.time()  
print(end_time - start_time)
Copy the code

Although this scheme solves the need to calculate the execution time of the function, it does not violate the principle of open and closed, but if the calculation of the running time of the function is a very common function, then the code will appear a lot of repeated parts, code redundancy.

Solution 3

The time calculation code in scheme 2 is encapsulated as a function to reduce code redundancy.

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y
    
def wrapper() :
    start_time = time.time()  # current time
	res = func(1.2)
	end_time = time.time()  
	print(end_time - start_time)
wrapper()
Copy the code

This solution solves the need to calculate the running time of the function and reduces code redundancy, but it changes the way the original function is called, and the arguments of the func function are written out, so only the running time of func(1, 2) can be calculated.

Solution 4

The wrapper function can be defined with the same parameters as the func function, and the parameters of the func function can be written alive by passing parameters to the Wrapper function.

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y
    
def wrapper(x, y) :
    start_time = time.time()  # current time
	res = func(x, y)
	end_time = time.time()  
	print(end_time - start_time)
wrapper(1.2)
Copy the code

This solution solves the need and fixes the problem of fixed arguments, but the invocation changes and the wrapper function parameters must be defined according to the format of func parameters. If the number of func parameters changes, the wrapper parameters must also change.

Solution 5

How to prevent the parameters of the Wrapper function from being affected by the parameters of the func function is the goal of this scheme. When defining the Wrapper function, we define the wrapper function as a variable length parameter that can be used to receive any syntactically correct argument as long as it is passed in the same way as func. In the definition phase, the Wrapper function can accept all arguments regardless of how func’s parameters change. There is no need to change the Wrapper parameter.

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y
    
def wrapper(*args, **kwargs) :
    start_time = time.time()  # current time
	res = func(*args, **kwargs)  # here is the argument
	end_time = time.time()  
	print(end_time - start_time)
wrapper(1.2)
Copy the code

This solution resolves the problem that the Wrapper function parameters are limited to func parameters, but the way the function is called has changed, and the Wrapper function can only count the runtime of the func function.

Solution 6

Func = func; func = func; func = func;

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y
    
def wrapper(function, *args, **kwargs) :
    start_time = time.time()  # current time
	res = function(*args, **kwargs)  # here is the argument
	end_time = time.time()  
	print(end_time - start_time)
wrapper(func, 1.2)
Copy the code

We can pass values to func, but we need to change the way the decorated object is called and the source code. We still change the way the original function is called, and the shape of the Wrapper function is inconsistent with the parameters of the func function. Therefore, consider using closure functions to pass arguments to embedded functions.

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y

def outer(function) :
    def wrapper(*args, **kwargs) :
    	start_time = time.time()  # current time
		res = function(*args, **kwargs)  # here is the argument
		end_time = time.time()  
		print(end_time - start_time)
        return res  Returns the return value of the decorated object
    return wrapper

wrapper = outer(func)
wrapper(1.2)
Copy the code

Since the wrapper function itself is in the global namespace, it needs to pass arguments to the Wrapper function, so it is put in the local namespace. When passing arguments to the Wrapper function is finished, the Wrapper function needs to be put back into the global namespace.

This solution still does not solve the problem of how to call decorated objects.

Solution 7 – Final version

Outer can be assigned to any variable. It can be assigned to wrapper, and func can be assigned to wrapper. If you need a wrapper, return the wrapper from the decorated function.

import time

def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y

def outer(function) :
    def wrapper(*args, **kwargs) :
    	start_time = time.time()  # current time
		res = function(*args, **kwargs)  # here is the argument
		end_time = time.time()  
		print(end_time - start_time)
        return res  Returns the return value of the decorated object
    return wrapper

func = outer(func)
func(1.2)
Copy the code

Now we’ve done this and we’ve added new functionality to func without changing the source code of func or the way we call it and now the memory address that func points to is not the memory address that func points to, but the memory address of the Wrapper function, and we’ve changed the name of the function to func.

No argument decorator summary

Through the derivation of the parameterless decorator, a universal template and the principle of parameterless decorator can be obtained.

def outer(function) :  # outer is just a name, can you call it anything
    def wrapper(*args, **kwargs) :
        res = function()
        return res
    returnWrapper decorates the function name = outer# The principle of the no-parameter decoratorDecorated function name ()Copy the code

Syntactic sugar

To use decorators concisely and elegantly, Python provides a special decorator syntax that replaces the decorator function name = outer by adding a single line @outer directly above the decorator object. Outer is called when the interpreter executes to @outer. The function name directly below it is passed in as an argument, and the result returned is reassigned to the original function name.

import time

def outer(function) :
    def wrapper(*args, **kwargs) :
    	start_time = time.time()  # current time
		res = function(*args, **kwargs)  # here is the argument
		end_time = time.time()  
		print(end_time - start_time)
        return res  Returns the return value of the decorated object
    return wrapper

@outer
def func(x, y) :
    time.sleep(3)  Can sleep the application for 3s
    print(x, y)
    return x, y
Copy the code

Parameterized decorator implementation

To implement the authentication decorator, you need to determine the user name and password from where? Like files or databases; With authentication function; The code of the decorated object can be executed only after the authentication is passed; The outer function can only take one argument, the memory address of the decorated function. So the implementation code is as follows

def auth(db_type) :
    def outer(func) :  The index db_type can only be obtained from an external function due to the syntactic sugar limit of passing only one parameter
        def wrapper(*args, **kwargs) :
            user = input('name').strip()
            pwd = input('pwd').strip()
            if db_type == 'file':
                print('I'm logged in based on a file')
                if user == 'egon' and pwd == '123':
                    res = func(*args, **kwargs)
                    return res
                else:
                    print('Login failed')
            elif db_type == 'mysql':
                print('I'm logging in based on database MSQL')
            else:
                print('I don't support this type')
        return wrapper
    return outer

@auth('file')  # place the return result of auth('file') after @outer
def index(x, y) :
    print(x, y)
Copy the code

The reference decorator summary

Through the above code can be summed up the reference decorator universal template and the principle of the reference decorator.

defParamedic decorator (x,y,z) :
    def outer(func) :
        def wrapper(*args, **kwargs) :
            res = func(*args, **kwargs)
            return res
        return wrapper
    return outter

@ Parametrized decorator (1,y=2,z=3)  # execute the parameterized decorator (x, y, z), place the return value after @, and get the parameterless decorator @outer.
defDecorated object ():
    pass
Copy the code

wraps

The basic principle of decorators is to make the Wrapper function exactly the same as the original one: do not change the source code, do not change the way it is called, but add other functions. The decorator template summarized above has a slight flaw. For example, printing the name of the decorated function or the decorated function’s documentation will reveal that the function being called is not the original function.

import time

def timer(func) :
    def wrapper(*args,**kwargs) :
        start_time = time.time()
        print('hello')
        time.sleep(2)
        end = time.time()
        print(end-start_time)
        res = func(*args,**kwargs)


        return res
    return wrapper

@timer
def index(x,y) :
    Doc: I'm index :param x: :param y: :return:"
    print(x,y)

index(1.2)

print(index.__name__) Look at the function name, wrapper
print(index.__doc__) # Look at the function specification document None
Copy the code

How to solve this problem? You can refer to the following two methods:

- the way aAssign attributes of the original function to the Wrapper function manually
Function wrapper.__name__ = function.__name__
# 2, function wrapper.__doc__ = function
# wrapper.__name__ = func.__name__
# wrapper.__doc__ = func.__doc__Disadvantages: Each function has many attributes, do you really need to change them one by one? - Wraps: This is essentially a wrapper that assigns all the attributes of the original function to the Wrapper functionfrom functools import wraps
def outer(func) :
	# Pass the wraps function as an argument
	@wraps(func)
	def wrapper(*args,**kwargs)
		res = func(*args,**kwargs)
		return res
	return wrapper
Copy the code