Original blog post wrote by Brian Holdefehr. This is a Chinese version of original post .
本文原文作者 Brian Holdefehr。若有不足,敬请斧正。
装饰器(Decorators) 是Python众多强大特性之一。装饰器除了本身编程语法上的作用之外,另外提供了一种有趣思维方式——"函数化"思维(a functional way)
我会从头开始介绍介绍装饰器的工作原理,接着会介绍几个必须了解的基本概念。之后,我们会详细深入探索一些装饰器实例以及其工作方式。最后我们会讨论一些装饰器的高阶应用,例如参数化装饰器(optional arguments),以及嵌套装饰器(chaining)。
首先,我们给出最简单的Python函数装饰器的定义。
函数 是一个执行特定任务并且可以复用的代码块。
那么,什么是装饰器呢?
装饰器 是一个修改其他函数的函数
现在,我们从几个基本概念的解释开始,逐渐详细深入装饰器的含义。
函数是对象
在Python世界里,所有一切都是对象。这就意味着说,函数本身可以通过名字对其引用(referred to by name),并且函数和其他对象一样可以进行传递。例如:
def traveling_function():
print "Here I am!"
function_dict = {
"func": traveling_function
}
trav_func = function_dict['func'] #存储字字典中的函数可以和普通函数一样调用。
trav_func()
# >> Here I am!
在function_dict 字典(dictionary)中,键(key)"func" 存储着 traveling_function 函数对象。在字典中的这个函数对象可以和普通函数一样进行调用。
高阶函数
函数对象可以像其他对象一样在执行过程中进行传递。我们可以把函数对象存储在字典里,或者把它们放到列表里面,甚至可以把它们赋值给对象属性中(object properies) 。那么,我们能不能让函数像参数一样传递到另外一个函数中?当然可以!如果某函数A接受另外一个函数B作为参数 或者该函数A返回另外一个函数对象C,那么这个函数称之为”高阶函数“。
def self_absorbed_function():
return "I'm an amazing function!"
def printer(func):
print "The function passed to me says: " + func()
# Call `printer` and give it `self_absorbed_function` as an argument
printer(self_absorbed_function)
# >> The function passed to me says: I'm an amazing function!
这里你可以看到,函数对象self_absorbed_function可以作为参数传入另外函数printer中。传入之后,函数对象printer可以调用传入的函数对象self_absorbed_function。在了解这种特性之后,我们就可以建立有趣的函数,比如 ---- 装饰器!
装饰器初探
本质上,装饰器只是一个接受函数对象A作为参数的函数。在大多数情况下,装饰器会返回修改过的函数对象A’ 。下面简单的装饰器例子可以帮助我们理解其工作方式。
def identity_decorator(func):
def wrapper():
func()
return wrapper
def a_function():
print "I'm a normal function."
#`decorated_function`存储了`identity_decorator`返回的函数对象, 也是其返回的wrapper返回的对象
decorated_function = identity_decorator(a_function)
# 下面语句调用 `identity_decorator` 所返回的函数对象(因为这里使用了括号对)
decorated_function()
# >> I'm a normal function
这里,identity_decorator 并没有修改其包装(wraps)的函数对象func 。在调用identity_decorator的时候,它仅仅返回函数wrapper执行结果,该wrapper会调用函数identity_decorator的参数func。目前,这装饰器没有实际用途。
identity_decorator这个函数很特别,即便func对象没有作为参数传入到wrapper函数,wrapper函数仍然可以读取 func对象!可是为什么会这样呢?这就是因为"闭包"(closures)。
闭包
闭包是个看似高级的词语,实际意思是当函数声明之后,该函数仍然保留一个指向其诞生环境的引用(reference)。
在前例定义wrapper函数时候,wrapper在其本地作用域(local scope)中读取了func对象。这就意味着在wrapper函数生命周期中(从返回wrapper()函数对象到 函数对象identity_decorator返回到decorated_function为止 ),该wrapper一直可以读取func对象。一旦identity_decorator 函数进行返回之后,唯一读取func对象的方法就是通过 函数对象decorated_function。func 仅仅存在于 decorated_function的闭包环境内。
实例:简单装饰器
现在,我们创建一个装饰器。这个装饰器的功能是记录其所修饰函数对象的调用次数。
def logging_decorator(func):
def wrapper():
wrapper.count += 1
print "The function I modify has been called {0} times(s).".format(
wrapper.count)
func()
wrapper.count = 0
return wrapper
def a_function():
print "I'm a normal function."
modified_function = logging_decorator(a_function)
modified_function()
# >> The function I modify has been called 1 time(s).
# >> I'm a normal function.
modified_function()
# >> The function I modify has been called 2 time(s).
# >> I'm a normal function.
之前我们说过,“装饰器会修改函数对象”,这样想往往可以帮助我们理解。但是你可以从我们的例子中看出来:logging_decorator装饰器真正做到事情是“返回”的是一个全新,拥有记录日志功能的类似a_function函数对象。
在这里例子中,logging_decorator 不仅仅接受了一个函数对象作为参数,它同时返回了函数对象"wrapper"。每次logging_decorator返回函数被调用的时候,它会自动对wrapper.count 进行加一并输出该值,然后再调用logging_decorator所包装的函数func(在这里是a_function())。
你可能奇怪为什么我们把wrapper的一个属性(property)作为计数器,而不是用单独的变量作为计数器。wrapper的闭包空间不是给我们读取任何在本地空间(local scope)变量的权限吗?是的,但是这里需要特别注意:在Python,闭包提供了在函数作用范围内读取任何变量的权限,但是只提供向(mutable)对象写的权限。一个整型变量是(immutable)的Python不可变对象,所以我们无法在整型变量上进行累加。因为wrapper对象是可变(mutable)对象,所以我们选择在wrapper对象的属性上进行累加。
装饰器语法
我们在之前的例子中看到,通过函数对象作为参数的传入进行装饰器的使用。实际情况是用装饰器函数包装(wrapping)该传入的函数。然而,当你熟悉装饰器之后,Python 另外有一个更加直观,更为精简的语法(syntax pattern)。
# 在之前例子中,我们向装饰器logging_decorator传入需要修改的函数对象some_function,然后把修改后的函数对象赋予名为some_function的变量。
def some_function():
print "I'm happiest when decorated."
# 这里,我们把装饰器所返回的函数对象名字赋予原传入函数对象的名字。
some_function = logging_decorator(some_function)
# 可见,传入函数对象名字“some_function”字样重复出现2次略显冗余,优雅的Python提供更为简便表述方式--装饰器语法(decorator syntax)。
@logging_decorator
def some_function():
print "I'm happiest when decorated."
使用装饰器语法 ,语句执行逻辑如下:
- Python解释器读到装饰器函数的时候,首先编译some_function函数 ,并且标记其名字为“some_function”。
- 随后把some_function函数对象传入到 @字符后面名为“logging_decorator” 的装饰器函数。
- “logging_decorator”装饰器返回修改后的函数对象替代了原来some_function函数对象,并且和名称“some_function”名称进行绑定。
当你记住这3个步骤之后,我们对identity_decorator 进行详细的解释。
def identity_decorator(func):
# 当装饰器初始化,并且被func对象传入的时候,所有写在这里的语句会被执行(上述第二步)
def wrapper():
# 每次最后包装的函数返回对象被调用时,这里语句在会被执行
func()
return wrapper
希望这些注释足够明了。在包装函数wrapper所返回对象调用时候,其wrapper函数体内的命令都会被执行。在wrapper函数外,装饰器identity_decorator 内的语句只会在装饰器被第一次传入参数的时候会被调用(即第二步)
在继续了解装饰器之前,我还需要解释*args 和**kwargs 用法。
*args 和 **kwargs
你以前可能经常看见这两个修饰符同时出现,现在我们将对其一一进行讲解。
- Python函数可以通过*args传入任意个数的位置参数(positional arguments), *args 会把所有匿名参数压缩到一个元组(tuple)中。函数可以访问该元组的所有成员。相反地,当调用函数使用*args时候, *args 变量将会自己解压成匿名参数列表。
def function_with_many_arguments(*args):
print args
# args在函数体内是一个由传入多个参数所组成的元组,该元组可以和其他元组一样被函数体读取
function_with_many_arguments('hello', 123, True)
# >> ('hello', 123, True)
def function_with_3_parameters(num, boolean, string):
print "num is " + str(num)
print "boolean is " + str(boolean)
print "string is " + string
arg_list = [1, False, 'decorators']
# 通过在arg_list加上* 之后,arg_list将会自动解压缩3个位置参数
function_with_3_parameters(*arg_list)
# >> num is 1
# >> boolean is False
# >> string is decorators
- 获取列表内容, 在定义函数后面括号里的参数*args 会压缩一系列位置参数成为名字为‘args'的元组变量。在函数体内元组*args 会包含一系列位置参数供函数调用。
- 你可以看到参数展开的例子, * 符号不仅仅可以修饰‘args’,其他名字也同样适用。我们这里只是作为约定俗成地命名‘args'所有位置参数用。
- **kwargs 和*args 的作用是类似的,但是**kwargs 不是压缩/解压 位置参数,而是 压缩/解压 命名参数。 如果**kwargs 出现在函数的参数列表里面,它会把所有可能的命名参数都压缩到一个字典里面。
def function_with_many_keyword_args(**kwargs):
print kwargs
function_with_many_keyword_args(a='apples', b='bananas', c='cantalopes')
# >> {'a': 'apples', 'b': 'bananas', 'c': 'cantalopes'}
def multiply_name(count=0, name=''):
print name * count
arg_dict = {'count': 3, 'name': 'Brian'}
multiply_name(**arg_dict)
# >> BrianBrianBrian
你现在明白*args 和 **kwargs 的魔法作用。现在我们看看装饰器的有用之处。
缓存(Memoization)
缓存是避免冗余的计算开销。你可以将每次函数返回的结果暂时存储起来,通过缓存这个方法,如果下次对改函数传入相同的参数,该函数就不必重新花时间再去计算一遍,而是直接返回在缓存中已经存在的结果。
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def an_expensive_function(arg1, arg2, arg3):
...
你可能已经注意到这里@wraps 奇怪的用法。在讨论缓存之前我会扼要地简述这个看似奇怪的wraps用法。
- 装饰器的一个副作用就是被包装的函数失去了其原本的3个属性,__name__, __doc__以及__module__。wraps函数 包装了装饰器所需要返回的函数,保留了这三个属性,如同包装函数没有修改过他们一样。例如:如果不是用wraps函数进行包装的话,an_expensive_function名字就可能变成‘wrapper',
我认为 缓存 诠释了装饰器好用处之一。这能满足众多函数希望提升性能的需求,并且 如果创建一个灵活(generic)的装饰器,我们可以用它来 装饰 其他不同函数,这样其他函数都可以从中获得速度的提升。这就避免了在不同地方为不同函数分别造轮子。通过这样的抽象,我们的代码会更加易于维护,同事更加易于阅读和理解。之后,如果你看到一个@开始的单词之后,马上明白这个函数已经被缓存了。
我应该注意到 缓存仅仅适用于纯函数(pure functions)。所谓纯函数就是 “当参数是一定时候,该函数永远返回相对应的值”。是不是纯函数,这个取决于全局变量是否参与了函数体内的计算,或者取决于系统的输入输出,或者其他东西影响到该函数的返回值。缓存在这些外接变量影响下会返回和函数真实情况不一致的值。同样,一个纯函数不会有任何副作用。所以如果你的函数对计数器进行累加,或者调用方法,或者调用其他对象,只要这些不会影响到这个函数的返回值,那么缓存返回的值副作用不会发生。
类装饰器
原本我们说装饰器是一个修改函数的函数。但是,他们同时可以用来修改类或者方法。装饰器很少用来装饰类,但是它可以在一些特定的场景下替代 metaclasses(译者:metaclasses在99%的场景下是没有什么用处的)。
foo = ['important', 'foo', 'stuff']
def add_foo(klass):
klass.foo = foo
return klass
@add_foo
class Person(object):
pass
brian = Person()
print brian.foo
# >> ['important', 'foo', 'stuff']
现在,所有 Person类下面所有对象都会有一个“Foo”属性。注意,由于我们装饰了一个类,该装饰器并不是返回了一个函数对象,而是返回了一个类对象。于是,在这里我们拓展了装饰器的定义:
装饰器是 修改函数,方法(methods)或者类(classes)的函数。
装饰器是可调用的类
事实证明,我之前对你有所隐瞒。装饰器不仅仅可以装饰类,装饰器本身也可以成为一个类!唯一需要满足要求:装饰器的返回值必须是可以调用的(callable)。这意味着装饰器的返回值必须定义 默认的"__call__()"方法。"__call__()"方法 即为你调用对象时,该被调用对象所执行的内容。当然,函数对象都会自动定义了这个__call__() 方法。现在,我们重新建立identity_decorator类, 看看它是如何工作的。
class IdentityDecorator(object):
def __init__(self, func):
self.func = func
def __call__(self):
self.func()
@IdentityDecorator
def a_function():
print "I'm a normal function."
a_function()
# >> I'm a normal function
以下是执行过程:
- 当IdentityDecorator 装饰 a_function时候,它像装饰器函数一样。这段代码等同于: a_function= IdentityDecorator(a_function)。这个装饰器类通过传入函数对象进行初始化。
- 当IdentityDecorator 实例化后, IdentityDeorator的初始化函数函数__init__()接受需要装饰的函数对象。在这个例子中,这个初始化函数是接受函数对象,并且把它添加到IdentityDecorator对象属性中,这样之后该函数对象就能够被其他函数访问。
- 最后,当a_function (由IdentityDecorator对象返回包装过的a_function)被调用的时候,该对象的__call__()方法将会运行。
现在,我们再次拓展装饰器的定义:
装饰器是一个可调用的对象,它能修改函数对象,方法(methods),和类。
参数化的装饰器
有时候你需要针对不同情况,针对性地需要改变修饰器行为。这种修改可以通过传递参数来实现这种变动。
from functools import wraps
def argumentative_decorator(gift):
def func_wrapper(func):
@wraps(func)
def returned_wrapper(*args, **kwargs):
print "I don't like this " + gift + " you gave me!"
return func(gift, *args, **kwargs)
return returned_wrapper
return func_wrapper
@argumentative_decorator("sweater")
def grateful_function(gift):
print "I love the " + gift + "! Thank you!"
grateful_function()
# >> I don't like this sweater you gave me!
# >> I love the sweater! Thank you!
我们看看如果不用装饰器语法情况下,不带参数和带参数的修饰器函数各自写法:
# 无参数形式的装饰器
grateful_function = argumentative_function(grateful_function)
# 带有参数形式的装饰器
grateful_function = argumentative_decorator("sweater")(grateful_function)
这里需要关注的是: 当给定一个参数后,装饰器首先会随同参数一起调用(就是 argumentative_decorator 和"sweater"),包装函数(grateful_function)不同于以往那样会直接作为参数传入argumentative_decorator,而是直接和包装函数中的参数func 进行绑定。
步步分解:
- 解释器执行到被装饰的函数时候,对grateful_function进行编译(如图,将执行“print gift 语句”), 并且将被装饰的函数和名称“grateful_function”进行绑定。
- 随后"argumentative_decorator"会被调用,然后传递参数“sweater”到gift,最后它返回‘func_wrapper’函数对象。
- 'func_wrapper' 会把grateful_function函数作为参数传入,执行语句,func_wrapper将会返回 returned_wrapper函数对象。
- 最终,returned_wrapper 将会替代原来的函数对象grateful_function,然后其对象将会和“grateful_function"名称进行绑定。
我认为这些代码比无参数的装饰器要略难于理解, 但是如果你花时间去思考联系一下,应该能够像明白是怎么一回事。
可选参数的装饰器
装饰器接受可选参数的方法有很多。至于用什么,取决于你是否想要使用位置参数(positional arguments),命名参数(keyword arguments),或者是同时适用两者。这里展示一个可选命名参数的装饰器示例。
from functools import wraps
GLOBAL_NAME = "Brian"
def print_name(function=None, name=GLOBAL_NAME):
def actual_decorator(function):
@wraps(function)
def returned_func(*args, **kwargs):
print "My name is " + name
return function(*args, **kwargs)
return returned_func
if not function: # User passed in a name argument
def waiting_for_func(function):
return actual_decorator(function)
return waiting_for_func
else:
return actual_decorator(function)
@print_name
def a_function():
print "I like that name!"
@print_name(name='Matt')
def another_function():
print "Hey, that's new!"
a_function()
# >> My name is Brian
# >> I like that name!
another_function()
# >> My name is Matt
# >> Hey, that's new!
如果我们传入命名参数"name"到”print_name“装饰器,那么该函数的行为和之前argumentative_decorator类似。首先,带有参数name的"print_name"装饰器会调用。然后该装饰器"print_name"返回的函数对象会传入到"print_name"函数所包装的函数对象"a_function"
如果我们不提供参数name的话,print_name 就会和无参数装饰器一样。它仅仅会把其包装的函数作为唯一的参数。
装饰器print_name 考虑了这两种情况。它会检查是否接受了一个包装函数参数。如果没有包装函数参数,那么它会返回waiting_for_func函数对象,该对象会将其包装函数作为参数调用。如果有包装函数参数,那么它跳过中间步骤,直接立即调用actual_decorator 函数。
装饰器的嵌套
我们探索最后一个修饰器的特性:嵌套(Chaining)。你可以在特定函数上放置一个以上的装饰器。这语法类似于类的多重继承一样,可以建立一个“多重继承”的函数对象。
@print_name('Sam')
@logging_decorator
def some_function():
print "I'm the wrapped function!"
some_function()
# 输出如下
# >> My name is Sam
# >> The function I modify has been called 1 time(s).
# >> I'm the wrapped function!
当你嵌套装饰器的时候,他们嵌套的顺数是由下而上。被包装的函数“some_function”最先会被传递到其定义处上面最近的装饰器(这里也就是logging_decorator)。然后经过第一个装饰器作用之后,返回一个函数对象又再一次被最近的装饰器(这里也就是print_name)装饰。如此反复知道所有嵌套的装饰器执行完毕。
由于我们使用的这两个装饰器首先打印了一个值,然后运行被传入的函数对象。这意味着,当被包装的函数被调用的时候,嵌套过程中最后一个装饰器“print_name”将会最早打印出第一行输出。
小结
我认为装饰器最大的好处之一就是他们使你可以用更为抽象地思维方式进行思考。如果你在开始检查一个函数,发现这个函数有一个memoize装饰器的时候,你会立即明白:你在监视一个memoized的函数。如果函数体内包含了memoization code,那么需要你的大脑额外去理解一下,并且总结出可能的缺陷。使用装饰器同样可以让代码复用更加简便同时也节约了时间,让debugging和重构更加简单。
捣鼓装饰器 是学习函数式编程概念(比如高阶函数和闭包)非常好的途径。我希望此文能对大家有所启示。

0 Comments.