闭包、装饰器

在学习 Python 的时候,庆幸自己有 JavaScript 的基础,在学习过程中,发现许多相似的地方,如导包的方式、参数解包、lambda 表达式、闭包、引用赋值、函数作为参数等。
装饰器是 Python 语言中的一个重点,要学习装饰器的使用,应该首先从闭包看起,以掌握装饰器的基本原理。

词法作用域

闭包是函数能够访问并记住其所在的词法作用域,由于 Python 和 JavaScript 都是基于词法作用域的,所以二者闭包的概念是共通的。
为了说明 Python 是基于词法作用域,而不是动态作用域,我们可以看下面的例子:

def getA():
    return a

def test():
    a = 100
    print(getA())

test()

运行结果为:

Traceback (most recent call last):
  File "C:\Users\Charley\Desktop\py\py.py", line 8, in <module>
    test()
  File "C:\Users\Charley\Desktop\py\py.py", line 6, in test
    print(getA())
  File "C:\Users\Charley\Desktop\py\py.py", line 2, in getA
    return a
NameError: name 'a' is not defined

报错了~我们再在 getA 函数所在的作用域声明一个变量 a

def getA():
    return a
    
a = 10010
def test():
    a = 100
    print(getA())

test()

运行结果为:

10010

这里输出了 10010 ,说明 getA 函数是依赖于词法作用域的,其作用域在函数定义伊始就决定的,其作用域不受调用位置的影响。
理解了词法作用域,就理解了闭包。

闭包

前面说到过:闭包是函数能够访问并记住其所在的词法作用域的特性。那么只要函数拥有这个特性,这个函数就是一个闭包,理论上,所有函数都是闭包,因为他们都可以访问其所在的词法作用域。像这样的函数也是一个闭包:

a = 100
def iAmClosure():
    print(a)

iAmClosure()

理论上是如此,但在实际情况下,闭包的定义要复杂一点点,但仍然基于前面的理论:如果一个函数(外层函数)中返回另外一个函数(内层函数),内层函数能够访问外层函数所在的作用域,这就叫一个闭包。
下面是一个例子:

def outer():
    a = 100
    def inner():
        print(a)
    return inner

outer()()

运行结果如下:

100

如上所示,inner 函数就是一个闭包。

闭包中调用参数函数

同 JavaScript,Python 中也可以将函数作为参数传递,由于 Python 是引用传值,因此实际上传入的参数并不是原始函数本身,而是原始函数的一个引用。基于闭包的特性,我们也可以在闭包中访问(或者说调用)这个函数:

def outer(fn):
    def inner():
        # 闭包能够访问并记住其所在的词法作用域
        # 因此在闭包中可以调用 fn 函数
        fn()
    return inner

def test():
    print("We will not use 'Hello World'")

ret = outer(test)
ret()

运行结果:

We will not use 'Hello World'

装饰器引入

认识了闭包,就可以来说一说装饰器了。想象有这么一种需求:
你所在的公司有一些核心的底层方法,后来公司慢慢壮大,增加了其他的部门,这些部门都有自己的业务,但它们都会使用这些核心的底层方法,你所在的部门也是如此。
有一天,项目经理找到你,让你在核心代码的基础上加一些验证之类的玩意,但是不能修改核心代码(否则会影响到其他的部门),你该怎么做呢?
首先你可能想到这种方式:

def core():
    pass

def fixCore():
    doSometing()...
    core()

fixCore()

通过一个外层函数将 core 函数进行包装,在执行了验证功能后再调用 core 函数。
这时项目经理又说了,你不能改变我们的调用方式呀,我们还是想以 core 的方式进行调用,于是你又修改了代码:

def core():
    pass

tmp = core;
def fixCore():
    tmp()

core = fixCore
core()

通过临时函数 tmp 交换了 corefixCore,狸猫换太子。这下就可以愉快的直接使用 core 了。
这是项目经理又说了,我们需要对多个核心函数进行包装,总不能全部使用变量交换吧,并且这样很不优雅,再想想其他的办法?
好吧,要求真多!于是你想啊想,想到了闭包的方式:将需要包装的函数作为参数传入外层函数,外层函数返回一个内层函数,该函数在执行一些验证操作后再调用闭包函数。这样做的好处是:

  • 可以对任意函数进行包装,只要将函数作为参数传入外层函数
  • 可以在执行外层函数时对返回值进行任意命名

你写的代码是这个样子的:

# 外层函数,接收待包装的函数作为参数
def outer(fn):
    def inner():
        doSometing()...
        fn()
    return inner

# 核心函数1
def core1():
    pass

# 核心函数2
def core2():
    pass

# core1 重新指向被包装后的函数
core1 = outer(core1)
# core2 重新指向被包装后的函数
core2 = outer(core2)

# 调用核心函数
core1()
core2()

大功告成!简直完美。同时恭喜你,你已经实现了一个装饰器,装饰器的核心原理就是:闭包 + 函数实参。

Python 原生装饰器支持

在上面你已经实现了一个简单的装饰器,也知道了装饰器的基本原理。其实,在 Python 语言中,有着对装饰器的原生支持,但核心原理依旧不变,只是简化了一些我们的操作:

# 外层函数,接收待包装的函数作为参数
def outer(fn):
    def inner():
        print("----验证中----")
        fn()
    return inner

# 应用装饰器
@outer
# 核心函数1
def core1():
    print("----core1----")
# 应用装饰器
@outer
# 核心函数2
def core2():
    print("----core2----")

core1()
core2()

运行结果如下:

----验证中----
----core1----
----验证中----
----core2----

Python 原生的装饰器支持,省去了传参和重命名的步骤,应用装饰器时,会将装饰器下方的函数(这里为 core1core2)作为参数,并生成一个新的函数覆盖原始的函数。

装饰器函数的执行时机

装饰器函数(也就是我们前面所说的外层函数)在什么时候执行呢?我们可以进行简单的验证:

def outer(fn):
    print("----正在执行装饰器----")
    def inner():
        print("----验证中----")
        fn()
    return inner

@outer
def core1():
    print("----core1----")

@outer
def core2():
    print("----core2----")

运行结果为:

----正在执行装饰器----
----正在执行装饰器----

这里我们并没有直接调用 core1core2 函数,装饰器函数执行了。也就是说,解释器执行过程中碰到了装饰器,就会执行装饰器函数

多重装饰器

我们也可以给函数应用多个装饰器:

def outer1(fn):
    def inner():
        print("----outer1 验证中----")
        fn()
    return inner

def outer2(fn):
    def inner():
        print("----outer2 验证中----")
        fn()
    return inner

@outer2
@outer1
def core1():
    print("----core1----")

core1()

运行结果如下:

----outer2 验证中----
----outer1 验证中----
----core1----

从输出效果中可以看到:装饰器的执行是从下往上的,底层装饰器执行完成后返回函数再传给上层的装饰器,以此类推。

给被装饰函数传参

如果我们需要给被装饰函数传参,就需要在装饰器函数返回的 inner 函数上做文章了,让其代理接受被装饰器函数的参数,再传递给被装饰器函数:

def outer(fn):
    def inner(*args,**kwargs):
        print("----outer 验证中----")
        fn(*args,**kwargs)
    return inner


@outer
def core(*args,a,b):
    print("----core1----")
    print(a,b)

core(a = 1,b = 2)

运行结果为:

----outer 验证中----
----core1----
1 2

这里提一下 参数解包的问题:在 inner 函数中的 *** 表示该函数接受的可变参数和关键字参数,而调用参数函数 fn 时使用 *** 表示对可变参数和关键字参数进行解包,类似于 JavaScript 中的扩展运算符 ...。如果直接将 argskwargs 作为参数传给被装饰函数,那么被装饰函数接收到的只是一个元组和字典,所以需要在解包后传入。

对有返回值的函数进行包装

如果被包装函数有返回值,如何在包装获取返回值呢?先看一下下面的例子:

def outer(fn):
    def inner():
        print("----outer 验证中----")
        fn()
    return inner

@outer
def core():
    return "Hello World"

print(core())

运行结果为:

----outer 验证中----
None

为什么函数执行的返回值是 None 呢?不应该是 Hello World 吗?这是因为装饰的过程其实是引用替换的过程,在装饰之前,core 变量指向其自初始的函数体,在装饰后就重新进行了指向,指向到了装饰器函数所返回的 inner 函数,我们没有给 inner 函数定义返回值,自然在调用装饰后的 core 函数也是没有返回值的。为了让装饰后的函数仍有返回值,我们只需让 inner 函数返回被装饰前的函数的返回值即可

def outer(fn):
    def inner():
        print("----outer 验证中----")
        return fn()
    return inner

@outer
def core():
    return "Hello World"

print(core())

运行结果如下:

----outer 验证中----
Hello World

装饰器的参数

有时候我们想要根据不同的情况对函数进行装饰,可以有以下两种处理方式:

  • 定义多个不同条件下的装饰器,根据条件应用不同的装饰器
  • 定义一个装饰器,在装饰器内部根据条件的不同进行装饰

第一种方法很简单,这里说一下第二种方式。
要在装饰器内部对不同条件进行判断,我们就需要一个或多个参数,将参数传入:

# main 函数接受参数,根据参数返回不同的装饰器函数
def main(flag):
    # flag 为 True
    if flag:
        def outer(fn):
            def inner():
                print("立下 Flag")
                fn()
            return inner
        return outer
    # flag 为 False
    else:
        def outer(fn):
            def inner():
                print("Flag 不见了!")
                fn()
            return inner
    return outer

# 给 main 函数传入 True 参数
@main(True)
def core1():
    pass

# 给 main 函数传入 False 参数
@main(False)
def core2():
    pass

core1()
core2()

运行结果如下:

立下 Flag
Flag 不见了!

上面我们根据给 main 传入不同的参数,对 core1core2 函数应用不同的装饰器。这里的 main 函数并不是装饰器函数,其返回值才是装饰器函数,我们是根据 main 函数的返回值对目标函数进行装饰的

类作为装饰器

除了函数,类也可以作为装饰器,在说类作为装饰器之前,首先需要了解 __call__ 方法。

__call__ 方法

我们创建的实例也是可以调用的, 调用实例对象将会执行其内部的 __call__ 方法,该方法需要我们手动实现,如果没有该方法,实例就不能被调用:

class Test(object):
    def __call__(self):
        print("我被调用了呢")

t = Test()
t()

运行结果:

我被调用了呢

类作为装饰器

我们已经知道对象的 __call__ 方法在对象被调用时执行,其实类作为装饰器的结果就是将被装饰的函数指向该对象,在调用该对象时就会执行对象的 __call__ 方法,要想让被装饰的函数执行 __call__ 方法,首先会创建一个对象,因此会连带调动 __new____init__ 方法,在创建对象时,test 函数会被当做参数传入对象的 __init__ 方法。

class Test(object):
    # 定义 __new__ 方法
    def __new__(self,oldFunc):
        print("__new__ 被调用了")
        return object.__new__(self)
    # 定义 __init__ 方法
    def __init__(self,oldFunc):
        print("__init__ 被调用了")
    # 定义 __call__ 方法
    def __call__(self):
        print("我被调用了呢")

# 定义被装饰函数
@Test
def test():
    print("我是test函数~~")

test()

运行结果:

__new__ 被调用了
__init__ 被调用了
我被调用了呢

保存原始的被装饰函数

装饰后的 test 函数指向了新建的对象,那么有没有办法保存被装饰之前的原始函数呢?通过前面我们已经知道,在新建对象的时候,被装饰的函数会作为参数传入 __new____init__ 方法,因此我们可以在这两个方法中获取原始函数的引用:

class Test(object):
    # 定义 __new__ 方法
    def __new__(self,oldFunc):
        print("__new__ 被调用了")
        return object.__new__(self)
    # 定义 __init__ 方法
    def __init__(self,oldFunc):
        print("__init__ 被调用了")
        self.__oldFunc = oldFunc
    # 定义 __call__ 方法
    def __call__(self):
        print("我被调用了呢")
        self.__oldFunc()

# 定义被装饰函数
@Test
def test():
    print("我是test函数~~")

test()

运行结果如下:

__new__ 被调用了
__init__ 被调用了
我被调用了呢
我是test函数~~

总结

本文主要讲到了 Python 中闭包和装饰器的概念,主要有以下内容:

  • Python 是基于词法作用域
  • 闭包是函数能记住并访问其所在的词法作用域
  • 利用礼包实现简单的装饰器
  • Python 原生对装饰器的支持
  • 给函数应用多个装饰器
  • 如何给被装饰函数传参
  • 如何给有返回值的函数应用装饰器
  • 如何根据不同条件为函数应用不同的装饰器
  • 类作为装饰器的情况以及 __call__ 方法

完。

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

推荐阅读更多精彩内容