Decorator
根据定义,装饰器是一个函数,它接受另一个函数并扩展后者的行为,而不显式地修改它。
这听起来,有点令人困惑,但它真的不是,尤其在你看到了几个例子关于修饰器如何工作,你能够找到所有的例子。
函数和修饰器密切相关
第一级对象——python将函数视为对象
在Python中,函数是第一级对象。这意味着函数可以被传递当作参数来使用。就像其他任何的对象一样。考虑下面三个函数:
def say_hello(name):
return f"Hello {name}"
def be_awesome(name):
return f"Yo {name}, together we are the awesomest!"
def greet_bob(greeter_func):
return greeter_func("Bob")
内联函数
就是函数内部定义的函数。
def parent():
print("Printing from the parent() function")
def first_child():
print("Printing from the first_child() function")
def second_child():
print("Printing from the second_child() function")
second_child()
first_child()
通过函数返回函数
Python还允许使用函数作为返回值。下面的示例从外部父()函数返回一个内部函数:
def parent(num):
def first_child():
return "Hi, I am Emma"
def second_child():
return "Call me Liam"
if num == 1:
return first_child
else:
return second_child
就像这样:
>>> first = parent(1)
>>> second = parent(2)
>>> first
<function parent.<locals>.first_child at 0x7f599f1e2e18>
>>> second
<function parent.<locals>.second_child at 0x7f599dad5268>
有些神秘的输出仅仅意味着第一个变量引用parent()内部的本地first_child()函数,而第二个变量指向second_child()。
现在可以像使用常规函数一样使用first和second,即使它们指向的函数不能直接访问:
>>> first()
'Hi, I am Emma'
>>> second()
'Call me Liam'
简单的修饰器
既然您已经了解了函数与Python中的任何其他对象一样,那么您就可以继续了解Python装饰器了。让我们从一个例子开始:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
def say_whee():
print("Whee!")
say_whee = my_decorator(say_whee)
运行后,
>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.
要理解这里发生了什么,请回顾前面的示例。我们只是应用了你目前所学到的所有知识。
所谓的装饰发生在下面这一行:
say_whee = my_decorator(say_whee)
实际上,名称say_whee现在指向wrapper()内部函数。记住,当您调用my_decorator(say_whee)时,将包装器作为函数返回:
>>> say_whee
<function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>
然而,wrapper()将原始say_whee()引用为func,并在两个调用之间调用该函数来打印()。
简单地说:装饰器包装一个函数,修改它的行为。
在继续之前,让我们看一下第二个例子。因为wrapper()是一个常规的Python函数,装饰器修改函数的方式可以动态地改变。为了不打扰邻居,下面的例子只会在白天运行经过修饰的代码:
from datetime import datetime
def not_during_the_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
else:
pass # Hush, the neighbors are asleep
return wrapper
def say_whee():
print("Whee!")
say_whee = not_during_the_night(say_whee)
>>> say_whee()
>>>
糖衣语法
上面装饰say_whee()的方法有点笨拙。首先,您需要输入三次say_whee名称。此外,装饰会隐藏在函数的定义之下。
相反,Python允许您以更简单的方式使用修饰符@符号,有时称为“pie”语法。下面的例子和第一个装饰器例子做的是一样的:
原来:
def say_whee():
print("Whee!")
say_whee = not_during_the_night(say_whee)
使用@后:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_whee():
print("Whee!")
即是将say_whee传入装饰器,返回一个新的函数(装饰器)。
所以,@my_decorator只是说say_whee = my_decorator(say_whee)的一种更简单的方式。这就是如何将装饰器应用于函数。
有参数的装饰器函数
from decorators import do_twice
@do_twice
def greet(name):
print(f"Hello {name}")
>>> greet("World")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given
问题是内部函数wrapper_do_twice()不接受任何参数,但是name="World"被传递给它。您可以通过让wrapper_do_twice()接受一个参数来解决这个问题,但是这样它就不能用于前面创建的say_whee()函数。
解决方案是在内部包装函数中使用*args和kwargs**。然后它将接受任意数量的位置和关键字参数。重写decorator .py如下:
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice
从修饰函数返回值
from decorators import do_twice
@do_twice
def return_greeting(name):
print("Creating greeting")
return f"Hi {name}"
使用它后,
>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
None
我们的返回值去哪了呢?
因为do_twice_wrapper()没有显式地返回值,所以调用return_greeting(“Adam”)最终没有返回值。
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
到此,我们可以实现了正确返回。
那么,被修饰的函数是谁?
在使用Python时,尤其是在交互式shell中,它强大的内省能力是一个极大的便利。自省是对象在运行时了解自身属性的能力。例如,一个函数知道它自己的名称和文档:
>>> print
<built-in function print>
>>> print.__name__
'print'
>>> help(print)
Help on built-in function print in module builtins:
print(...)
<full help message>
自省也适用于你定义自己的函数:
>>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>
>>> say_whee.__name__
'wrapper_do_twice'
>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:
wrapper_do_twice()
但是,在修饰之后,say_whee()对它的标识非常困惑。现在它报告为do_twice()装饰器中的wrapper_do_twice()内部函数。虽然在技术上是正确的,但这不是非常有用的信息
要解决这个问题,装饰器应该使用@functools。包装装饰器,它将保存关于原始函数的信息。再次更新decorators.py:
import functools
def do_twice(func):
@functools.wraps(func)
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
那么,
>> say_whee
<function say_whee at 0x7ff79a60f2f0>
>>> say_whee.__name__
'say_whee'
>>> help(say_whee)
Help on function say_whee in module whee:
say_whee()
技术细节:@functools.wrap 装饰器使用函数functools.update_wrapper()来更新内省中使用的特殊属性,就像__name__和__doc__。
一些真实的例子
让我们看一些更有用的装饰器示例。你会注意到它们主要遵循的模式与你目前所学的相同:
这是一个很好的模板。
import functools
def decorator(func):
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# Do something before
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_decorator
计时函数
让我们从创建一个@timer装饰器开始。它将度量函数执行所需的时间,并将持续时间打印到控制台。这是代码:
import functools
import time
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
运行,
>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs
>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs
**注意:**如果您只是想了解函数的运行时,@timer装饰器非常棒。如果希望对代码进行更精确的度量,则应该考虑标准库中的timeit模块。它暂时禁用垃圾收集,并运行多次试验来消除快速函数调用中的噪声。
调试代码
import functools
def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug
签名是通过连接所有参数的字符串表示创建的。以下列表中的数字对应于代码中编号的注释:
- 创建位置参数列表。使用repr()获得一个表示每个参数的漂亮字符串。
- 创建关键字参数列表。f-string将每个参数格式化为key=value,其中!r说明符表示使用repr()表示该值。
- 位置参数和关键字参数列表连接到一个签名字符串,每个参数之间用逗号分隔。
- 返回值在函数执行后打印。
让我们看看装饰器如何在实践中工作,将它应用到一个简单的函数,一个位置和一个关键字参数:
@debug
def make_greeting(name, age=None):
if age is None:
return f"Howdy {name}!"
else:
return f"Whoa {name}! {age} already, you are growing up!"
执行,
>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'
>>> make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Whoa Richard! 112 already, you are growing up!'
>>> make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)
'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
'Whoa Dorrisile! 116 already, you are growing up!'
这个例子可能不会立即有用,因为@debug装饰器只是重复您刚刚编写的内容。当它应用于您自己不直接调用的小型方便函数时,它会更强大。
数学例子
import math
from decorators import debug
# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)
def approximate_e(terms=18):
return sum(1 / math.factorial(n) for n in range(terms))
这个示例还展示了如何将装饰器应用于已经定义的函数。e的近似基于以下级数展开:
调用approate_e()函数时,可以看到@debug装饰器在工作:
approximate_e(5)
Calling factorial(0)
‘factorial’ returned 1
Calling factorial(1)
‘factorial’ returned 1
Calling factorial(2)
‘factorial’ returned 2
Calling factorial(3)
‘factorial’ returned 6
Calling factorial(4)
‘factorial’ returned 24
2.708333333333333
在本例中,只添加5项,就得到了e = 2.718281828的近似值。
减速代码
下一个例子可能不是很有用。为什么要降低Python代码的速度?可能最常见的用例是您希望对一个函数进行速率限制,该函数不断地检查资源(如web页面)是否发生了更改。@slow_down装饰器会在调用修饰函数之前休眠一秒钟:
import functools
import time
def slow_down(func):
"""Sleep 1 second before calling the function"""
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
time.sleep(1)
return func(*args, **kwargs)
return wrapper_slow_down
@slow_down
def countdown(from_number):
if from_number < 1:
print("Liftoff!")
else:
print(from_number)
countdown(from_number - 1)
要查看@slow_down装饰器的效果,您确实需要亲自运行示例:
>>> countdown(3)
3
2
1
Liftoff!
注意:countdown()函数是一个递归函数。换句话说,它是一个调用自身的函数。要了解更多关于Python中的递归函数的知识,请参阅我们的Python中递归思维指南。
@slow_down装饰器总是休眠一秒钟。稍后,您将看到如何通过向装饰器传递参数来控制速率。
注册插件
装饰师不需要包装他们正在装饰的功能。它们还可以简单地注册一个函数的存在并返回unwrapped。例如,这可以用来创建轻量级插件架构:
import random
PLUGINS = dict()
def register(func):
"""Register a function as a plug-in"""
PLUGINS[func.__name__] = func
return func
@register
def say_hello(name):
return f"Hello {name}"
@register
def be_awesome(name):
return f"Yo {name}, together we are the awesomest!"
def randomly_greet(name):
greeter, greeter_func = random.choice(list(PLUGINS.items()))
print(f"Using {greeter!r}")
return greeter_func(name)
@register装饰器只是在全局插件dict中存储对被装饰函数的引用。注意,您不必编写内部函数或使用@functools。在本例中进行包装,因为您将返回未修改的原始函数。
>>> PLUGINS
{'say_hello': <function say_hello at 0x7f768eae6730>,
'be_awesome': <function be_awesome at 0x7f768eae67b8>}
>>> randomly_greet("Alice")
Using 'say_hello'
'Hello Alice'
这个简单插件架构的主要好处是,您不需要维护存在哪些插件的列表。该列表是在插件注册时创建的。这使得添加新插件变得很简单:只需定义函数并用@register修饰它。
如果您熟悉Python中的globals(),您可能会发现它与插件体系结构的工作方式有一些相似之处。globals()允许访问当前范围内的所有全局变量,包括您的插件:
>>> globals()
{..., # Lots of variables not shown here.
'say_hello': <function say_hello at 0x7f768eae6730>,
'be_awesome': <function be_awesome at 0x7f768eae67b8>,
'randomly_greet': <function randomly_greet at 0x7f768eae6840>}
使用@register装饰器,您可以创建自己的有趣变量列表,有效地从globals()中手动选择一些函数。
用户登陆
在继续介绍一些更高级的装饰器之前,最后一个示例通常用于处理web框架。在本例中,我们使用Flask设置了一个/secret web页面,这个页面应该只对登录或者通过其他认证的用户可见:
from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)
def login_required(func):
"""Make sure user is logged in before proceeding"""
@functools.wraps(func)
def wrapper_login_required(*args, **kwargs):
if g.user is None:
return redirect(url_for("login", next=request.url))
return func(*args, **kwargs)
return wrapper_login_required
@app.route("/secret")
@login_required
def secret():
...
虽然这提供了如何向web框架添加身份验证的概念,但是通常不应该自己编写这些类型的装饰器。对于Flask,您可以使用Flask- login扩展,这增加了更多的安全性和功能。