python装饰器的基本流程

[toc]

装饰器是python的一种语法模式,本质上是一种“函数的函数”。装饰器的主要目的是增强函数的功能,当多个函数需要重复使用同一个功能的时候,装饰器可以更加简介的进行实现。装饰器的底层逻辑是 函数的闭包,但这篇文章并不会涉及到。下面的内容将会从一个最简单、常见的案例出发,逐步推导装饰器的实现过程。

函数 是python中最基本的语法单元,假设有这么一个需求:

  • 需要统计每个函数的消耗时间
  • 需要在函数运行时打印函数名
  • 增强函数功能不能影响函数原有的参数和返回值
  • 需要对消耗时间做逻辑判断,如超过多少秒就记录下来

如果没有装饰器该如何实现

在没有装饰器的前提下,对一个函数统计其运行时间最简单的办法是直接修改原函数内部代码,比如在开始和结束后分别记录时间戳并进行相减运算。这种模式的劣势在于无法通用,每个函数都得增加重复代码块,且不便于以后的修改。那么很自然的逻辑是把“统计耗时”这个功能的代码块抽象出一个独立的方法,如下:

import time


def timer(func):
    start_time = time.time()
    func()
    end_time = time.time()
    print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")


def func():
    print('执行fun 函数...')
    time.sleep(1)


timer(func)

用简易装饰器如何实现

这种方法虽然不必要在每个函数进行增加重复的代码,但却需要在函数的调用处修改,将原函数包裹起来。同样,这种方法也不是很好的方式。那么,接下来的方法是,将函数作为参数传入到另一个函数中去,经过添加功能的修饰后,再把修饰后的复合函数返回出来作为一个新的复合函数,这样函数调用的时候,用这个新的复合函数即可。类似于给一颗糖包裹上一层糖衣。如下:

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
    return wrapper


def func():
    print('执行fun 函数...')
    time.sleep(1)


var = timer(func)
var()
# timer(func)() 上述两行可以等价于这种语法
执行fun 函数...
函数func 消耗时间 1.0023250579833984

这种把函数作为参数的方式传入,经过修饰后再返回的过程就是python装饰器的基本模型。但如上代码所示,这种方式看起来也不是简洁的,每次调用的时候还得使用装饰函数包裹一次,再用新的函数执行。所以在python中这一步骤用@的语法糖进行替代。

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
    return wrapper


@timer
def func():
    print('执行fun 函数...')
    time.sleep(1)


func()
执行fun 函数...
函数func 消耗时间 1.0023250579833984

如上,函数fun新增的功能通过另外一个装饰性函数实现每次调用核心函数的时候,因为装饰器语法糖的存在,总是先将核心函数作为参数传入装饰函数中,执行修饰形后代码逻辑。相比最初的思路,这种方式简洁、阅读性好,且不需要改动核心函数的逻辑。到此,python装饰器的最简过程已经完成。

如果核心函数有返回值怎么办

上述的模型中,核心函数是一个没有输入和输出的函数。但如果核心函数有输出,即返回值的时候,因为函数生命周期的存在,经过修饰后的复合函数并没有接收到核心函数的返回值。所以如果用上述的最简装饰模型测试,调用函数的返回值为None。

所以,当核心函数有返回值时,必须在装饰函数的内部也将返回值传递出来给复合函数。如下:

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        result = func()
        end_time = time.time()
        print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
        return result  # 将核心函数的输出值,丢给装饰函数保管
    return wrapper


@timer
def func():
    print('执行fun 函数...')
    time.sleep(1)
    return 1024  # 核心函数有输出


result = func()
print(result)
执行fun 函数...
函数func 消耗时间 1.0048329830169678
1024

如果核心函数有参数怎么办

同理,如果核心函数不仅有返回值,也有输入值,即参数,那么修饰函数也必须接受该参数的输入。但在案例中,修饰方法接收的是任意函数,无法确定核心函数的参数类型。所以用万能参数替代 *args, **kwargs 如下:

import time


def timer(func):
    def wrapper(*args, **kwargs):  # 核心函数携带参数,丢给装饰函数输入
        start_time = time.time()
        result = func(*args, **kwargs)  # 核心函数执行过程接受参数
        end_time = time.time()
        print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
        return result  # 将核心函数的输出值,丢给装饰函数保管
    return wrapper


@timer
def func(x, y):
    # 带参数的核心函数
    print('执行fun 函数...')
    time.sleep(1)
    return x+y  # 核心函数有输出


result = func(1024, 996)
print(f"核心函数输出:{result}")
print(f"调用函数名称为:{func.__name__}")
执行fun 函数...
函数func 消耗时间 1.0039029121398926
核心函数输出:2020
调用函数名称为:wrapper

在这一过程中,修饰函数并不关心核心函数的具体逻辑,也不关心参数是什么形式。所做的只是把核心函数的参数一起包裹进来,并将核心函数的返回值打包丢给复合函数并丢出来。

关于复合函数的名字

python中函数有自己的名字,通过func.__name__即可看到。上面的测试代码显示,复合函数的函数名是wrapper。这样的结果符合代码逻辑,但不符合业务逻辑。例如糖衣包裹的棉花糖应该叫棉花糖,而不是叫糖衣。为解决这个问题,可以在修饰器中返回wrapper之前,把wrapper的name重置为func的name。如下:

# 函数名重置
wrapper.__name__ = function.__name__ 
wrapper.__doc__ = function.__doc__
return wrapper

作为装饰器的通用功能,python内部用另一个装饰器进行修饰来代替上面这两行代码。functools.wraps(function)这个内置的装饰器的功能实际上等价于上诉两行代码。

所以上面的装饰器经过再次修饰后,最终的形式如下:

import time
import functools
def timer(func):
    @functools.wraps(func) # 内置装饰器,让被修饰后的函数的函数名与原函数一致
    def wrapper(*args, **kwargs):  # 核心函数携带参数,丢给装饰函数输入
        start_time = time.time()
        result = func(*args, **kwargs)  # 核心函数执行过程接受参数
        end_time = time.time()
        print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
        return result  # 将核心函数的输出值,丢给装饰函数保管
    return wrapper
执行fun 函数...
函数func 消耗时间 1.000427007675171
核心函数输出:2020
调用函数名称为:func

至此,案例中的需求已经基本满足。从头来看完整的过程,即便不考虑底层逻辑,也可以从中发现一个完整的装饰器实现模型:

  1. 需要给核心函数增加功能
  2. 将核心函数当参数传入修饰器
  3. 将核心函数的参数也传入修饰器
  4. 核心函数的返回值在修饰中被抛出,交给复合函数托管
  5. 给核心函数添加装饰器语法糖变成一个复合函数
  6. 调用核心函数实际上是调用复合函数
  7. 最后解决函数名的归属问题

装饰器的基本模版如下:


image

如果装饰器需要定制

上述的需求中还有一个对计时进行逻辑判断的需求,如果对于每个核心函数的判断都一样,那么在装饰器中直接增加代码逻辑即可。但如果每个核心函数的判断不一样,或者不同时间的要求不一样,那么就需要定制装饰器,把装饰器再装饰一下,也就是装饰器的套娃。

import time
import functools


def timer_super(max_time=1):  # 在原装饰器上继续套娃,增加装饰器参数
    def timer(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            print(f"函数{func.__name__} 消耗时间 {end_time-start_time}")
            if (end_time-start_time) > max_time:
                print(f"超过最大耗时 {max_time}s")
            return result
        return wrapper
    return timer


@timer_super(0.5)
def func(x, y):
    print('执行fun 函数...')
    time.sleep(1)
    return x+y  # 核心函数有输出


result = func(1024, 996)
print(f"核心函数输出:{result}")
print(f"调用函数名称为:{func.__name__}")

执行fun 函数...
函数func 消耗时间 1.001241683959961
超过最大耗时 0.5s
核心函数输出:2020
调用函数名称为:func

总结

装饰器的目的是重构重复且通用代码块,使代码得到简化,提高阅读性。上面的案例中,只是最常用的统计函数运行时间。装饰器还可以应用到打印日志、登陆检查、邮件发送等等具体的业务场景。同时,装饰器的实现也不一定是函数的形式,也可以是装饰器类。但无论是装饰器函数还是装饰器类,其基本逻辑和模版并没有很大的区别。

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

推荐阅读更多精彩内容