12 easy steps to get the Python decorator

12 easy steps to get the Python decorator

1. Function

In python, functions are defined by defkeywords, function names, and optional parameter lists. returnReturn value by keyword. We give an example to illustrate how to define and call a simple function:

>>> def foo():
...     return 1
>>> foo()
1

The method body (of course, multiple lines are the same) is necessary. It is indicated by indentation. Adding double brackets after the method name ()can call the function

2. Scope

In python, the function creates a new scope. Python developers may say that functions have their own namespace, which is almost the same. This means that when a variable is encountered inside a function, the function will first look for it in its own namespace. Let's write a simple function to look at 本地作用域and 全局作用域what is the difference:

>>> a_string = "This is a global variable"
>>> def foo():
...     print locals()
>>> print globals()
{..., 'a_string': 'This is a global variable'}
>>> foo() # 2
{}

The built-in function globalsreturns a dictionary containing all the variable names known by the python interpreter (for clean and clean, I omitted some variables created by python). # 2 I call a function footo function inside 本地作用域the contents inside printed. We can see that functions foohave their own independent namespace, although there is nothing in the namespace for the time being.

3. Variable resolution rules

Of course, this does not mean that we cannot access external global variables in the function. In the scope rules of python, creating a variable will definitely create a variable in the current scope, but when accessing or modifying a variable, the variable will be searched in the current scope first, and if no matching variable is found, it will turn up in order to close the effect. Look inside the domain. So if we modify foothe implementation of the function so that it prints the variables in the global scope:

>>> a_string = "This is a global variable"
>>> def foo():
...     print a_string # 1
>>> foo()
This is a global variable

At #1, the python interpreter will try to find the variable a_string. Of course, 本地作用域it can't be found in the function , so it will go to the upper scope to find it.
But on the other hand, if we assign values ​​to global variables inside the function, the result is different from what we thought:

>>> a_string = "This is a global variable"
>>> def foo():
...     a_string = "test" # 1
...     print locals()
>>> foo()
{'a_string': 'test'}
>>> a_string # 2
'This is a global variable'

We can see that global variables can be accessed (if it is a variable data type (such as list, dict) can even be changed) but the assignment cannot. At #1 inside the function, we actually 新创建have a local variable, a variable with 隐藏the same name in the global scope. We can draw this conclusion by printing out the contents of the local namespace. We can also see that a_stringthe value of the variable printed at #2 has not changed.

4. Variable life cycle

It is worth noting that variables not only live in one namespace, they also have their own life cycle. Please see the following example:

>>> def foo():
...     x = 1
>>> foo()
>>> print x # 1
Traceback (most recent call last):
  ...
NameError: name 'x' is not defined

The error that occurs at #1 is not only 作用域规则caused by (although this is the cause of the NameError error), it is also related to the mechanism of function call implementation in python and many other programming languages. At this point of execution time, there is no effective syntax for us to get xthe value of the variable , because it does not exist at this time! Function foonamespace with the start of a function call start, end and destroyed.

5. Function parameters

Python allows us to pass parameters to functions, and the parameters become local variables and exist inside the function.

>>> def foo(x):
...     print locals()
>>> foo(1)
{'x': 1}

There are many ways to define and pass parameters in Python. For the full version, you can view the official Python documentation . Let's briefly explain here: the parameters of the function can be required 位置参数or optional 命名,默认参数.

>>> def foo(x, y=0): # 1
...     return x - y
>>> foo(3, 1) # 2
2
>>> foo(3) # 3
3
>>> foo() # 4
Traceback (most recent call last):
  ...
TypeError: foo() takes at least 1 argument (0 given)
>>> foo(y=1, x=3) # 5
2

At #1, we define the function foo, which has a positional parameter xand a named parameter y. At #2, we can call the function in the usual way. Although there is a named parameter, the parameter can still be passed to the function by position. When calling the function, ywe can also completely ignore the named parameters as shown in #3. If the named parameter does not receive any value, python will automatically use the declared default value 0. It should be noted that we cannot omit the first positional parameter x, otherwise an error will occur as shown in #5.
It's pretty concise and clear for now, but it might be a bit confusing next. Python supports named parameters when calling functions (personally think it should be named actual parameters). Look at the function call at #5. We are passing two named actual parameters. At this time, because of the name identification, the order of parameter passing does not matter.
Of course, the opposite is true: the second parameter of the function is y, but we pass the value to it by position. In the function call at #2 foo(3,1), we 3passed to the first parameter and 1passed to the second parameter, even though the second parameter was a named parameter.
Sang can not afford to feel good with a great while to get it clear such a simple concept: the function parameters can have 名称and 位置. This means that the definition of the function and the call will be slightly different in understanding. We can pass named parameters (actual parameters) to functions that only define positional parameters, and vice versa! If you feel that it is not enough, you can check the official documentation

6. Nested functions

Python allows the creation of nested functions. This means that we can define functions inside functions and the existing scope and variable life cycle still apply.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

This example is a bit complicated, but it looks fine. Think about what happened in #1: The python interpreter needs to find a xlocal variable called , and after the search fails, it will continue to look in the upper scope, which is defined in another function. For a function outer, a variable xis a local variable, but as mentioned earlier, the function innercan access a closed scope (at least it can be read and modified). At #2, when we call the function inner, it is very important that it is inneronly a variable name that follows the python variable parsing rules. The python interpreter will look for matching variables outerin the variable name first in the scope inner.

7. Functions are first-class objects in the python world

Obviously, functions are objects in python just like everything else. (It should be singing loudly here) Ah! You are not that special with functions that contain variables!

>>> issubclass(int, object) # all objects in Python inherit from a common baseclass
True
>>> def foo():
...     pass
>>> foo.__class__ # 1
<type 'function'>
>>> issubclass(foo.__class__, object)
True

You may never have thought that the function you define actually has attributes. No way, functions are objects in python, just like other things. Perhaps this description would be too academic and official: in python, functions are just some ordinary values ​​and they are the same as other values. This means that you can pass a function to other functions like parameters or return a function from a function! If you have never thought about it this way, take a look at the following example:

>>> def add(x, y):
...     return x + y
>>> def sub(x, y):
...     return x - y
>>> def apply(func, x, y): # 1
...     return func(x, y) # 2
>>> apply(add, 2, 1) # 3
3
>>> apply(sub, 2, 1)
1

This example should not seem strange to you. addAnd subit is a very common python two functions, takes two values and returns the result of a calculation value. At #1, you can see that the variable to receive a function is just a normal variable, just like other variables. At #2 we call the passed in function: " ()represents the operation of the call and the value contained in the call variable. At #3, you can also see that there is no special syntax for the transfer function." The name of the function is just other The table identifier is the same as the variable.
You may have seen this behavior: "Python turns frequently used operations into functions and uses them as parameters, like passing a function to the keyparameters of the built-in sorting function to customize the sorting rules. Then the function is regarded as the return value. What about this situation:

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
...
>>> foo = outer() #2
>>> foo
<function inner at 0x...>
>>> foo()
Inside inner

This example may look even stranger. At #1, I innerreturn the variable that happens to be the function identifier as the return value. There is no special syntax for this: " innerReturn the function , otherwise it will not be called at all." Remember the lifetime of the variable? Each time the function outeris called, the function innerwill be redefined. If it is not returned as a variable, it will cease to exist after each execution.
At #2, we capture the return value-function inner, and store it in a new variable foo. We can see that when a variable foois evaluated, it does contain a function inner, and we can call it. It might seem strange at first, but it's not difficult to understand, right. Hold on, because the weird turning point is coming soon (hehehehe, I don’t laugh awkwardly!)

8. Closures

Let's not rush to define what a closure is, let's take a look at a piece of code, just a simple adjustment of the previous example:

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure
(<cell at 0x...: int object at 0x...>,)

In the previous example, we learned that it inneris outerreturned as a function , saved in a variable foo, and we can call it foo(). But will it run normally? Let's first look at the scope rules.
Everything works under the scope rules of python: "It xis outera local variable in the function. When the function inneris printed at #1 x, the python interpreter will innerlook for the corresponding variable internally, and of course it will not find it. So then it will look in the closed scope and find a match.
But from the life cycle of the variable, how should we understand it? Our variable xis outera local variable of the function , which means that only when the function outeris running Will exist. According to our known python operating mode, we cannot outercontinue to call the function after the function returns inner. When the function inneris called, the variable xno longer exists, and a runtime error may occur.
Never expected, return The function inneractually works normally. Python supports a 函数闭包feature called , in human terms, the function nested in 非全局作用域it can remember the enclosed namespace when it was defined . This can be done by looking at the function’s func_closureThe attribute concludes that this attribute contains the value in the closed scope (only the captured value is included, for example x, if outerother values ​​are defined in it, there will be no in the closed scope).
Remember, Every time the function outeris called, the functioninnerWill be redefined. Now xthe value of the variable will not change, so the function returned innerwill have the same logic every time . What if we change it a bit?

>>> def outer(x):
...     def inner():
...         print x # 1
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

From this example, you can see 闭包that-the closed scope remembered by the function-can be used to create a custom function, which is essentially one 硬编码的参数. In fact, we are not passing parameters 1or 2to functions inner, we are actually creating various customized versions that can print various numbers.
Closure alone is a very powerful feature. In some respects, you might think of it as an object-oriented technology: outerlike a innerconstructor for a service, xlike a private variable. There are many ways to use closures: if you are familiar with the parameters of python's built-in sorting method key, you might have written a lambdamethod to sort a list of lists based on the second element instead of the first. Now you might also write a itemgettermethod that receives an index value to return a perfect function, and passes it to the sorting function's parameters key.
However, we will not use closures to do such low things now (⊙o⊙)...! Instead, 让我们再爽一次write a tall one 装饰器!

9. Decorator

A decorator is actually a closure that takes a function as a parameter and returns an alternative function. We step by step from simple to complex to take a look:

>>> def outer(some_func):
...     def inner():
...         print "before some_func"
...         ret = some_func() # 1
...         return ret + 1
...     return inner
>>> def foo():
...     return 1
>>> decorated = outer(foo) # 2
>>> decorated()
before some_func
2

Take a closer look at the decorator example above. We define a function outer, which has only one some_funcparameter, and in it we define a nested function inner. innerWill print a string of characters, and then call some_func, get its return value at #1. In outereach invocation of some_funcvalues may be different, but no matter some_funchow values, we will call it. Finally, the innerreturn some_func() + 1value-we decoratedcan see the printed string and the return value 2by calling the function stored in the variable at #2 , instead fooof the return value obtained by calling the function as expected 1.
We can think of variables decoratedas fooa decorated version of a function , an enhanced version. In fact, if we plan to write a useful decorator, we might want to completely replace the original function with a decorated version foo, so that we will always get our "enhanced version" foo. To achieve this effect, you don't need to learn new syntax at all, just simply assign values ​​to variables foo:

>>> foo = outer(foo)
>>> foo # doctest: +ELLIPSIS
<function inner at 0x...>

Now, any call will not involve the original function foo, and will get a new decorated version foo. Now let's write a useful decorator.
Imagine we have a library that can provide objects with similar coordinates, maybe they are just some coordinate pairs of x and y. Unfortunately, these coordinate objects do not support mathematical operators, and we cannot modify the source code, so we cannot directly add operator support. We will do a series of mathematical operations, so we want functions that can perform appropriate addition and subtraction operations on two coordinate objects. These methods are easy to write:

>>> class Coordinate(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return "Coord: " + str(self.__dict__)
>>> def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)
>>> def sub(a, b):
...     return Coordinate(a.x - b.x, a.y - b.y)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}

What if, unfortunately, our addition and subtraction functions also require some boundary checking behavior? Maybe you can only perform addition and subtraction operations on positive coordinate objects, and any returned value should also be positive coordinates. So now the expectation is this:

>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}

We expect one, two, threethat the value to be onesubtracted without changing the coordinate object twois {x: 0, y: 0}, oneand threethe value to be added is {x: 100, y: 200}. Instead of adding the logic of boundary checking for parameters and return values ​​to each method, let's write a decorator for boundary checking!

>>> def wrapper(func):
...     def checker(a, b): # 1
...         if a.x < 0 or a.y < 0:
...             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
...         if b.x < 0 or b.y < 0:
...             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
...         ret = func(a, b)
...         if ret.x < 0 or ret.y < 0:
...             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
...         return ret
...     return checker
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}

This decorator can work like the previous decorator example, returning a modified function, but in this example, it can do some very useful checks and formatting work on the input parameters and return value of the function. values xand yreplace 0.
Obviously, in this way, our code becomes more concise: Isolate the logic of boundary checking into a separate method, and then apply it to the place where we need to check through the decorator package. Another way can achieve the same purpose by calling the boundary check method at the beginning of the calculation method and before the return value. But it cannot be denied that the use of decorators allows us to achieve the purpose of coordinate boundary checking with the least amount of code. In fact, if we are decorating our own defined method, we can make the decorator application more compelling.

Its basic function is to pass in a function, perform some optimizations on the passed function in the decorator function, and then pass the optimized function out. Therefore, in the decorator function, an internal function (the optimized function that will be sent out) is defined, and the internal function will call the incoming function to achieve the basic function and be returned by the decorator function.

10. Use @ identifier is applied to the decorative function

Python 2.4 supports the use of identifiers to @apply decorators to functions. You only need to add @the name of the decorator before the definition of the function . In the example in the previous section, we replaced the original method with the decorated method:

>>> add = wrapper(add)

In this way, any method can be packaged at any time. But if we customize a method, we can use it @to decorate:

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

It needs to be understood that this approach is the same as the previous simple packaging method to replace the original method. Python just adds some syntactic sugar to make the decoration behavior more direct, clear and elegant.

11. *args and **kwargs

We have completed a useful decorator, but due to hard coding, it can only be applied to a specific type of method. This type of method receives two parameters and passes them to the function captured by the closure. What if we want to implement a decorator that can be applied to any method? For another example, if we want to implement a counter-like decorator that can be applied to any method, there is no need to change any logic of the original method. This means that the decorator can accept a function with any signature as its decorated method, and can call the decorated method with the parameters passed to it.
Coincidentally, Python happens to have a syntax that supports this feature. You can read Python Tutorial for more details. Used when defining the function, it *means that the parameters passed by position will be placed in *the variable with the prefix, so:

>>> def one(*args):
...     print args # 1
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args): # 2
...     print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)

The first function onesimply says that any positional parameters passed in are printed out. As you can see, in code #1, we only quoted the variables in the function args, which *argsare only used to indicate the position when the function is defined. Parameters should be stored in variables args. Python allows us to specify some parameters and argscapture all other remaining uncaptured positional parameters, as shown in #2.
*Operators can also be used when the function is called. The meaning is basically the same. When calling a function, a *marked variable means that the contents of the variable need to be extracted and used as a positional parameter. Similarly, let's look at an example:

>>> def add(x, y):
...     return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) # 1
3
>>> add(*lst) # 2
3

The code at #1 and the code at #2 do the same thing. At #2, what python does for us can actually be done manually. This is not a bad thing *argseither , either it means that additional parameters can be obtained from an iterable list when calling the method, or it means that the method can accept arbitrary positional parameters when defining the method.
The next mentioned **will be a bit more and more complicated. **The parameter dictionary representing key-value pairs *is almost the same as what it represents. It is also very simple, right:

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

When we define a function, we can use **kwargsto show that all key parameters are not captured should be stored in kwargsthe dictionary. As mentioned earlier, argshe is kwargsnot part of the python grammar, but it is an unwritten convention to use such variable names when defining functions. And the *same, we can also use it when defining or calling functions **.

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     return x + y
>>> bar(**dct)
3

12. More general decorators

With this new skill, we can casually write a decorator that can record the parameters passed to the function. Let's first give an example of simply outputting the log to the interface:

>>> def logger(func):
...     def inner(*args, **kwargs): #1
...         print "Arguments were: %s, %s" % (args, kwargs)
...         return func(*args, **kwargs) #2
...     return inner

Please pay attention to our function inner, it can accept any number and type of parameters and pass them to the wrapped method, which allows us to decorate any method with this decorator.

>>> @logger
... def foo1(x, y=1):
...     return x * y
>>> @logger
... def foo2():
...     return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
20
>>> foo1(1)
Arguments were: (1,), {}
1
>>> foo2()
Arguments were: (), {}
2

Just call which method we define, and the corresponding log will be printed to the output window, as we expected.

Guess you like

Origin blog.csdn.net/a40850273/article/details/88313032