Since a function is also an object, and a function object can be assigned to a variable, the function can also be called through that variable.
>>> def now():
... print('2024-6-1')
...
>>> f = now
>>> f()
2024-6-1
A function object has a __name__ attribute (note: two underscores before and after), which allows you to retrieve the function’s name:
>>> now.__name__
'now'
>>> f.__name__
'now'
Now, suppose we want to enhance the functionality of the now() function, for example, by automatically printing logs before and after the function call, but without modifying the definition of now(). This way of dynamically adding functionality during code execution is called a Decorator.
Essentially, a decorator is a higher-order function that returns a function. Therefore, to define a decorator that prints logs, we can do the following:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
Looking at the log function above, since it is a decorator, it takes a function as an argument and returns a function. We use Python’s @ syntax to place the decorator at the function’s definition:
@log
def now():
print('2024-6-1')
Calling the now() function will not only execute the now() function itself but also print a log line before executing now():
>>> now()
call now():
2024-6-1
Placing @log at the definition of the now() function is equivalent to executing the statement:
now = log(now)
Since log() is a decorator that returns a function, the original now() function still exists, but now the variable now with the same name points to the new function. Thus, calling now() will execute the new function, which is the wrapper() function returned within the log() function.
The parameter definition of the wrapper() function is (*args, **kw), so the wrapper() function can accept calls with any parameters. Inside the wrapper() function, it first prints the log and then immediately calls the original function.
If the decorator itself needs to take parameters, we need to write a higher-order function that returns a decorator, which is more complex. For example, to customize the log text:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
This 3-level nested decorator is used as follows:
@log('execute')
def now():
print('2024-6-1')
The execution result is as follows:
>>> now()
execute now():
2024-6-1
Compared with the 2-level nested decorator, the effect of the 3-level nested one is like this:
>>> now = log('execute')(now)
Let’s analyze the above statement: first, log('execute') is executed, which returns the decorator function. Then, the returned function is called with the now function as the parameter, and the final return value is the wrapper function.
Both of the above decorator definitions are correct, but there is one last step missing. As we mentioned, a function is also an object and has attributes like __name__, but if you check the function decorated by the decorator, its __name__ has changed from the original 'now' to 'wrapper'.
This is because the wrapper() function returned by the decorator has overwritten the original function’s name. This can cause errors in some code that relies on the function signature.
There’s no need to write code like wrapper.__name__ = func.__name__ manually. Python’s built-in functools.wraps is designed for this purpose. Therefore, a complete decorator should be written as follows:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
Or for a decorator with parameters:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
import functools imports the functools module. The concept of modules will be explained later. For now, just remember to add @functools.wraps(func) before defining wrapper().
Please design a decorator that can be applied to any function and prints the execution time of that function:
import time, functools
def metric(fn):
print('%s executed in %s ms' % (fn.__name__, 10.24))
return fn
# Test
@metric
def fast(x, y):
time.sleep(0.0012)
return x + y;
@metric
def slow(x, y, z):
time.sleep(0.1234)
return x * y * z;
f = fast(11, 22)
s = slow(11, 22, 33)
if f != 33:
print('Test failed!')
elif s != 7986:
print('Test failed!')
Please write a decorator that can print the logs ‘begin call’ and ‘end call’ before and after a function call.
Also, think about whether you can write an @log decorator that supports both:
@log
def f():
pass
and:
运行
@log('execute')
def f():
pass
decorator.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print("call %s():" % func.__name__)
return func(*args, **kw)
return wrapper
@log
def now():
print("2024-6-1 12:34")
now()
def logger(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print("%s %s():" % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
@logger("DEBUG")
def today():
print("2024-6-1")
today()
print(today.__name__)
In Object-Oriented Programming (OOP) design patterns, the decorator is known as the Decorator Pattern. The OOP Decorator Pattern needs to be implemented through inheritance and composition. However, in addition to supporting the OOP-style decorator, Python directly supports decorators at the syntactic level. Python’s decorators can be implemented using functions or classes.
Decorators can enhance the functionality of functions. Although their definition is a bit complex, they are very flexible and convenient to use.