python3 第二十二章 - 函数式编程之Decorator(装饰器)
前面我们说了,在python中,一切皆对象。函数也是一个对象,而且函数对象可以被赋值给变量,通过变量也能调用该函数。如:
def sayHello(name): print(name + ' hello') fn = sayHello fn('roy')
以上代码,输出:
roy hello
函数对象有一个__name__属性,可以拿到函数的名字:
def sayHello(name): print(name + ' hello') f =sayHello print(f.__name__) print(sayHello.__name__)
以上代码,输出:
sayHello
sayHello
你会发现,上例中的变量 f 也获得了sayHello函数的功能,而且本质上它就是 sayHello 函数
如果,我想sayHello()这个函数在调用前先打印一句话,你可能会立刻想到,在函数的实现里再加一个print。这当然能达到效果,但是这也修改了函数的定义。
事实上,很多时候我们希望在不修改函数定义的情况下增强函数的功能,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
装饰器其实就是一个以函数作为参数并返回一个替换函数的可执行函数。让我们先从简单的开始:
def log(fn): def inner(): print('调用 fn 之前') fn() return inner def sayHello(): print('say hello') decorated = log(sayHello) decorated()
以上代码,输出:
调用 fn 之前
say hello
我们可以说变量 decorated 是 sayHello 的装饰版——即 sayHello加上一些东西。事实上,如果写了一个实用的装饰器,可能会想用装饰版来代替 sayHello,这样就总能得到“附带其他东西”的 sayHello版本。用不着学习任何新的语法,通过将包含函数的变量重新赋值就能轻松做到这一点:
def log(fn): def inner(): print('调用 fn 之前') fn() return inner def sayHello(): print('say hello') sayHello = log(sayHello) sayHello()
以上代码,输出:
调用 fn 之前
say hello
现在任意调用 sayHello() 都不会得到原来的 sayHello,而是新的装饰器版!明白了吗?
现在这个sayHello函数是不带参数的,那假如带有参数的,又该怎么写呢?看实例:
def log(fn): def inner(name): print('调用 fn 之前') fn(name) return inner def sayHello(name): print(name, 'say hello') sayHello = log(sayHello) sayHello('roy')
以上代码,输出:
调用 fn 之前
roy say hello
在上面代码示例中,用了一个包装的函数来替换包含函数的变量来实现了装饰函数:
sayHello = log(sayHello) sayHello('roy')
在Python3中通过在函数定义前添加一个装饰器名和 @ 符号,来实现对函数的包装,如:
def log(fn): def inner(name): print('调用 fn 之前') fn(name) return inner @log def sayHello(name): print(name, 'say hello') sayHello('roy')
以上代码,输出:
调用 fn 之前
roy say hello
值得注意的是,这种方式和简单的使用 log 函数的返回值来替换原始变量的做法没有什么不同—— Python 只是添加了一些语法糖来使之看起来更加明确。
上面我们写了一个装饰器,但它是硬编码的,只适用于特定类型的函数——带有1个参数的函数。内部函数 inner 接收1个参数,然后继续将参数传给闭包中的函数。如果我们想要一个能适用任何函数的装饰器呢?这意味着这个装饰器必须接收它所装饰的任何函数的调用信息,并且在调用这些函数时将传递给该装饰器的任何参数都传递给它们,所幸python提供这样的语法,我们来写一个通用的装饰器:
def log(fn): """ 通用的装饰器 :param fn: :return: """ def inner(*args, **kwargs): print('调用 %s 之前' % fn.__name__) fn(*args, **kwargs) return inner @log def sayHello(name): print(name, 'say hello') @log def sayGoodbye(): print('say goodbye') @log def sayMessage(name, message): print(name, 'say', message) sayHello('roy') print() sayGoodbye() print() sayMessage('roy', 'Hello World')
以上代码,输出:
调用 sayHello 之前
roy say hello
调用 sayGoodbye 之前
say goodbye
调用 sayMessage 之前
roy say Hello World
注:当定义一个函数时,*args 可以表示在调用函数时从迭代器中取出位置参数, 也可以表示在定义函数时接收额外的位置参数。使用 **kwargs 来表示所有未捕获的关键字参数将会被存储在字典 kwargs 中。此前 args 和 kwargs 都不是 Python 中语法的一部分,但在函数定义时使用这两个变量名是一种惯例。和 * 的使用一样,可以在函数调用和定义时使用 **
但,还有个问题,由于log函数是一个Decorator(装饰器),返回一个函数,所以,原来的 sayGoodbye 函数仍然存在,只是现在同名的sayGoodbye变量指向了新的函数,于是调用sayGoodbye()将执行新函数,即在log函数中返回的inner函数。但你去看经过log函数装饰之后的函数,它们的__name__已经从原来的'sayGoodbye'变成了'inner'了:
def log(fn): """ 通用的装饰器 :param fn: :return: """ def inner(*args, **kwargs): print('调用 %s 之前' % fn.__name__) fn(*args, **kwargs) return inner @log def sayGoodbye(): print('say goodbye') print(sayGoodbye.__name__)
以上代码,输出:
inner
因为log函数中返回的那个函数名字就是'inner',所以,需要把原始函数的__name__等属性复制到inner函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写inner.__name__ = fn.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:
import functools
def log(fn): """ 通用的装饰器 :param fn: :return: """ @functools.wraps(fn) def inner(*args, **kwargs): print('调用 %s 之前' % fn.__name__) fn(*args, **kwargs) return inner @log def sayGoodbye(): print('say goodbye') print(sayGoodbye.__name__)
以上代码,输出:
sayGoodbye
如果Decorator(装饰器)本身需要传入参数,那要怎么做呢?那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
import functools def log(msg): def decorator(fn): """ 通用的装饰器 :param fn: :return: """ @functools.wraps(fn) def inner(*args, **kwargs): print('调用 %s 之前' % fn.__name__) print('msg:',msg) fn(*args, **kwargs) return inner return decorator @log('这是一个自定义消息') def sayGoodbye(): print('say goodbye') sayGoodbye()
以上代码,输出:
调用 sayGoodbye 之前
msg: 这是一个自定义消息
say goodbye