译注:本文译自 StackOverFlow 上对一个问题的回答。虽然提的问题不是很难,但回答者却非常认真,详述了 Python 中装饰器,是一个很不错的入门和进阶的材料。而且作者的回答很容易看懂,大家可以直接看英文。不过,为了更加深入理解装饰器,本人还是翻译一下。另外,作者在回答的时候,有较多的口语化描述,所以本文在部分地方会进行意译和删节。
装饰器基础
Python 函数是对象
为了理解装饰器,你首先必须理解,在 Python 中,函数是对象。理解这点有重要意义(consequences)。让我们通过一个简单例子来了解:
1 | def shout(word="yes"): |
Python 函数的另一个有趣的特性就是它们能够被定义在其他函数的内部!
1 | def talk(): |
函数引用
好了,还不理解?现在开始进入有趣的部分…
你已经知道函数是对象,所以,函数能够:
赋给一个变量
能够被定义在另一个函数内部
这意味着一个函数能够返回另一个函数
。
1 | def getTalk(kind="shout"): |
还有更多!
如果你能够返回一个函数,你也可以把函数当成参数进行传递。
1 | def doSomethingBefore(func): |
现在你已经知道所有理解装饰器的必要知识。正如你所看到的,装饰器就是一个包装器(wrapper)。这意味着,它允许你,在不修改待装饰的函数的前提下,在该函数(执行)的前后执行其他代码。
手动构造装饰器
下边展示了如何手动构造装饰器:
1 | # A decorator is a function that expects ANOTHER function as parameter |
现在,你可能想要每次执行函数a_stand_alone_function
的时候都执行的是a_stand_alone_function_decorated
。这很简单,只要用my_shiny_new_decorator
返回值覆盖a_stand_alone_function
就行了:
1 | a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function) |
装饰器解密
对前面的例子使用 Python 装饰器语法:
1 |
|
对,就这么简单。@decorator
只是下式的缩写:
1 | another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function) |
装饰器只是一个装饰器设计模式的一个 Pythonic 的变体。Python 集成了几种经典的设计模式以便于开发(如迭代器)。
当然,你也可以堆叠(accumulate )装饰器:
1 | def bread(func): |
用 Python 的装饰器语法:
1 |
|
装饰器的顺序很重要:
1 |
|
现在,回答提出的问题
现在,你很容易就能够回答提出的问题:
1 | # The decorator to make it bold |
现在,你可以高兴地(带着答案)离开,或者多烧下脑,看看装饰器的高级用法。
装饰器进阶
向待装饰函数传参
1 | # It’s not black magic, you just have to let the wrapper |
修饰(类)方法
Python 一个奇特的地方在于方法跟函数实际上是一样的,唯一的区别在于方法的第一个参数是当前对象的引用(self
)。这意味着你可以像装饰函数一样装饰方法。记得考虑self
变量。
1 | def method_friendly_decorator(method_to_decorate): |
如果你想要构造一个通用装饰器——能够应用到任意函数或方法,不管它们的参数是什么——那么就使用*args, **kwargs
:
1 | def a_decorator_passing_arbitrary_arguments(function_to_decorate): |
向装饰器传参
很好!如果我们向装饰器传参会怎么样?这样做看起来有点怪怪的,因为一个装饰器必须接受一个函数作为参数,因此,你不能够将被装饰的函数的参数直接传给装饰器。
在看结论之前,我们先写一点以备提醒:
1 | # Decorators are ORDINARY functions |
上述两种方法效果完全一样,my_decorator
被调用。所以当你写下@my_decorator
,你在告诉 Python 解释器去调用标签为 my_decorator
的函数。这很重要!你给定的标签能够直接指向装饰器,或者不是。
让我们更进一步:
1 | def decorator_maker(): |
这没什么好惊讶的。我们再重复原来的事情,不过去掉一些繁杂的中间变量:
1 | def decorated_function(): |
更简要一点:
1 |
|
嘿,看到了吗?我们用@
语法来调用一个函数。所以,回到带参数装饰器,如果我们能够直接(on the fly)利用函数来生成装饰器,我们就能够向该函数传递参数,对吧?
1 | def decorator_maker_with_arguments(decorator_arg1, decorator_arg2): |
这就是带参数的装饰器。这些参数也可以是变量:
1 | c1 = "Penny" |
就如你所看到的,使用这个方法,你能够像其他参数一样向装饰器传递参数。你甚至能够使用*args, **kwargs
参数,如果你愿意。不过需要记住的是,装饰器只被调用一次,且仅仅只在 Python 解释器导入代码的时候。你不能够后续动态地设定装饰器的参数。
当你执行 import x
时,函数就已经被装饰,所以你不能改变什么。
装饰装饰器
好了,作为奖励,我会给你们一个代码片段,创建一个能够接受任意参数的装饰器。毕竟,为了接受参数,我们用另外一个函数来创建装饰器。我们将装饰器包装起来。
让我们写一个用于装饰装饰器的装饰器:
1 | def decorator_with_args(decorator_to_enhance): |
我们能够这样使用它:
1 | # You create the function you will use as a decorator. And stick a decorator on it :-) |
最佳实践
- 装饰器从 Python 2.4 开始引入,保证你的代码可以运行在大于等于 2.4 版本的 Python
- 装饰器会降低函数调用效率,记住
- 你不能反装饰一个函数(有方法可以创建一个可移除的装饰器,但没人使用)。所以一旦一个函数被装饰了,那在所有代码中它都是被装饰的
- 用装饰器包装函数,会使得这些函数难以调试(在大于等于 2.5 版本的 Python,这种情况有所改善,见下文)
functools
模块在 Python 2.5 中被引入,它包含了函数functools.wraps()
。该函数会拷贝被装饰函数的名字、模块和注释(docstring)到它的包装器(wrapper)。
(有趣的地方: functools.wraps() 是一个装饰器)
1 | # For debugging, the stacktrace prints you the function __name__ |
怎么有效使用装饰器
现在有一个很重要的问题:我能用装饰器来做什么?
装饰器看起来很酷也很强大,但有一个实际的例子会更好。好吧,这有无数种例子。经典的用法是拓展一个来自外部库函数的功能(你不能修改它),或者用于调试(你不想修改函数,因为调试只是暂时的)。
你可以遵循 DRY(Don’t repeat yourself)规则,使用装饰器来拓展函数功能,如:
1 | def benchmark(func): |
当然,好消息是你几乎可以把装饰器应用在任意函数上而不用重写它们:
1 |
|
Python 自身也提供了几个装饰器:property
,staticmethod
等。
- Django 使用装饰器来管理缓存和查看权限
- Twisted 使用装饰器来伪造(fake)内联异步函数调用
可以发挥的空间太大了!