了解Python装饰器

说起Python的装饰器(Decorator),其实是一个特别有用且有趣的功能,可以让原本的Python函数(不只是函数)拥有之前没有的功能,使函数的功能更完美、健壮,并且不需要修改该函数原来的代码,用起来十分简单易懂。

不过呢,在了解装饰器之前,我们需要先了解一下闭包的概念。

闭包

其实装饰器就是一个闭包(closure)。闭包简单来讲就是一个函数定义中,引用了函数外定义的变量,并且该函数可以在其定义环境外被执行,可以延申函数的作用域。

"""案例一"""

# outer是外函数
def outer():
    msg = "I am closure"
    
    # inner是内函数
    def inner():
        print(msg)

    return inner


closure = outer()

# 输出 I am closure
closure()

由上述代码可见,闭包其实就是外函数中定义内函数(套娃),内函数还可以使用外函数的变量。

闭包所实现的功能,有些类似于将函数作为参数传参,可参考下列代码理解。

"""案例二"""

def outer(func):
    print(func())


def inner():
    msg = "I am parameter"
    return msg

# 输出 I am parameter
outer(inner)

通过上述两种代码再看闭包的概念,其实也挺容易理解的。

一个简单的装饰器

理解了闭包,那么就可以更友好的理解装饰器。前文说过,装饰器其实就是一个闭包,装饰器也是通过函数嵌套的方式实现。

"""案例三"""

# 定义装饰器tips
def tips(func):
    def wrapper(*args, **kwargs):
        print("计算完成:")
        return func(*args, **kwargs)

    return wrapper


# 调用装饰器tips
@tips
def add(a,b):
    print(f"{a}+{b}={a+b}")


# 计算完成:
# 1+2=3
add(1, 2)

分析一下上述代码。当add函数被装饰时,add函数就成为了装饰器的参数func,当执行过装饰器内的wrapper函数后,最后再返回func函数,即执行add函数。

由此可见,装饰器可以让我们自定义的给函数新增功能,可以在其调用之前,亦可在其调用之后。

@functools.wraps(func)

笔者之前做的一个flask项目中,曾经遇见过一个问题,那就是装饰器的重名问题,困扰了笔者很长时间,也只能怪当时学艺不精。

以上述代码为例。说到底,装饰器不过都是执行的wrapper函数罢了。既然如此,add函数的属性要怎么访问呢?

@tips
def add(a, b):
    print(f"当前函数:{add.__name__}")
    print(f"{a}+{b}={a+b}")

上述代码对案例三中的add函数进行了小修改,添加了一条输出语句,通过魔法方法name让其输出当前函数名称。然而,结果却出人意料。

# 调用add函数
add(1, 2)

# 计算完成:
# 当前函数:wrapper
# 1+2=3

可见,装饰器使add本身的属性消失了。上文中笔者便提到,装饰器执行的都是内部的包装函数(调用闭包),也就是当前装饰器中的wrapper函数。

由此可见,一旦函数增加了装饰器,那么函数本身的属性会被装饰器内部的包装函数替换。@functools.wraps(func)便是解决该问题的关键。

引入wraps后,对案例三的tips装饰器进行修改。

"""案例四"""

from functools import wraps

# 装饰器tips
def tips(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("计算完成:")
        return func(*args, **kwargs)

    return wrapper

@wraps是python提供的装饰器。它能把原函数的元信息拷贝到装饰器里面的func函数中,这个函数将自动为包装器函数添加一个 __ wrapped __ 属性,以此为关联被包装的函数,让我们在装饰器里面访问在装饰之前的函数的属性。

此时,再调用add函数。

# 调用add函数
add(1, 2)

# 计算完成:
# 当前函数:add
# 1+2=3

带有参数的装饰器

一个携带了参数的装饰器将有三层函数(套娃),第一层函数用来接收装饰器传递的参数。请参考下列代码。

"""案例五"""

from functools import wraps

# 装饰器tips
def tips(parameter):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(parameter)
            return func(*args, **kwargs)

        return wrapper

    return decorator


# 调用装饰器tips
@tips('计算完成:')
def add(a, b):
    print(f"当前函数:{add.__name__}")
    print(f"{a}+{b}={a+b}")


# 调用add函数
add(1, 2)

# 输出:
# 计算完成:
# 当前函数:add
# 1+2=3

装饰器重叠

当一个被装饰的对象同时叠加多个装饰器时,装饰器的加载顺序是由下而上,装饰器的执行顺序是由上而下。只要了解这个顺序,便可以对装饰器进行各种各样的重叠使用了。

"""案例六"""

from functools import wraps

# 第一个装饰器
def first_decorator(func):
    print("i am first")

    @wraps(func)
    def wrapper(*args, **kwargs):
        print("i am no.1")
        return func(*args, **kwargs)

    return wrapper


# 第二个装饰器
def second_decorator(func):
    print("i am second")

    @wraps(func)
    def wrapper(*args, **kwargs):
        print("i am no.2")
        return func(*args, **kwargs)

    return wrapper


# 为text函数增加两个装饰器
@first_decorator  
@second_decorator
def text():
    print("i am text")


# 调用text函数
text()

上述代码的执行结果为:

# i am second
# i am first
# i am no.1
# i am no.2
# i am text

由上述可见,所谓的加载程序即装饰器第一层的函数的执行过程,会按照由下往上进行加载。

执行程序即调用装饰器内的wrapper函数,会按照由上往下执行。

大家可以尝试使用一些开发工具的debug模式逐步观察运行顺序,以便加深理解。

类装饰器

首先,需要先说明一下,装饰器不仅仅只能装饰函数,还可以装饰类。

"""案例七"""

from functools import wraps

# 定义装饰器laugh
def laugh(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("ha ha ha")
        return func(*args, **kwargs)
    return wrapper


# 调用装饰器laugh
@laugh
class Animal:
    def __init__(self, a_type=''):
        self.a_type = a_type
    
    # 重写魔法方法str,便于友好展示
    def __str__(self):
        if self.a_type:
            return f"i am {self.a_type}"
        else:
            return "i am animal"


dog = Animal("dog")

print(dog)
print(Animal())

# 输出:
# ha ha ha
# i am dog
# ha ha ha
# i am animal

我们也可以通过重写类的魔法方法call,使类具有装饰器的效果(类装饰器)。

"""案例八"""

# 定义Motion类
class Motion:
    def __init__(self, func):
        self.func = func
    
    # 重写魔法方法call
    def __call__(self, *args, **kwargs):
        print("i am decorator")

        return self.func(*args, **kwargs)


# 调用类装饰器Motion
@Motion
def run():
    print("i am run")


# 调用run函数
run()

# 输出
# i am decorator
# i am run

以上便是笔者对Python装饰器的简单理解及一些简单案例。笔者不由得感叹python语言的简洁精悍。

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

推荐阅读更多精彩内容

  • 一、装饰器的基本使用 在不改变函数源代码的前提下,给函数添加新的功能,这时就需要用到“装饰器”。 0.开放封闭原则...
    NJingZYuan阅读 529评论 0 0
  • 在python编程中,我们经常看到下面的函数用法: with open("test.txt", "w") as f...
    hugoren阅读 825评论 0 0
  • 装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返...
    胡一巴阅读 416评论 0 0
  • 部分细节自己改了点,也加了点自己例子,基本上属于转载。转载出处:https://my.oschina.net/le...
    洛克黄瓜阅读 1,976评论 0 3
  • 算了,怕我记住太差以后忘了说。 其实昨天删除无效信息的时候才发现,当你问我看什么?看你的自拍照么?我无言以对,隐隐...
    阿布歲月不擾阅读 246评论 0 0