Python——第四章:闭包(Closure)、装饰器(Decorators)

闭包:

本质, 内层函数对外层函数的局部变量的使用. 此时内层函数被称为闭包函数
    1. 可以让一个变量常驻与内存,可随时被外层函数调用。
    2. 可以避免全局变量被修改、被污染、更安全。(通过版本控制工具,将不同人所写的代码都整合的时候,避免出现问题)

def func():
    a = 10
    def inner():
        print(a)
        return a
    return inner

ret = func()

代码定义了一个函数 func,它返回了另一个函数 inner。这种结构被称为闭包(closure),因为 inner 函数引用了在其外部定义的变量 a。在这里,a 是 func 函数的局部变量,但由于 inner 函数引用了它,a 的值在 inner 函数被调用时仍然是可用的。

这段代码可以实现的特殊效果:

1、因为迟迟没有使用ret()调用retinner函数),程序为了能够持续提供可用性,会将该段代码常驻于内存。不会被内存回收。

2、用函数来定义局部变量a,并且局部变量不会被后续函数外的代码操控、改变,仅可以被赋值后读取、打印。不能被其他全局变量修改。实现局部变量仅可以在本函数内部才可以被操作。a被保护起来了。

3、想调用这个局部变量a,可以随时调用ret()函数,使得局部变量既可以被调用,还不会被修改。

未来某一时刻,ret()调用了 func 函数并将其结果赋值给变量 ret。此时,ret 包含了 inner 函数。如果你调用 ret(),它将输出 10,因为 inner 函数引用了外部的 a 变量,而 a 的值在 func 函数被调用时被设置为 10

 

再看下面一段代码

def func():
    a = 0
    def inner():
        nonlocal a
        a += 1
        return a
    return inner


ret = func()
a = 20    #此时即使出现了全局变量a=20,也不会干扰到func函数局部变量a的计数器累计
# inner => ret => 什么时候执行
r1 = ret()
print(r1)    #第一次输出,结果为1

# 可能1000000行后才执行

r2 = ret()
print(r2)    #第二次输出,结果为2

print(ret())    #结果为3
print(ret())    #结果为4

这段代码,常驻于内存,有实现内部计数器的作用。

 

装饰器

装饰器本质上是一个闭包

作用:在不改变原有函数调用的情况下. 给函数增加新的功能.(直白地说: 可以在函数前后添加新功能, 但是不改原来的代码)

哪里会用到装饰器:

  1. 程序用户登录的地方
  2. 操作日志(增、删、改、查)
  3. 可以在函数前后添加新功能, 但是不改变原来的代码功能。

 

一、推导装饰器需要用到的原理:

  1. 函数可以做为参数进行传递(代理执行)
    def func():
        print('我是函数')
    
    def gggg(fn):  # fn要求是个函数
        fn()  # func()
    
    gggg(func)
  2. 函数可以作为返回值进行返回(闭包)
    def func():
        def inner():
            print("123")
        return inner
    
    ret = func()
    ret()
  3. 函数名称可以当成变量一样进行赋值操作
    def func1():
        print("我是函数1")
    def func2():
        print("我是函数2")
    
    func1 = func2
    func1()

二、装饰器的推导过程:

1.定义2个游戏,运行2个游戏

def play_dnf():
    print("开始玩dnf游戏")
def play_lol():
    print("开始玩lol游戏")
    
play_dnf()
play_lol()

2.此时我们出现了新的需求:

  • 在运行dnf游戏前,先打开外挂;在结束游戏后,关闭外挂。
  • 在运行lol游戏前,先打开外挂;在结束游戏后,关闭外挂。
  • 我们要用游戏管家自动搞这2个事情

因此我们又试图定义了一个管家,并且用代理执行的逻辑(函数可以做为参数进行传递)传输函数执行.

def guanjia(game):
    print("打开外挂")
    game()  # 玩起来了
    print('关闭外挂')
    
def play_dnf():
    print("开始玩dnf游戏")
def play_lol():
    print("开始玩lol游戏")

guanjia(paly_dnf)
guanjia(paly_lol)

但是我们运行后发现:打游戏的主题不是我,而是管家 。这样就非常的不合理。因为运行的主体变了。我们只想要管家负责在我玩游戏之前开外挂、玩游戏之后关外挂,而不是代理打游戏。

因此我们使用了:

1、闭包的玩法(函数可以作为返回值进行返回);

2、函数名称可以当成变量一样进行赋值操作,再次对游戏进行封装;

def guanjia(game):
    def inner():
        print("打开外挂")
        game()  # 玩起来了
        print('关闭外挂')
    return inner

def play_dnf():
    print("开始玩dnf游戏")
def play_lol():
    print("开始玩lol游戏")

play_dnf = guanjia(play_dnf)  # 让管家把游戏重新封装一遍. 我这边把原来的游戏替换了
play_lol = guanjia(play_lol)  # 让管家把lol也重新封装一下.

play_dnf()  # 此时运行的是管家给的内层函数inner
play_lol()

解读:这里是把play_dnf(实参)传给game(形参),然后用return返回inner,把打开外挂、玩游戏、关闭外挂一切都返回给(全新的)打包的play_dnf = guanjia(play_dnf),让一切都是我执行的。

这样操作就把原先的管家打游戏,变回了我打游戏,只是这一切都是“我自己操作的”。主体没有发生变化。

*注意:因为这种写法play_dnf = guanjia(play_dnf)非常不容易阅读,容易造成混淆,并且有更好的替代写法——在运行程序的前面进行标记(@guanjia),每次你要运行程序之前,guanjia就会自动加载封装的代码。

def guanjia(game):
    def inner():
        print("打开外挂")
        game()  # 玩起来了
        print('关闭外挂')
    return inner

@guanjia        
def play_dnf():
    print("开始玩dnf游戏")

@guanjia        # 相当于 play_dnf = guanjia(play_lol)
def play_lol():
    print("开始玩lol游戏")

play_dnf()    # 因为前面有@guanjia,这里就相当于 play_dnf = guanjia(play_dnf)
play_lol()    # 因为前面有@guanjia,这里就相当于 play_dnf = guanjia(play_lol)

至此,我们搞到了装饰器的雏形:

def wrapper(fn):   #wrapper: 装饰器, fn: 目标函数
    def inner:
        pass    # 在目标函数执行之前操作
        fn()   # 执行目标函数
        pass    # 在目标函数执行之后操作
    return inner     #千万别加()

@wrapper    #让装饰器代理运行目标函数
def target():    #目标函数
    pass

target()  #  实际上这里运行的是wrapper里的 =>inner()

被装饰函数的参数问题

新的问题又来了:比如目标函数中有参数,打lol游戏或者dnf要输入账号和密码

def play_dnf(username, password):
    print("打开dnf,输入账号密码。 ", username, password)
    print('开始玩dnf游戏!')
    
play_dnf(admin,123456)

#运行结果
打开dnf,输入账号密码。  admin 123456
开始玩dnf游戏!

不加装饰器,我们这样是可以正常执行程序的。

但是加这个装饰器雏形里是没有办法加入任何参数的

def guanjia(game):
    def inner():
        print("打开外挂")
        game()  # 玩起来了
        print('关闭外挂')
    return inner

@guanjia
def play_dnf(username, password):
    print("打开dnf,输入账号密码。 ", username, password)
    print('开始玩dnf游戏!')
    
play_dnf("admin","123456")

#执行结果
    play_dnf("admin", "123456")
TypeError: guanjia.<locals>.inner() takes 0 positional arguments but 2 were given #在guanjia的局部变量的inner里,没有位置接收变量,但是程序却给了2个

Process finished with exit code 1

TypeError: guanjia.<locals>.inner() takes 0 positional arguments but 2 were given 在guanjia的局部变量的inner里,没有位置接收变量,但是程序却给了2个

因此我们需要改造装饰器的def inner()def inner(username,password)如下执行

def guanjia(game):
    def inner(username, password):
        print("打开外挂")
        game()  # 玩起来了
        print('关闭外挂')
    return inner

@guanjia
def play_dnf(username, password):
    print("打开dnf,输入账号密码。 ", username, password)
    print('开始玩dnf游戏!')

play_dnf("admin", "123456")

#执行结果
打开外挂
Traceback (most recent call last):
  File "D:\装饰器.py", line 13, in <module>
    play_dnf("admin", "123456")
  File "D:\装饰器.py", line 4, in inner
    game()  # 玩起来了
    ^^^^^^
TypeError: play_dnf() missing 2 required positional arguments: 'username' and 'password'

Process finished with exit code 1

第13行存在的参数,给到第4行的game() 时,缺少了2个参数'username' and 'password'

为此我们也需要将game()变成game(username, password),把参数也给到game中,并将来一起作为inner返回给原函数做调用(来回穿透传输),程序就顺利执行起来了.

def guanjia(game):
    def inner(username, password):
        print("打开外挂")
        game(username, password)  # 玩起来了
        print('关闭外挂')
    return inner

@guanjia
def play_dnf(username, password):
    print("打开dnf,输入账号密码。 ", username, password)
    print('开始玩dnf游戏!')

play_dnf("admin", "123456")

#执行结果
打开外挂
打开dnf,输入账号密码。  admin 123456
开始玩dnf游戏!
关闭外挂

但是,新的问题又来了,dnf暂时解决了,但是再加入lol的时候,又来了新问题,lol的参数更多,还有hero.

def guanjia(game):
    def inner(username, password):
        print("打开外挂")
        game(username, password)  # 玩起来了
        print('关闭外挂')
    return inner

@guanjia
def play_dnf(username, password):
    print("打开dnf,输入账号密码。", username, password)
    print("开始玩dnf游戏!")

@guanjia
def play_lol(username, password, hero):
    print("打开lol,输入账号密码,选择英雄。", username, password, hero)
    print("开始玩lol游戏!")

play_dnf("admin", "123456")
play_lol("admin", "456789", "盖伦")

#执行结果
    play_lol("admin", "456789", "盖伦")
TypeError: guanjia.<locals>.inner() takes 2 positional arguments but 3 were given

TypeError: guanjia.<locals>.inner() takes 2 positional arguments but 3 were given在guanjia的局部变量的inner里,有2个位置函数接收变量,但是程序却给出了3个。

这里为了要让guanjia接收到任意函数,并回传给目标函数。我们得采用通用写法,可实现通用调用。

def guanjia(game):
    #         *和**表示接收所有参数, *把所有位置参数打包成元组;**把所有关键字参数打包成字典。
    def inner(*args, **kwargs):  # 给inner添加了参数, args一定是一个元组;kwargs一定是字典(admin, 123456, "大盖伦")
        print("打开外挂")
        #    *, ** 表示把args元组打散成位置参数,以及把kwargs字典打散成关键字参数,传递进game()去
        game(*args, **kwargs)  # 玩起来了  # game('admin', '123456', "大盖伦")
        print('关闭外挂')
    return inner

@guanjia
def play_dnf(username, password):
    print("打开dnf,输入账号密码。", username, password)
    print("开始玩dnf游戏!")

@guanjia
def play_lol(username, password, hero):
    print("打开lol,输入账号密码,选择英雄。", username, password, hero)
    print("开始玩lol游戏!")

play_dnf("admin", "123456")
play_lol("admin", "456789", "盖伦")

注意:在game(*args, **kwargs)这个表达式中,*args的作用是将元组中的元素解包,分别作为位置参数传递给func函数。这就是所谓的“打散”操作。同样地,**kwargs会将字典中的键值对解包成关键字参数传递给game

可以参考阅读:实参位置调用列表和字典——最后的两个案例。

被装饰函数的返回值问题:

说完了参数问题,还要考虑返回值问题,比如玩dnf掉落:屠戮之刃:return"掉落:屠戮之刃",在没有guanjia处理的时候应该这样操作函数,接收返回值:

def play_dnf(username, password):
    print("打开dnf,输入账号密码。", username, password)
    print("开始玩dnf游戏!")
    return"掉落:屠戮之刃"

ret=play_dnf("admin", "123465")
print(ret)

这里我们可以分析到,return是需要在guanjiagame()中运行后获得的返回值,为此我们应该给game()做一个ret接收,并且在innerreturn ret

def guanjia(game):
    def inner(*args, **kwargs):
        print("打开外挂")
        game(*args, **kwargs)    # =>应该在这里接收返回值
        print('关闭外挂')
    return inner

因此,整个代码应该

def guanjia(game):
    def inner(*args, **kwargs):
        print("打开外挂")
        ret = game(*args, **kwargs)  # 这里是目标函数的执行, 这里是能够从目标函数拿到返回值的.
        print('关闭外挂')
        return ret    #在这里将返回值返回全局(局部变量)
    return inner

@guanjia
def play_dnf(username, password):
    print("打开dnf,输入账号密码。", username, password)
    print("开始玩dnf游戏!")
    return"掉落:屠戮之刃"

ret = play_dnf("admin", "123465")    #这里虽然也叫ret,但是是全局变量,不要混淆
print(ret)

至此,整个装饰器的推导过程完成了。

通用装饰器的写法:

def wrapper(fn):   wrapper: 装饰器, fn: 目标函数
    def inner(*args, **kwargs):
        pass # 在目标函数执行之前.....
        ret = fn(*args, **kwargs)   # 执行目标函数
        pass # 在目标函数执行之后.....
        return ret
    return inner     # 千万别加()

@wrapper
def target():
    pass

ret = target()  #  在外层,执行内层函数 =>inner()

一个函数可以被多个装饰器装饰:

@wrapper1
@wrapper2
def target():
    print('我是目标')

规则和规律:wrapper1进入——wrapper2进入——target()——wrapper2出去——wrapper1出去 

def wrapper1(fn):  # fn: wrapper2.inner
    def inner(*args, **kwargs):    # 1
        print("这里是wrapper1 进入")  # 2
        ret = fn(*args, **kwargs)  # 3     => wrapper2.inner
        print("这里是wrapper1 出去")  # 11
        return ret    # 12
    return inner    # 13

def wrapper2(fn):  # fn: target
    def inner(*args, **kwargs):    # 4
        print("这里是wrapper2 进入")  # 5
        ret = fn(*args, **kwargs)  # 6 => taget 
        print("这里是wrapper2 出去")  # 8
        return ret    # 9
    return inner    # 10

@wrapper1  # target = wrapper1(wrapper2.inner)   => target: wrapp1.inner
@wrapper2  # target = wrapper2(target)   => target: wrapper2.inner
def target():
    print('目标函数')  # 7

target()    # 0 => 从这里开始

#运行结果
这里是wrapper1 进入    #套壳顺序4    #输出顺序1
这里是wrapper2 进入    #套壳顺序2    #输出顺序2
目标函数               #套壳顺序1    #输出顺序3
这里是wrapper2 出去    #套壳顺序3    #输出顺序4
这里是wrapper1 出去    #套壳顺序5    #输出顺序5

这里可以看到,目标函数在结构上的规律是:

  1. 最中间是目标函数
  2. 先被离着最近的@wrapper2上下套壳
  3. 再被上层的@wrapper1上下套壳
  4. 套壳顺序和输出顺序是不一样的

*大家自行掌握这个规律即可,不用关心我做的序号。这里只是为了方便展示,我自己做了个顺序标记。

装饰器也可以传参

装饰器的实战操作

登录状态判断:login_flag

账号密码判断:while 1break

login_flag = False

def login_verify(fn):
    def inner(*args, **kwargs):
        global login_flag
        if login_flag == False:  # 关键判断位置*****
            # 这里完成登录校验
            print('还未完成用户登录操作')
            while 1:             # 无脑循环登录
                username = input("输入你的账号")
                password = input("输入你的密码")
                if username == "admin" and password == "123":
                    print("登录成功")
                    login_flag = True
                    break        #成功后退出循环
                else:
                    print("登录失败, 用户名或密码错误")
        ret = fn(*args, **kwargs)  # 后续程序的执行
        return ret
    return inner

@login_verify
def add():
    print("添加员工信息")

@login_verify
def delete():
    print("删除信息")

@login_verify
def upd():
    print("修改信息")

@login_verify
def search():
    print("查询员工信息")

add()
upd()
delete()
search()
posted @   Magiclala  阅读(96)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示