Python 装饰器: Decorators
- PEP 318 – Decorators for Functions and Methods 描述了装饰器的语法以及决策过程
- Python Decorators 讨论了 Python 装饰器的历史,展示了关于装饰器不同提案实现语法及其优劣
- Python Decorator Library 展示了很多 关于 Python 装饰器的示例
- Decorators
- 装饰器
动机:Motivation
在装饰器语法出现之前,如果需要在不改动函数和方法 (function and method, 类中的函数称为方法) 的原始代码的情况下对函数行为做出改变或调整,需要将具体的改变置于函数体之后。对于大型函数来说,这将函数行为的关键组件与函数的其余外部接口的定义分离开来。如:
def foo(self):
perform method operation
foo = classmethod(foo)
对于较长的方法而言,可读性也会降低。对于概念上是一个声明的函数,将其命名三次似乎也显得不那么 Pythonic。这个问题的一个解决方案是将方法的转换移到更接近方法本身声明的地方。
def foo(cls):
pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)
新的装饰器语法的目的就是将函数变换部分放置在函数声明中 (新的语法形式借鉴了Java Annotations (Java 注解),Python Decorators 中介绍了装饰器语法的其他提案)。
@classmethod
@synchronized(lock)
def foo(cls):
pass
在 Python 2.4 中,只添加了关于函数和方法 (function and method) 的装饰器,而关于类的装饰器语法在 Python 3.0 中才添加 (见 PEP 3129 – Class Decorators)。类的装饰器语法与函数的语法一致,如下面的两个代码段是等价的。
class A:
pass
A = foo(bar(A))
@foo
@bar
class A:
pass
装饰器语法是纯粹的语法糖1,因为它使得改变函数的行为变得更加简单易读,但是去掉了它也并不影响语言所能实现的功能2。
语法
@dec2
@dec1
def func(arg1, arg2, ...):
pass
def func(arg1, arg2, ...):
pass
func = dec2(dec1(func))
以上两段代码是等价的,前者是使用了装饰器的语法,函数嵌套的顺序是自底向上的,类似数学中的复合函数,
(
g
⋅
f
)
(
x
)
(g \cdot f)(x)
(g⋅f)(x) 等价于
g
(
f
(
x
)
)
g(f(x))
g(f(x)),所以 @g @f def foo()
也等价于 foo=g(f(foo))
。
示例
简单用例
In [1]: def decorator(func):
...: def decorated():
...: """decorated docstring"""
...: pass
...:
...: return decorated
...:
In [2]: @decorator
...: def fun():
...: """fun docstring"""
...: pass
...:
In [3]: fun.__name__
Out[3]: 'decorated'
In [4]: fun.__doc__
Out[4]: 'decorated docstring'
在上面的例子中,fun
被 decorated
替代,函数名和文档字符串也都被改变。可以使用 functools.wraps
函数来保留原始的函数名及文档字符串。
In [1]: from functools import wraps
In [2]: def decorator(func):
...: @wraps(func)
...: def decorated():
...: """decorated docstring"""
...: pass
...:
...: return decorated
...:
In [3]: @decorator
...: def fun():
...: """fun docstring"""
...: pass
...:
In [4]: fun.__name__
Out[4]: 'fun'
In [5]: fun.__doc__
Out[5]: 'fun docstring'
functools 标准库中实现了一些高阶函数,即将函数作为参数的函数,可以用作装饰器来改变函数行为,如为函数提供缓存功能的装饰器 @functools.lru_cache
。
当被装饰的函数带有参数时
In [1]: from functools import wraps
In [2]: def decorator(func):
...: @wraps(func)
...: def decorated(*args, **kwargs):
...: """decorated docstring"""
...: print("In decorator")
...: return func(*args, **kwargs)
...:
...: return decorated
...:
In [3]: @decorator
...: def fun(s):
...: """fun docstring"""
...: print(s)
...:
In [4]: fun("In fun")
In decorator
In fun
认证: Authorization3
from functools import wraps
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, **kwargs)
return decorated
日志:Logging4
from functools import wraps
def logit(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
@logit
def addition_func(x):
"""Do some math."""
return x + x
result = addition_func(4)
# Output: addition_func was called
带参数的装饰器
有时候的装饰器可能带有参数,从而实现更复杂的流程控制。如上述的用法中 functools.wraps
就将一个函数作为了参数。
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function
Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
查看 functools.wraps
的源码可以看到,它调用了 partial
,进一步查看 partial
,发现它是一个类,并实现了特殊方法 __call__
,从而使得类本身也可以作为函数使用。
def __call__(*args, **keywords):
if not args:
raise TypeError("descriptor '__call__' of partial needs an argument")
self, *args = args
newkeywords = self.keywords.copy()
newkeywords.update(keywords)
return self.func(*self.args, *args, **newkeywords)
实际上,因为 Python 中一切皆为对象,函数也可以赋值给别的变量,装饰器作为语法糖,只是简化了函数作为参数的函数调用,所以只要作为装饰器是一个函数,并且这个函数的唯一传入参数也是函数就行。
使用函数嵌套实现带参数的装饰器
In [1]: from functools import wraps
In [2]: def greet(name):
...: def decorator(func):
...: @wraps(func)
...: def decorated(*args, **kwargs):
...: print(f"hello {name}")
...: return func(*args, **kwargs)
...: return decorated
...: return decorator
...:
In [3]: @greet("world")
...: def fun():
...: pass
...:
In [4]: fun()
hello world
In [5]: @greet("universe")
...: def bar():
...: pass
...:
In [6]: bar()
hello universe
使用类实现带参数的装饰器
In [1]: from functools import wraps
In [2]: class greet:
...: def __init__(self, name):
...: self.name = name
...:
...: def __call__(self, func):
...: @wraps(func)
...: def decorated(*args, **kwargs):
...:
...: print(f"hello {self.name}")
...: return func(*args, **kwargs)
...:
...: return decorated
...:
In [3]: @greet("world")
...: def fun():
...: pass
...:
In [4]: fun()
hello world
In [5]: @greet("universe")
...: def bar():
...: pass
...:
In [6]: bar()
hello universe
总结
- Python 中一切皆对象。
- Python 作为语法糖,简化了函数调用写法。
- 在编写 Python 装饰器实现时,要注意区别什么是函数,什么是函数调用。