本文含 10720 字,16 图表截屏
建议阅读 54 分钟
本文作为 Python 系列的特别篇第 4 篇,主要介绍 Python 的装饰器 (decorator)。
现在什么都不用懂,什么都不用想,看一个例子 (看我怎么把它和装饰器扯上关系的)。
斯蒂文是个厨师,有一天开始研究汉堡 (burger) 的做法,第一次他只用鸡肉饼做汉堡。
def meat(food='--鸡肉饼--'):
print(food)
burger = meat
burger()
--鸡肉饼--
很明显汉堡都是肉,太荤了。加点蔬菜 (vegetable) 如何?
def vegetable(func):
def wrapper():
print(' #西红柿#')
func()
print(' ~沙拉菜~')
return wrapper
burger = vegetable(meat)
burger()
#西红柿#
--鸡肉饼--
~沙拉菜~
现在汉堡看起来不错,可是好像看缺少了什么?对,再加点面包就齐活了。
def bread(func):
def wrapper():
print('</------\>')
func()
print('<\------/>')
return wrapper
burger = bread(vegetable(meat))
burger()
</------\>
#西红柿#
--鸡肉饼--
~沙拉菜~
<\------/>
现在看上去真像个汉堡,面包夹着蔬菜,蔬菜夹着肉。
要点:面包和蔬菜「装饰」着鸡肉饼,bread() 和 vegatable() 这两个函数起着「装饰器」的作用,它们没有改变 meat() 函数,只在它的基础上添砖加瓦,最后把鸡肉饼装饰成汉堡。
下面是装饰器的正规语法,用 @func 语法 (注意@符),将@bread 和 @vegatable 放在要装饰的函数上面。
@bread
@vegetable
def meat(food='--鸡肉饼--'):
print(food)
再调用被装饰后的 meat() 函数并赋值给 burger,就做出汉堡了。
burger = meat
burger()
</------\>
#西红柿#
--鸡肉饼--
~沙拉菜~
<\------/>
装饰器是有序的,如下例所示,如果互换 bread() 和 vegatable() 这两函数的位置,那么这汉堡最外层是蔬菜,中间是面包,里面是鸡肉饼,不像汉堡了。
@vegetable
@bread
def meat(food='--鸡肉饼--'):
print(food)
burger = meat
burger()
#西红柿#
</------\>
--鸡肉饼--
<\------/>
~沙拉菜~
要点:一个函数可以被多个装饰器装饰,装饰器的顺序很重要。
对装饰器有点感觉了么?有就往下看把。
本帖目录如下:
目录
第一章 - 函数复习
1.1 把函数赋值给变量
1.2 在函数里定义函数
1.3 在函数里返回函数
1.4 把函数传递给函数
第二章 - 装饰器
2.1 闭包和装饰器
2.2 装饰器初体验
2.3 装饰器知识点
2.4 装饰器实际案例
总结
1
函数复习
在 Python 里函数是「一等公民」,我们可以
把函数赋值给变量
在函数里定义函数
在函数里返回函数
把函数传递给函数
函数在〖Python 入门篇 (下)〗一贴第 4 节已经详细讨论过,为了自然地带出装饰器,本节再复习一遍相关的函数知识。
1.1
把函数赋值给变量
在 Python 里,函数是对象,因此可以把它赋值给变量,如下代码所示。
def shout(word='yes'):
return word.upper() + '!'
定义一个 shout() 函数,该函数名的字面意思是大喊,做的事情是把 word 转成大写加上感叹号给「大喊」出来。形不形象?
测试一下结果,没问题,输出 "YES!"。
print(shout())
YES!
把函数 shout() 赋值给变量 scream,打印出 scream 信息发现它的类型是函数 (注意 function__main__)。
scream = shout
scream
<function __main__.shout(word='yes')>
这里有个非常重要的细节。当你不带括号用 scream,你只是输出函数对象 (return function object),而不是在调用函数 (call function)。
当带上括号用 scream(),你是真正的调用该函数,结果输出"YES!",没任何问题。
print(scream())
YES!
突发奇想来删除函数 shout(),用 try-except block 来确认它已经被删除了,因为输出是 shout is not defined。
del shout
try:
print(shout())
except NameError as e:
print(e)
name 'shout' is not defined
但是函数 shout() 已经复制给变量 scream 了,即便它已经被删除了,也不影响函数 scream() 的功能。
print(scream())
YES!
小结:函数是对象,可以赋值给变量 a,再用 a() 来调用函数。
1.2
在函数里定义函数
在 Python 里,我们还可以在函数里定义函数。如下代码所示。
def talk():
def whisper(word='yes'):
return word.lower() + '...'
print(whisper())
在函数 talk() 里面定义了一个函数 whisper(),该函数名的字面意思是轻声说,做的事情是把 word 转成小写加上省略号给「轻吟」出来。形不形象?
测试一下结果,没问题,输出 "yes..."。
talk()
yes...
一个重要的点,函数 whisper() 只存在于函数 talk() 里面,即只能调用 talk() 时起作用。用 try-except block 来确认,果然函数 whisper() 在函数 talk() 外面没有被定义。
try:
print(whisper())
except NameError as e:
print(e)
name 'whisper' is not defined
小结:在函数 a 里可以定义函数 b,但 b 在函数 a 外面不存在。
1.3
在函数里返回函数
前两节我们已经验证了一下两点:
可以把函数赋值给变量
可以在另一个函数定义函数
综合这两点,我们就可以在函数里返回函数了。如下代码所示。
在函数 getTalk() 里面定义两个函数,shout() 和 whisper(),它们根据参数 kind 的不同取值来返回「大喊」的 YES! 和「轻吟」的 yes...。
注意第 12 和 14 行,返回值是 shout 和 whisper (可把它们当成变量),不是函数 shout() 和 whisper()。就是说当返回时我们不希望调用函数,就只单纯的返回函数对象 (看第 10-11 行的注释)。
将函数 getTalk() 赋值给变量 talk,打印其信息看出它的类型是函数。
talk = getTalk()
talk
<function __main__.getTalk.<locals>.shout(word='yes')>
运行 talk(),默认参数是 'shout',那么应该大声喊出来 YES!。
talk()
YES!
我们还可以直接调用 getTalk(),设置参数为 'whisper',那么应该返回函数whisper(),但是以变量形式返回。因此你可以把 getTalk('whisper') 当成一个函数对象 (function object)。
getTalk('whisper')
<function __main__.getTalk.<locals>.whisper(word='yes')>
只有在函数对象后加小括号 () 时,才是真正调用它,这是应该轻声吟出来 yes...。
getTalk('whisper')()
'yes...'
小结:在函数 a 里可以定义函数 b,然后把 b 当成对象返回。
1.4
把函数传递给函数
最后,函数可以当成参数传递给另一个函数,举例代码如下。
def doSomething(func):
print('Before do something')
print(func())
print('After do something')
函数 func 当做参数传给另一个函数 doSomething(),它其实已经有点装饰器的味道了,它没有改变函数 func() 里的任何内容,就在运行 func() 的前后加了些代码。
将之前的具体函数 scream() 传递进去,结果没问题。
doSomething(scream)
Before do something
YES!
After do something
总结:函数 a 传到函数 b,函数 b 只在函数 a 运行前后有操作,但是不改变函数 a。函数 b 可以看成是装饰器的雏形。
读懂本节后,你已经打好了所有帮助理解装饰器的基础了。
2
装饰器
2.1
闭包到装饰器
关于闭包的详细介绍请参考〖Python 入门篇 (下)〗一贴第 4.3 节。看下面的例子。
def outer_func(msg):
def inner_func():
print(msg)
return inner_func
内部函数 inner_func() 可以使用外部函数 outer_func() 的参数 msg (注意 msg 不在自己定义范围内),最后返回内部函数的对象 inner_func。
传递不同的参数 'Hi' 和 'Bye' 定义成不同的函数:
hi_func() 输出 'Hi'
bye_func() 输出 'Bye'
hi_func = outer_func('Hi')
bye_func = outer_func('Bye')
hi_func()
Hi
bye_func()
Bye
接下来,我们
将 outer_func() 改成 decorator_func()
将 inner_func() 改成 wrapper_func()
上面的代码变成下面的样子。
def decorator_func(msg):
def wrapper_func():
print(msg)
return wrapper_func
这就是装饰器,它返回 wrapper_func 对象,随时等着被调用,一旦被调用就运行 print(msg) 而打印出 msg。
等等,严格来说,对于装饰器,参数是函数而不是变量 (1.4 节讲了函数可以当成参数传递给另一个函数)。
def decorator_func(func):
def wrapper_func():
return func()
return wrapper_func
2.2
装饰器初体验
下面看一个装饰器最简单的例子,我们具体定义函数参数 func,代码如下。
def display():
print('Run display function')
用上节末定义的装饰器来装饰 display():
decorated_display = decorator_func(display)
decorated_display()
Run display function
但装饰器的特性是给原函数做装饰,但不改变原函数里的内容,比如下面代码第 3 行,我们希望在运行原函数 func() 之前,输出原函数的名字 (用 __name__属性)。
def decorator_func(func):
def wrapper_func():
print('Executed before {}'.format(func.__name__))
return func()
return wrapper_func
验证一下,结果没问题。
decorated_display = decorator_func(display)
decorated_display()
Executed before display
Run display function
但是,每次这样调用装饰器太过繁琐。Python 里有一种等价语法。把 @decorator_func 写在被装饰的函数上面即可,代码如下。
@decorator_func
def display():
print('Run display function')
它等价于
display = decorator_func(display)
语法 @decorator_func 也称为语法糖。
知识点
语法糖 (syntactic sugar):指计算机语言中添加的某种语法,对语言的功能没有影响,但是让程序员更方便地使用。
这时我们只需单用 display()。
display()
Executed before display
Run display function
2.3
装饰器知识点
在本节我们了解几个装饰器的知识点:
多个装饰器来装饰一个函数
传递参数给装饰函数 (wrapper function)
functools.wraps 的用法
传递参数装饰器 (decorator)
多个装饰器
我们可以从多个方面装饰一个函数,而这需要多个装饰器来完成。
例子:定义 slogan() 函数打印出 'I love Python'。
def slogan():
return 'I love Python'
我们希望这句话
大写化 (用 uppercase_decorator 装饰器)
被分词 (用 split_decorator 装饰器)
两个装饰器代码如下。
def uppercase_decorator(func):
def wrapper():
return func().upper()
return wrapper
def split_decorator(func):
def wrapper():
return func().split()
return wrapper
按先「大写」再「分词」的顺序装饰 slogan() (注意两个装饰器的顺序),我们得到想要的结果。
@split_decorator
@uppercase_decorator
def slogan():
return 'I love Python'
slogan()
['I', 'LOVE', 'PYTHON']
其实这个上面这个装饰器的等价语句是
slogan = split_decorator(uppercase_decorator(slogan))
但是按先「分词」再「大写」的顺序装饰 slogan() (注意两个装饰器的顺序),结果报错了。
@uppercase_decorator
@split_decorator
def slogan():
return 'I love Python'
slogan()
明白这个上面这个装饰器的等价语句后,就不难理解报错信息了。
slogan = uppercase_decorator(split_decorator(slogan))
传递参数给装饰函数
装饰函数就是 wrapper(),由于它里面要调用原函数 func,一旦它有参数,那么肯定要把这些参数传递给 wrapper()。
首先看一个没有参数的 wrapper() 的例子。
def my_logger(func):
def wrapper():
name = func.__name__
print('Before calling {}'.format(name))
func()
print('After calling {}'.format(name))
return wrapper
那么装饰的原函数 func() 也一定那没有参数,结果没问题。
@my_logger
def func():
print('calling func')
func()
Before calling func
calling func
After calling func
如果装饰在一个参数的原函数上,就会报错。报错信息是 wrapper() 有 0 个位置参数,但实际传递进去了 2 个。
@my_logger
def func(a,b):
return a+b
func(1,2)
怎么办呢?在 wrapper() 里面也定义两个参数呗,在里面调用原函数 func(arg1,arg2) 就没问题了。注意下面代码第 2 和 5 行。
def my_logger(func):
def wrapper( arg1, arg2 ):
name = func.__name__
print('Before calling {}'.format(name))
func( arg1, arg2 )
print('After calling {}'.format(name))
return wrapper
你看,没有报错,但是怎么没有输入结果 3 呢?
@my_logger
def add(a,b):
return a+b
add(1,2)
Before calling add
After calling add
原因是在 wrapper() 里面没有返回值,这是只需把 func(arg1,arg2) 的结果复制给 results,再在 wrapper() 里面没有返回就行了。注意下面代码第 5 和 7 行。
def my_logger(func):
def wrapper( arg1, arg2 ):
name = func.__name__
print('Before calling {}'.format(name))
result = func( arg1, arg2 )
print('After calling {}'.format(name))
return result
return wrapper
结果没问题。
@my_logger
def add(a,b):
return a+b
add(1,2)
Before calling add
After calling add
3
如果函数需要 3 个参数呢?5 个参数呢?还记得 *args 可以表示任意个位置参数吗?注意下面代码第 2 和 5 行。
def my_logger(func):
def wrapper( *args ):
name = func.__name__
print('Before calling {}'.format(name))
result = func( *args )
print('After calling {}'.format(name))
return result
return wrapper
试试 3 个参数相加,结果没问题。
@my_logger
def add(a,b,c):
return a+b+c
add(1,2,3)
Before calling add
After calling add
6
试试 5 个参数相加,结果也没问题。
@my_logger
def add(a,b,c,d,f):
return a+b+c+d+f
add(1,2,3,4,5)
Before calling add
After calling add
15
除了 *args 可以表示任意个位置参数以外, *kwargs 可以表示任意个关键词参数,用 *args 和 *kwargs 可以使得装饰器装饰含有任意个参数的函数了。注意下面代码第 2 和 5 行。
def my_logger(func):
def wrapper( *args, **kwargs ):
name = func.__name__
print('Before calling {}'.format(name))
result = func( *args, **kwargs )
print('After calling {}'.format(name))
return result
return wrapper
functools.wrap
在装饰器里,装饰之后的函数的名称会弄乱。比如一个简单的函数 f(),它的名称就是 f (用 __name__)。
def f():
pass
print(f.__name__)
f
但在装饰器里,用 @decorator 来装饰 f(),我们再看它的名称已经变成了 wrapper。原因很简单,因为我们调用的是 decorator(f),而这个函数返回的确是 wrapper() 函数,因此名称是 wrapper。
def decorator(func):
def wrapper():
return func()
return wrapper
@decorator
def f():
pass
print(f.__name__)
wrapper
但这不是我们希望的,因为有些依赖函数签名的代码执行就会出错。我们还是希望
wrapper.__name__ = func.__name__
这时用 functools.wraps 即可。注意代码第 4 行。
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper():
return func()
return wrapper
@decorator
def f():
pass
print(f.__name__)
f
现在 f() 函数的名称又变成了 f。
传递参数给装饰器
我们除了可以传递参数给装饰函数 (wrapper),也可以传递参数给装饰函数 (decorator)。先看下面例子,只有装饰函数有参数,该装饰器将数值保留到小数点后两位 (很多金融产品的价值在显示时都是这个格式)。
def money_format(func):
def wrapper(*args, **kwargs):
r = func(*args, **kwargs)
formatted = '{:.2f}'.format(r)
return formatted
return wrapper
用 @money_format 装饰之后,我们相加两个现值 (PV),得到 200.45,小数点后面只有两位。
@money_format
def add( PV1, PV2 ):
return PV1 + PV2
add(100, 100.4545)
'200.45'
但是这个 200.45 是以什么货币为单位呢?USD?CNY? 这时我们可以在装饰器上传递参数来区分货币单位。代码如下,注意 @currency_unit 里面传递了参数 curr。
def currency_unit(curr):
def money_format(func):
def wrapper(*args, **kwargs):
r = func(*args, **kwargs)
formatted = '{:.2f}'.format(r) + ' ' + curr
return formatted
return wrapper
return money_format
测试:相加两个 PV,单位是 USD,结果没问题。
@currency_unit('USD')
def add( PV1, PV2 ):
return PV1 + PV2
add(100, 100.4545)
'200.45 USD'
测试:相加三个 PV,单位是 CNY,结果也没问题。
@currency_unit('CNY')
def add( PV1, PV2, PV3 ):
return PV1 + PV2 + PV3
add(100, 100, 100.4545)
'300.45 CNY'
2.4
装饰器实际案例
在实际工作中,装饰器经常会记录日志和时间,因此我们定义 my_logger() 和 my_timer() 函数。
由于举例的函数运行时间太短,我们可以的加了 1 秒延时,使得结果看起来好看些。该函数打印出用户的姓名和年龄信息。
来看看两个装饰器 @my_logger 和 @my_timer 装饰函数 display_info() 的效果。
display_info('Tom', 22)
display_info ran with arguments (Tom, 22)
display_info ran in: 1.0041701793670654 sec
点开生成的日志文件 display_info.log,发现它已经记录了 Tom 的个人信息。
再多加两个用户 Steven 和 Sherry。
display_info('Steven', 18)
display_info('Sherry', 15)
display_info ran with arguments (Steven, 18)
display_info ran in: 1.0037410259246826 sec
display_info ran with arguments (Sherry, 15)
display_info ran in: 1.0011024475097656 sec
这时日志文件里的记录已经更新了。
传递参数给装饰函数
为了能装饰任意个位置参数和任意个关键词参数,我们在 wrapper() 里用 *args 和 **kwargs 来传递任意个参数。
如果 display_info() 输入参数还有收入、住址、爱好等信息,那么这个装饰器依然适用。
结果如下,没问题。
display_info('John', 25)
display_info('Travis', 30)
Executed Before display_info
display_info ran with arguments (John, 25)
Executed After display_info
Executed Before display_info
display_info ran with arguments (Travis, 30)
Executed After display_info
传递参数给装饰器
如果我们的记录的信息有的是用来写日志 (LOG),有的是用来测试 (TEST),我们想加一个参数来区分 LOG 和 TEST,但是不想更改以写好 wrapper_func 函数,这是可以再加一层装饰器函数 prefix_decorator(),并传递个参数。
当记录日志时,我们在 @prefix_decorator 里传递参数 'LOG:',因此 wrapper_func 里打印的语句都多了个 'LOG:' 的前缀。
LOG: Executed Before display_info
display_info ran with arguments (John, 25)
LOG: Executed After display_info
LOG: Executed Before display_info
display_info ran with arguments (Travis, 30)
LOG: Executed After display_info
对于 TEST,同理。
TEST: Executed Before display_info
display_info ran with arguments (John, 25)
TEST: Executed After display_info
TEST: Executed Before display_info
display_info ran with arguments (Travis, 30)
TEST: Executed After display_info
3
总结
装饰器就是「接受函数为参数」并「返回函数为输出」的函数。
装饰器不会更改参数函数里的内容。
装饰器返回的其实是函数对象。
装饰器本质就是个函数。
就装饰器定义里面有这么多函数出现,要想理解装饰器,一定要理解好函数 (本贴第 1 节的内容)。
如果觉得所有知识点还是太难懂,那么就先理解下面一幅图吧,先把最简单的装饰器的情况弄明白。
Stay Tuned!
王的机器
机器学习、金融工程、量化投资的干货营;快乐硬核的终生学习者。