Python 中级知识之装饰器,滚雪球学 Python

橡皮擦,一个逗趣的互联网高级网虫,新的系列,让我们一起 Be More Pythonic

已完成的文章清单

  1. 滚雪球学 Python 第二轮开启,进阶之路,列表与元组那些事儿
  2. 说完列表说字典,说完字典说集合,滚雪球学 Python
  3. 关于 Python 中的字符串,我在补充两点,滚雪球学 Python
  4. 列表推导式与字典推导式,滚雪球学 Python
  5. 滚雪球学 Python 之 lambda 表达式
  6. 滚雪球学 Python 之内置函数:filter、map、reduce、zip、enumerate

七、函数装饰器

装饰器(Decorators)在 Python 中,主要作用是修改函数的功能,而且修改前提是不变动原函数代码,装饰器会返回一个函数对象,所以有的地方会把装饰器叫做 “函数的函数”。
还存在一种设计模式叫做 “装饰器模式”,这个后续的课程会有所涉及。

装饰器调用的时候,使用 @,它是 Python 提供的一种编程语法糖,使用了之后会让你的代码看起来更加 Pythonic

7.1 装饰器基本使用

在学习装饰器的时候,最常见的一个案例,就是统计某个函数的运行时间,接下来就为你分享一下。
计算函数运行时间:

import time

def fun():
    i = 0
    while i < 1000:
        i += 1
def fun1():
    i = 0
    while i < 10000:
        i += 1
s_time = time.perf_counter()
fun()
e_time = time.perf_counter()
print(f"函数{fun.__name__}运行时间是:{e_time-s_time}")

如果你希望给每个函授都加上调用时间,那工作量是巨大的,你需要重复的修改函数内部代码,或者修改函数调用位置的代码。在这种需求下,装饰器语法出现了。

先看一下第一种修改方法,这种方法没有增加装饰器,但是编写了一个通用的函数,利用 Python 中函数可以作为参数这一特性,完成了代码的可复用性。

import time
def fun():
    i = 0
    while i < 1000:
        i += 1

def fun1():
    i = 0
    while i < 10000:
        i += 1

def go(fun):
    s_time = time.perf_counter()
    fun()
    e_time = time.perf_counter()
    print(f"函数{fun.__name__}运行时间是:{e_time-s_time}")

if __name__ == "__main__":
    go(fun1)

接下来这种技巧扩展到 Python 中的装饰器语法,具体修改如下:

import time

def go(func):
    # 这里的 wrapper 函数名可以为任意名称
    def wrapper():
        s_time = time.perf_counter()
        func()
        e_time = time.perf_counter()
        print(f"函数{func.__name__}运行时间是:{e_time-s_time}")
    return wrapper

@go
def func():
    i = 0
    while i < 1000:
        i += 1
@go
def func1():
    i = 0
    while i < 10000:
        i += 1

if __name__ == '__main__':
    func()

在上述代码中,注意看 go 函数部分,它的参数 func 是一个函数,返回值是一个内部函数,执行代码之后相当于给原函数注入了计算时间的代码。在代码调用部分,你没有做任何修改,函数 func 就具备了更多的功能(计算运行时间的功能)。

装饰器函数成功拓展了原函数的功能,又不需要修改原函数代码,这个案例学会之后,你就已经初步了解了装饰器。

7.2 对带参数的函数进行装饰

直接看代码,了解如何对带参数的函数进行装饰:

import time

def go(func):
    def wrapper(x, y):
        s_time = time.perf_counter()
        func(x, y)
        e_time = time.perf_counter()
        print(f"函数{func.__name__}运行时间是:{e_time-s_time}")
    return wrapper

@go
def func(x, y):
    i = 0
    while i < 1000:
        i += 1
    print(f"x={x},y={y}")

if __name__ == '__main__':
    func(33, 55)

如果你看着晕乎了,我给你标记一下参数的重点传递过程。

20210307130853732[1].png

还有一种情况是装饰器本身带有参数,例如下述代码:

def log(text):
    def decorator(func):
        def wrapper(x):
            print('%s %s():' % (text, func.__name__))
            func(x)
        return wrapper
    return decorator

@log('执行')
def my_fun(x):
    print(f"我是 my_fun 函数,我的参数 {x}")

my_fun(123)

上述代码在编写装饰器函数的时候,在装饰器函数外层又嵌套了一层函数,最终代码的运行顺序如下所示:

my_fun = log('执行')(my_fun)

此时如果我们总结一下,就能得到结论了:使用带有参数的装饰器,是在装饰器外面又包裹了一个函数,使用该函数接收参数,并且返回一个装饰器函数。
还有一点要注意的是装饰器只能接收一个参数,而且必须是函数类型。

20210307141505987[1].png

7.3 多个装饰器

先临摹一下下述代码,再进行学习与研究。

import time

def go(func):
    def wrapper(x, y):
        s_time = time.perf_counter()
        func(x, y)
        e_time = time.perf_counter()
        print(f"函数{func.__name__}运行时间是:{e_time-s_time}")
    return wrapper

def gogo(func):
    def wrapper(x, y):
        print("我是第二个装饰器")
    return wrapper

@go
@gogo
def func(x, y):
    i = 0
    while i < 1000:
        i += 1
    print(f"x={x},y={y}")

if __name__ == '__main__':
    func(33, 55)

代码运行之后,输出结果为:

我是第二个装饰器
函数wrapper运行时间是:0.0034401339999999975

虽说多个装饰器使用起来非常简单,但是问题也出现了,print(f"x={x},y={y}") 这段代码运行结果丢失了,这里就涉及多个装饰器执行顺序问题了。

先解释一下装饰器的装饰顺序。

import time
def d1(func):
    def wrapper1():
        print("装饰器1开始装饰")
        func()
        print("装饰器1结束装饰")
    return wrapper1

def d2(func):
    def wrapper2():
        print("装饰器2开始装饰")
        func()
        print("装饰器2结束装饰")
    return wrapper2

@d1
@d2
def func():
    print("被装饰的函数")

if __name__ == '__main__':
    func()

上述代码运行的结果为:

装饰器1开始装饰
装饰器2开始装饰
被装饰的函数
装饰器2结束装饰
装饰器1结束装饰

可以看到非常对称的输出,同时证明被装饰的函数在最内层,转换成函数调用的代码如下:

d1(d2(func))

你在这部分需要注意的是,装饰器的外函数内函数之间的语句,是没有装饰到目标函数上的,而是在装载装饰器时的附加操作。
在对函数进行装饰的时候,外函数与内函数之间的代码会被运行。

测试效果如下:

import time

def d1(func):
    print("我是 d1 内外函数之间的代码")
    def wrapper1():
        print("装饰器1开始装饰")
        func()
        print("装饰器1结束装饰")
    return wrapper1

def d2(func):
    print("我是 d2 内外函数之间的代码")
    def wrapper2():
        print("装饰器2开始装饰")
        func()
        print("装饰器2结束装饰")
    return wrapper2

@d1
@d2
def func():
    print("被装饰的函数")

运行之后,你就能发现输出结果如下:

我是 d2 内外函数之间的代码
我是 d1 内外函数之间的代码

d2 函数早于 d1 函数运行。

接下来在回顾一下装饰器的概念:
被装饰的函数的名字会被当作参数传递给装饰函数。
装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。

这样看上文中的代码运行过程是这样的,d1(d2(func)) 执行 d2(func) 之后,原来的 func 这个函数名会指向 wrapper2 函数,执行 d1(wrapper2) 函数之后,wrapper2 这个函数名又会指向 wrapper1。因此最后的 func 被调用的时候,相当于代码已经切换成如下内容了。

# 第一步
def wrapper2():
     print("装饰器2开始装饰")
     print("被装饰的函数")
     print("装饰器2结束装饰")

# 第二步
print("装饰器1开始装饰")
wrapper2()
print("装饰器1结束装饰")

# 第三步
def wrapper1():
    print("装饰器1开始装饰")
    print("装饰器2开始装饰")
    print("被装饰的函数")
    print("装饰器2结束装饰")
    print("装饰器1结束装饰")

上述第三步运行之后的代码,恰好与我们的代码输出一致。

那现在再回到本小节一开始的案例,为何输出数据丢失掉了。

import time

def go(func):
    def wrapper(x, y):
        s_time = time.perf_counter()
        func(x, y)
        e_time = time.perf_counter()
        print(f"函数{func.__name__}运行时间是:{e_time-s_time}")
    return wrapper

def gogo(func):
    def wrapper(x, y):
        print("我是第二个装饰器")
    return wrapper

@go
@gogo
def func(x, y):
    i = 0
    while i < 1000:
        i += 1
    print(f"x={x},y={y}")

if __name__ == '__main__':
    func(33, 55)

在执行装饰器代码装饰之后,调用 func(33,55) 已经切换为 go(gogo(func)),运行 gogo(func) 代码转换为下述内容:

def wrapper(x, y):
    print("我是第二个装饰器")

在运行 go(wrapper),代码转换为:

s_time = time.perf_counter()
print("我是第二个装饰器")
e_time = time.perf_counter()
print(f"函数{func.__name__}运行时间是:{e_time-s_time}")

此时,你会发现参数在运行过程被丢掉了。

7.4 functools.wraps

使用装饰器可以大幅度提高代码的复用性,但是缺点就是原函数的元信息丢失了,比如函数的 __doc____name__

# 装饰器
def logged(func):
    def logging(*args, **kwargs):
        print(func.__name__)
        print(func.__doc__)
        func(*args, **kwargs)
    return logging

# 函数
@logged
def f(x):
    """函数文档,说明"""
    return x * x

print(f.__name__) # 输出 logging
print(f.__doc__) # 输出 None

解决办法非常简单,导入 from functools import wraps ,修改代码为下述内容:

from functools import wraps
# 装饰器
def logged(func):
    @wraps(func)
    def logging(*args, **kwargs):
        print(func.__name__)
        print(func.__doc__)
        func(*args, **kwargs)
    return logging

# 函数
@logged
def f(x):
    """函数文档,说明"""
    return x * x

print(f.__name__) # 输出 f
print(f.__doc__)  # 输出 函数文档,说明

7.5 基于类的装饰器

在实际编码中 一般 “函数装饰器” 最为常见,“类装饰器” 出现的频率要少很多。

基于类的装饰器与基于函数的基本用法一致,先看一段代码:

class H1(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<h1>' + self.func(*args, **kwargs) + '</h1>'

@H1
def text(name):
    return f'text {name}'

s = text('class')
print(s)

H1 有两个方法:

  • __init__:接收一个函数作为参数,就是待被装饰的函数;
  • __call__:让类对象可以调用,类似函数调用,触发点是被装饰的函数调用时触发。

最后在附录一篇写的不错的 博客,可以去学习。

在这里类装饰器的细节就不在展开了,等到后面滚雪球相关项目实操环节再说。

装饰器为类和类的装饰器在细节上是不同的,上文提及的是装饰器为类,你可以在思考一下如何给类添加装饰器。

7.6 内置装饰器

常见的内置装饰器有 @property@staticmethod@classmethod。该部分内容在细化面向对象部分进行说明,本文只做简单的备注。

7.6.1 @property

把类内方法当成属性来使用,必须要有返回值,相当于 getter,如果没有定义 @func.setter 修饰方法,是只读属性。

7.6.2 @staticmethod

静态方法,不需要表示自身对象的 self 和自身类的 cls 参数,就跟使用函数一样。

7.6.3 @classmethod

类方法,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数。

7.7 这篇博客的总结

关于 Python 装饰器,网上的文章实在太太多了,学习起来并不是很难,真正难的是恰到好处的应用在项目中,希望本篇博客能对你理解装饰器有所帮助。
其他内容也可以查阅 官方手册

相关阅读

  1. Python 爬虫 100 例教程,超棒的爬虫教程,立即订阅吧
  2. Python 爬虫小课,精彩 9 讲

今天是持续写作的第 <font color="red">103</font> / 200 天。
如果你想跟博主建立亲密关系,可以关注同名公众号 <font color="red">梦想橡皮擦</font>,近距离接触一个逗趣的互联网高级网虫。
博主 ID:梦想橡皮擦,希望大家<font color="red">点赞</font>、<font color="red">评论</font>、<font color="red">收藏</font>。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容