Python 装饰器( Decorator)
装饰器(Decorator) 是 Python 中最重要的特性之一。
简单来说装饰器就是一个可以修饰函数,类或方法的函数。
本文尝试着从零开始解释 Decorator 是怎么工作的。
函数(Function)是第一类对象
在 Python 中,一切皆是对象。这意味着即使一个函数被其他对象所包装(wrap),我们仍然可以通过对象名对函数进行调用。
def traveling_function():
print "Here I am!"
function_dict = {
"func": traveling_function
}
trav_func = function_dict['func']
trav_func()
# >> Here I am!
另外,还可以将一个函数作为参数传递给高阶函数。
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!
装饰器语法
装饰器是一个将函数作为参数的函数,通常它的作用是返回封装好的经过修饰的函数。下面的例子展示了装饰器的基本工作过程,这个装饰器所做的就是记录他所包装的方法被调用的次数。注意,在 logging_decorator 函数中,虽然参数 func 没有传递给内嵌包装函数 wrapper ,但是它依然可以被 wrapper 调用,这是因为闭包原理。
def logging_decorator(func):
# Everything here happens when the decorator LOADS and is passed
# the function as described in step 2 above
def wrapper():
# Things here happen each time the final wrapped function gets CALLED
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."
a_function = logging_decorator(a_function)
a_function()
# >> The function I modify has been called 1 time(s).
# >> I'm a normal function.
a_function()
# >> The function I modify has been called 2 time(s).
# >> I'm a normal function.
熟悉装饰器(Decorator)后, 还可以使用Python提供的一个特定语法 @decorator 使得它看上去更直观,更简单。
# In the previous example, we used our decorator function by passing the
# function we wanted to modify to it, and assigning the result to a variable
def some_function():
print "I'm happiest when decorated."
# Here we will make the assigned variable the same name as the wrapped function
some_function = logging_decorator(some_function)
# We can achieve the exact same thing with this syntax:
@logging_decorator
def some_function():
print "I'm happiest when decorated."
装饰器(Decorator )的简要工作原理:
- 当 Python 解释器遇到被装饰的方法时,先编译这个方法(some_function), 并给它赋一个名字 ‘some_function’;
- 这个方法(some_function)作为参数被传给装饰方法(decorator function) logging_decorator ;
- 装饰方法(decorator function) logging_decorator 返回的新方法替代原来的some_function方法, 与’some_function’这个名字绑定.
对带参数的函数进行装饰
一般来说,内嵌包装函数的形参和返回值必须与目标函数相同。
def deco(func):
def _deco(a, b):
print("before myfunc() called.")
ret = func(a, b)
print("after myfunc() called. result: %s" % ret)
return ret
return _deco
@deco
def myfunc(a, b):
print("myfunc(%s,%s) called." % (a, b))
return a + b
myfunc(1, 2)
myfunc(3, 4)
对参数数量不确定的函数进行装饰
# 参数用(*args, **kwargs),自动适应变参和关键字参数
def deco(func):
def _deco(*args, **kwargs):
print("before %s called." % func.__name__)
ret = func(*args, **kwargs)
print("after %s called. result: %s" % (func.__name__, ret))
return ret
return _deco
@deco
def myfunc(a, b):
print("myfunc(%s,%s) called." % (a, b))
return a+b
@deco
def myfunc2(a, b, c):
print("myfunc2(%s,%s,%s) called." % (a, b, c))
return a+b+c
myfunc(1, 2)
myfunc(3, 4)
myfunc2(1, 2, 3)
myfunc2(3, 4, 5)
带参数的装饰器
有时候你需要根据不同的情况改变装饰器的行为,这可以通过给装饰器传递参数来完成。示例如下:
# 在外层多了一层包装,deco 返回 ‘一个’ 装饰器 _dec
def deco(arg):
def _deco(func):
def __deco():
print("before %s called [%s]." % (func.__name__, arg))
func()
print("after %s called [%s]." % (func.__name__, arg))
return __deco
return _deco
# 等价于 myfunc = deco("mymodule")(myfunc)
@deco("mymodule")
def myfunc():
print("myfunc() called.")
@deco("module2")
def myfunc2():
print("myfunc2() called.")
myfunc()
myfunc2()
一步步来看上述代码是如何工作的:
- 解释器遇到被装饰的函数, 编译 myfuc, 并把它绑定给 ‘myfunc’ 这个名字.
- deco 函数被调用并传递参数“mymodule”, 返回 _deco 函数.
- _deco 函数被调用并传递 myfunc 函数作为参数, _deco 返回 __deco 函数.
- 最后, __deco 函数替代原始的函数 myfunc 并被绑定到 ‘myfunc’ 这个名字下.
链式装饰器
一个函数可以用多个装饰器进行修饰。当你将多个装饰器链接在一起时,他们在放置在栈中顺序是从下往上。在下面的例子中,被装饰的函数 some_fuction 在编译之后,传递给在它之上的第一个装饰器(loging_decorator),然后第一个装饰器的返回值传递给下一个装饰器,依此类推。
@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!
类装饰器
还可以用装饰器来修饰类。对于类装饰器,应该让装饰器函数始终返回类对象作为结果。虽然一般不会这么用它。但有些情况下用来替代元类也未尝不可。
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 属性,注意到我们的装饰器函数没有返回一个函数,而是一个类。
作为一个类的装饰器
装饰器不仅可以是一个方法,还可以是一个类。一个装饰器的唯一需求是它的返回值可以被调用,这味着返回的对象必须实现 _call_ 这个方法。函数设置了这个隐式方法。现在我们重新建立 identity_decorator 作为一个类,然后来看它是怎么运作的:
class IdentityDecorator(object):
def __init__(self, func):
self.func = func
def __call__(self):
self.func()
# 等价于 a_function = IdentityDecorator(a_function)。
# 当IdentityDecorator实例化时,它的初始化函 _init_ 被调用,参数是被装饰函数。
# 调用 a_function(真实的类型是 IdentityDecorator)时,实际调用的是这个对象的 _call_ 方法。
@IdentityDecorator
def a_function():
print "I'm a normal function."
a_function()
# >> I'm a normal function
使用装饰器需要注意的其他问题
装饰器(Decorator)可以同函数的其他方面进行交互,如递归、文档字符串和函数属性。
- 如果对递归函数使用装饰器,所有内部的递归调用都会通过装饰后的版本进行。如果使用装饰器的目的是进行一些系统管理,如同步或锁定,最好不要使用递归。
@locked
def factorial(n):
if n <= 1:
return 1
else:
return n * factorial(n -1) # 调用 factorial 函数的已包装版本
- 使用装饰器包装函数的一个副作用是会丢失原始函数的文档字符串和函数属性。因为此时是由包装函数而非原始函数来访问文档字符串和函数属性的。如下所示:
def wrap(func):
def call(*args, **kwargs):
return func(*args, **kwargs)
@wrap
def factorial(n):
"""Computes n factorial."""
if n <= 1:
return 1
else:
return n * factorial(n - 1)
help(factorial)
# >> Help on function call in module __main__:
# >>
# >> call(*args, **kwargs)
这个问题的解决办法是编写可以传递函数名称、文档字符串以及函数属性的装饰器函数。因为这是一个常见问题,所以functools
模块提供了函数wraps
(它也是一个装饰器)用于自动复制这些属性。
def wrap(func):
def call(*args, **kwargs):
return func(*args, **kwargs)
call.__name__ = func.__name__
call.__doc__ = func.__doc__
call.__dict__.update(func.__dict__)
return call
# 下面是等价的
from functools import wraps
def wrap(func):
@wraps(func)
def call(*args, **kwargs):
return func(*args, **kwargs);