首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >盘一盘 Python 系列特别篇 - 装饰器

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

作者头像
用户5753894
发布2019-10-23 19:45:14
7170
发布2019-10-23 19:45:14
举报
文章被收录于专栏:王的机器王的机器

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

斯蒂文是个厨师,有一天开始研究汉堡 (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 节的内容)。

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 王的机器 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档