What if you want to execute some other code before and after a function is executed, such as printing a log to output the calling status of this function?
#! /usr/bin/env python
# coding=utf-8
def logger(fn): #Function as a parameter, fn can be any parameter
def wrap(*args,**kwargs): #Variable parameters args and kwargs
print('call {}'.format(fn.__name__))
ret =fn(*args,**kwargs) #Parameter deconstruction during function call
print('{} called'.format(fn.__name__))return ret #Return the return value of the function
return wrap
def add(x, y):return x + y
logger_add =logger(add)print(logger_add.__name__)print(logger_add)
ret =logger_add(3,5)print(ret)
# Output result:
wrap
< function logger.<locals>.wrap at 0x7fba35f4fe18>
call add
add called
8
You can also use the following ways to achieve this effect
@ logger
def add(x, y):return x + y ret =add(3,5)print(ret)
# Output result:
call add
add called
8
This is a simple use of the Python decorator
Decorator is the name used for software design patterns. Decorators can dynamically change the functions of functions, methods or classes without directly using subclasses or changing the source code of the decorated function. Python decorator is a special change to Python syntax, which allows us to modify functions, methods and classes more conveniently.
When we write code in the following way:
@ logger
def add(x, y):...
It is the same as performing the following steps individually:
def add(x, y):...
logger_add =logger(add)
The code inside the decorator generally creates a new function, using *args
and **kwargs
to accept arbitrary parameters. The wrap() function in the above code is just like this. Inside this function, we need to call the original input function (the wrapped function, which is the input parameter of the decorator) and return its result. But you can also add any code you want to add, such as outputting the function call in the above code, you can also add timing processing and so on. This newly created wrap function will be returned as the result of the decorator, replacing the original function.
So in Python, the parameter of the decorator is a function, and the return value is a function of a function.
Write a decorator to calculate the execution time of a function
import time
def timethis(fn):
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()print(fn.__name__, end - start)return ret
return wrap
If you want to time the add function:
@ timethis
def add(x, y):return x + y
ret =add(3,5)print(ret)
# Output result
add 1.9073486328125e-068
If you want to time the sleep function:
@ timethis
def sleep(x):
time.sleep(x)sleep(3)
# Output result
sleep 3.003262519836426
For example, the name of the decorator, the doc of the decorator, and so on. We can use the dir function to list all the meta-information of the function: dir(sleep)
, the output is as follows
['__ annotations__','__call__','__class__','__closure__','__code__','__defaults__','__delattr__','__dict__','__dir__','__doc__','__eq__','__format__','__ge__','__get__','__getattribute__','__globals__','__gt__','__hash__','__init__','__kwdefaults__','__le__','__lt__','__module__','__name__','__ne__','__new__','__qualname__','__reduce__','__reduce_ex__','__repr__','__setattr__','__sizeof__','__str__','__subclasshook__']
You can see that there are a lot of meta-information. The two attributes we use are __name__
and __doc__
And the __doc__
attribute is the document information of the function, which can be viewed through the help function
Rewrite the application of the decorator 1: the sleep function in the timing process is as follows:
@ timeit
def sleep(x):'''This function is sleep.'''
time.sleep(x)sleep(3)print(sleep.__name__)print(sleep.__doc__)
The output of the above code is as follows:
3.0032713413238525
wrap
None
It can be found that the __name__
of the sleep function is wrap, not sleep, and the __doc__
attribute is empty, not the docstring of the sleep function. In other words, the meta-information of the function decorated by the decorator has changed. At this time, if the program needs the meta-information of the function, then there is a problem.
Take the two attributes of __name__
and __doc__
as examples
import time
def timeit(fn):
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()print(end - start)return ret
wrap.__doc__ = fn.__doc__ #Manual assignment__doc__information
wrap.__name__ = fn.__name__ #Manual assignment__name__information
return wrap
@ timeit
def sleep(x):'''This function is sleep.'''
time.sleep(x)if __name__ =="__main__":sleep(3)
# print(dir(sleep))print(sleep.__name__)print(sleep.__doc__)
The output is as follows
3.004547119140625
sleep
This function is sleep.
It can be found that the two attributes of __name__
and __doc__
are indeed successfully assigned.
We can rewrite the process of meta-information assignment as a function, as follows
import time
def copy_properties(src, dst): #Change the process of meta information assignment to function copy_properties
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
def timeit(fn):
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()print(end - start)return ret
copy_properties(fn, wrap) #Call copy_properties function to modify meta information
return wrap
@ timeit
def sleep(x):'''This function is sleep.'''
time.sleep(x)if __name__ =="__main__":sleep(3)
# print(dir(sleep))print(sleep.__name__)print(sleep.__doc__)
After this modification, the problem can also be solved.
Continue to modify the copy_properties function so that copy_properties can return a function
def copy_properties(src):
def _copy(dst): #Built-in one_The copy function is easy to return
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return _copy
def timeit(fn):
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()print(end - start)return ret
copy_properties(fn)(wrap) #Call copy_properties function
return wrap
The same can be a problem.
If you continue to modify the copy_properties function so that the _copy function is a decorator, pass in dst and return dst, and modify it as follows:
def copy_properties(src): #Fix dst first, and pass in src
def _copy(dst): #Incoming dst
dst.__name__ = src.__name__
dst.__doc__ = src.__doc__
return dst #Return dst
return _copy #Returns a decorator
def timeit(fn):
@ copy_properties(fn) #How to use parameter decorator
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()print(end - start)return ret
return wrap
copy_properties returns a decorator with parameters here, so you can decorate the wrap function directly according to the decorator's usage method. This process of modifying the copy_properties function is called function currying.
The @wraps decorator of the functools library is essentially an advanced version of the copy_properties function: it contains more function meta information. First check the help information of the wrap decorator:
import functools
help(functools.wraps)
The prototype of the wrap decorator function is:
wraps(wrapped, assigned=('module','name','qualname','doc','annotations'), updated=('dict',))
So this decorator will copy module and other meta information, but not all meta information, and will update the dict.
Examples of usage are as follows:
import time
import functools
def timeit(fn):
@ functools.wraps(fn) #Use of wraps decorator
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()print(end - start)return ret
return wrap
def sleep(x):
time.sleep(x)print(sleep.__name__)print(sleep.__doc__)
If the above timeit decorator, we need to output the name and execution time of the function whose execution time exceeds a few seconds (such as one second), then we need to pass in a parameter s to the decorator, which represents the incoming time interval, the default is 1s .
We can wrap a function timeitS outside the written decorator. The time interval s is passed in as a parameter of this function and is visible to the inner function, and then this function returns the written decorator.
import time
import functools
def timeitS(s):
def timeit(fn):
@ functools.wraps(fn)
def wrap(*args,**kwargs):
start = time.time()
ret =fn(*args,**kwargs)
end = time.time()if end - start > s:print('call {} takes {}s'.format(fn.__name__, end - start))else:print('call {} takes {}s less than {}'.format(fn.__name__, end - start, s))return ret
return wrap
return timeit
@ timeitS(2)
def sleep(x):
time.sleep(x)sleep(3)sleep(1)
The output is as follows:
call sleep takes 3.001342535018921s
call sleep takes 1.000471830368042s less than 2
Therefore, we can understand the decorator with parameters as:
Recommended Posts