盘一盘 Python 系列特别篇 - 装饰器

现在什么都不用懂,什么都不用想,看一个例子 (看我怎么把它和装饰器扯上关系的)。

斯蒂文是个厨师,有一天开始研究汉堡 (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 里函数是「一等公民」,我们可以

  1. 把函数赋值给变量
  2. 在函数里定义函数
  3. 在函数里返回函数
  4. 把函数传递给函数

函数在〖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

在函数里返回函数

前两节我们已经验证了一下两点:

  1. 可以把函数赋值给变量
  2. 可以在另一个函数定义函数

综合这两点,我们就可以在函数里返回函数了。如下代码所示。

在函数 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

装饰器知识点

在本节我们了解几个装饰器的知识点:

  1. 多个装饰器来装饰一个函数
  2. 传递参数给装饰函数 (wrapper function)
  3. functools.wraps 的用法
  4. 传递参数装饰器 (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 节的内容)。

如果觉得所有知识点还是太难懂,那么就先理解下面一幅图吧,先把最简单的装饰器的情况弄明白。

举报

扫码关注云+社区

领取腾讯云代金券