12 easy steps to get the Python decorator
1. Function
In python, functions are defined by def
keywords, function names, and optional parameter lists. return
Return 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 globals
returns 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 foo
to function inside 本地作用域
the contents inside printed. We can see that functions foo
have 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 foo
the 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_string
the 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 x
the value of the variable , because it does not exist at this time! Function foo
namespace 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 x
and 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, y
we 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 3
passed to the first parameter and 1
passed 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 x
local 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 x
is a local variable, but as mentioned earlier, the function inner
can 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 inner
only a variable name that follows the python variable parsing rules. The python interpreter will look for matching variables outer
in 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. add
And sub
it 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 key
parameters 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 inner
return the variable that happens to be the function identifier as the return value. There is no special syntax for this: " inner
Return the function , otherwise it will not be called at all." Remember the lifetime of the variable? Each time the function outer
is called, the function inner
will 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 foo
is 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 inner
is outer
returned 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 x
is outer
a local variable in the function. When the function inner
is printed at #1 x
, the python interpreter will inner
look 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 x
is outer
a local variable of the function , which means that only when the function outer
is running Will exist. According to our known python operating mode, we cannot outer
continue to call the function after the function returns inner
. When the function inner
is called, the variable x
no longer exists, and a runtime error may occur.
Never expected, return The function inner
actually 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_closure
The attribute concludes that this attribute contains the value in the closed scope (only the captured value is included, for example x
, if outer
other values are defined in it, there will be no in the closed scope).
Remember, Every time the function outer
is called, the functioninner
Will be redefined. Now x
the value of the variable will not change, so the function returned inner
will 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 1
or 2
to 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: outer
like a inner
constructor for a service, x
like 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 lambda
method to sort a list of lists based on the second element instead of the first. Now you might also write a itemgetter
method 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_func
parameter, and in it we define a nested function inner
. inner
Will print a string of characters, and then call some_func
, get its return value at #1. In outer
each invocation of some_func
values may be different, but no matter some_func
how values, we will call it. Finally, the inner
return some_func() + 1
value-we decorated
can see the printed string and the return value 2
by calling the function stored in the variable at #2 , instead foo
of the return value obtained by calling the function as expected 1
.
We can think of variables decorated
as foo
a 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, three
that the value to be one
subtracted without changing the coordinate object two
is {x: 0, y: 0}
, one
and three
the 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 x
and y
replace 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 one
simply 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 *args
are 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 args
capture 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 *args
either , 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 **kwargs
to show that all key parameters are not captured should be stored in kwargs
the dictionary. As mentioned earlier, args
he is kwargs
not 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.