Python(五)—— 闭包&装饰器

发布时间:2023年12月26日

13. 闭包

13.1 闭包的案例

给大家提个需求,然后用函数去实现:完成一个计算不断增加的系列值的平均值的需求

例如:整个历史中的某个商品的平均收盘价。就是从这个商品一出现开始,每天记录当天价格,然后计算他的平均值,平均值要考虑直至目前为止所有的价格:
第一天价格为:100元,平均收盘价:100 元
第二天价格为:110元,平均收盘价:(100+110) /2 元
第三天价格为:120元,平均收盘价:(100+110+120) /3 元
........

series = []
def make_averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100))    # 100.0
print(make_averager(110))    # 105.0
print(make_averager(120))    # 110.0

从上面的例子可以看出,基本上完成了我们的要求,但是这个代码相对来说是不安全的,因为你的这个series列表是一个全局变量,只要是全局作用域的任何地方,都可能对这个列表进行改变

series = []
def make_averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100))   # 100.0
print(make_averager(110))   # 105.0
series.append(50)           # 如果对数据进行改变,那么平均收盘价就会出现问题
print(make_averager(120))   # 95.0

那有人说了,你把他放在函数中不就行了,这样不就是局部变量了么?数据不就相对安全了么

def make_averager(new_value):
    series = []
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100))    # 100.0
print(make_averager(110))    # 110.0
print(make_averager(120))    # 120.0

这样计算的结果是不正确的,那是因为执行函数,会开启一个临时的名称空间,随着函数的结束而消失,所以你每次执行函数的时候,都是重新创建这个列表,那么这种情况下,就需要用到闭包了

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

avg = make_averager()

print(avg(100))     # 100.0
print(avg(110))     # 105.0
print(avg(120))     # 110.0


"""
注释说明:
在函数中嵌套了一个函数。那么avg这个变量接收的实际是averager函数名,
也就是其对应的内存地址,我执行了三次avg也就是执行了三次averager这个函数;
series变量叫自由变量,averager函数的作用域会延伸到包含自由变量series的绑定。
也就是说,每次我调用avg对应的averager函数时,都可以引用到这个自用变量series,
这个就是闭包
"""

13.2 闭包的过程演示

① 创建了一个make_averager()函数、一个series = []空列表、一个averager(new_value)函数

② 因为avg=make_averager(),因此执行avg(100),相当于执行了make_averager()函数,从而执行averager(new_value)函数,并将值100传给了new_value,添加进series = []空列表,相当于此时series = [100],此时的total=100,列表的len为1,并返回total/len(series),即100/1=100,得出结果为100.0

③ 同理,此时再执行avg(110),将值110传给了new_value,添加进series = [100]这个列表,相当于此时series = [100, 110],此时的total=210,列表的len为2,并返回total/len(series),即210/2=105,得出结果为105.0

④ 同理,此时再执行avg(120),将值120传给了new_value,添加进series = [100, 110]这个列表,相当于此时series = [100, 110, 120],此时的total=330,列表的len为3,并返回total/len(series),即330/3=110,得出结果为110.0

13.3?闭包的定义

  • 1、闭包是嵌套在函数中的函数

  • 2、闭包必须是内层函数对外层函数的变量(非全局变量)的引用

13.4?闭包的作用

  • 保存局部信息不被销毁,保证数据的安全性

13.5?闭包的应用

  • 1、可以保存一些非全局变量但是不易被销毁、改变的数据

  • 2、装饰器

13.6 闭包的判断

"""----- 例1 ------"""

def wrapper():          # 定义了外层函数
    a = 1               # a为外层函数的变量,且非全局变量
    def inner():        # 定义了内层函数
        print(a)        # 调用了a这个变量
    return inner
ret = wrapper()         # 外层函数进行引用

# 因此:该例是闭包的应用
"""----- 例2 ------"""

a = 2                # a在外层函数外,为全局变量
def wrapper():
    def inner():
        print(a)
    return inner
ret = wrapper()

# 因此:该例非闭包的应用
"""----- 例3 ------"""

def wrapper(a,b):
    def inner():
        print(a)
        print(b)
    return inner
a = 2
b = 3
ret = wrapper(a,b)

# 因此:该例是闭包的应用

13.6.1 通过函数属性判断

"""
注释说明:
函数名.__code__.co_freevars 查看函数的自由变量
如果输出结果为:('series',) 说明series是自由变量
"""

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager
avg = make_averager()

print(avg.__code__.co_freevars)

# 输出结果:
('series',)

其他参数,仅供了解: ?

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager
avg = make_averager()


# 1、函数名.__code__.co_varnames 查看函数的局部变量
print(avg.__code__.co_varnames)
# 输出结果:('new_value', 'total')

# 2、函数名.__closure__ 获取具体的自由变量对象,也就是cell对象
print(avg.__closure__)
# 输出结果:(<cell at 0x104b59930: list object at 0x104b7ddc0>,)

# 3、cell_contents 自由变量具体的值
print(avg.__closure__[0].cell_contents)
# 输出结果:[]

14. 装饰器

14.1 开放封闭原则

开放封闭原则具体具体定义是这样:

  • 1、对扩展是开放的

    我们说,任何一个程序,不可能在设计之初就已经想好了所有的功能并且未来不做任何更新和修改。所以我们必须允许代码扩展、添加新功能

  • 2、对修改是封闭的

    就像我们刚刚提到的,因为我们写的一个函数,很有可能已经交付给其他人使用了,如果这个时候我们对函数内部进行修改,或者修改了函数的调用方式,很有可能影响其他已经在使用该函数的用户

14.2 初识装饰器

在不改变原被装饰的函数的源代码以及调用方式下,为其添加额外的功能

举例:你现在公司的开发部分任职,领导给你一个业务需求让你完成:让你写代码测试你写的函数的执行效率

def index():
    print('欢迎访问雨落主页')

版本1:

需求分析:你要想测试此函数的执行效率,应该在此函数执行前记录一个时间, 执行完毕之后记录一个时间,这个时间差就是具体此函数的执行效率。那么执行时间如何获取,可以利用time模块,有一个time.time() 功能

"""
此方法返回的是格林尼治时间,即此刻距离1970年1月1日0点0分0秒的时间秒数,也叫时间戳,
所以要是计算函数的执行效率就是在执行前后计算这个时间戳的时间,然后求差值即可
"""

import time
print(time.time())

# 输出结果
1703569318.8058171

由于index函数只有一行代码,执行效率太快了,所以我们利用time模块的一个sleep模拟一下,默认让他程序睡眠2秒之后再执行

import time
def index():
    time.sleep(2)      # 睡眠2秒,模拟一下网络延迟以及代码的效率
    print('欢迎访问雨落主页')

start_time = time.time()
index()
end_time = time.time()
print(f'此函数的执行效率为{end_time-start_time}')

# 输出结果:
欢迎访问雨落主页
此函数的执行效率为2.0052030086517334

版本1分析:你现在已经完成了这个需求,但是问题是虽然你只写了四行代码,但是你完成的是一个测试其他函数的执行效率的功能,如果让你测试一下庆言的函数效率呢? 你是不是全得复制一遍,例如:

import time
def index1():
    time.sleep(2)   # 模拟一下网络延迟以及代码的效率
    print('欢迎访问雨落首页')

def index2(name):
    time.sleep(3)   # 模拟一下网络延迟以及代码的效率
    print(f'欢迎访问{name}主页')

start_time = time.time()
index1()
end_time = time.time()
print(f'此函数的执行效率为{end_time-start_time}')

start_time = time.time()
index2('庆言')
end_time = time.time()
print(f'此函数的执行效率为{end_time-start_time}')

# 输出结果:
欢迎访问雨落首页
此函数的执行效率为2.0052318572998047
欢迎访问庆言主页
此函数的执行效率为3.002941131591797

重复代码太多了,所以要想解决重复代码的问题,可以利用一下函数,函数就是以功能为导向,减少重复代码,我们继续完善代码

版本2:

import time

def index():
    time.sleep(2)   # 模拟一下网络延迟以及代码的效率
    print('欢迎访问雨落主页')

def inner():
    start_time = time.time()
    index()
    end_time = time.time()
    print(f'此函数的执行效率为{end_time-start_time}')

inner()

# 输出结果:
欢迎访问雨落主页
此函数的执行效率为2.005192279815674

但是你这样写也是有问题的,你虽然将测试功能的代码封装成了一个函数,但是这样,你只能测试你自己的的函数index,你要是测试庆言等其他人的函数呢

import time
def index1():
    time.sleep(2)  # 模拟一下网络延迟以及代码的效率
    print('欢迎访问博客园主页')

def index2(name):
    time.sleep(3)  # 模拟一下网络延迟以及代码的效率
    print(f'欢迎访问{name}主页')

def inner():
    start_time = time.time()
    index1()
    index2('庆言')
    end_time = time.time()
    print(f'此函数的执行效率为{end_time-start_time}')

inner()

# 输出结果:
欢迎访问博客园主页
欢迎访问庆言主页
此函数的执行效率为5.008612871170044

你要是像上面那么做,每次测试其他同事的代码还需要手动改,所以如何变成动态测试其他函数,我们学过函数的传参,能否将被装饰函数的函数名作为函数的参数传递进去呢

版本3:

import time
def index1():
    time.sleep(2)
    print('欢迎访问雨落主页')

def index2(name):
    time.sleep(3)
    print(f'欢迎访问{name}主页')

def timmer(func):   # 第2步、此时func相当于index函数
    start_time = time.time()
    func()          # 第3步、执行func(),相当于执行index()
    end_time = time.time()
    print(f'此函数的执行效率为{end_time-start_time}')

timmer(index1)       # 第1步、执行timmer()函数,里面的index被传给func

# 输出结果:
欢迎访问雨落主页
此函数的执行效率为2.0050861835479736

这样我将index1函数的函数名作为参数传递给timmer函数,然后在timmer函数里面执行index1函数,这样就变成动态传参了。对比着开放封闭原则说: 首先,index1函数除了完成了自己之前的功能,还增加了一个测试执行效率的功能,符合开放原则。 其次,index1函数源码没有改变,但是执行方式改变了,所以不符合封闭原则。 原来如何执行,index1() 现在如何执行,timmer(index1)这样会造成什么问题?假如index1在你的项目中被100处调用,那么这相应的100处调用我都得改成timmer(index),非常麻烦,且不符合开放封闭原则

版本4:实现真正的开放封闭原则:装饰器

import time
def index1():                     # 第8步,执行该程序
    time.sleep(2)
    print('欢迎访问雨落主页')        # 第9步,打印

def index2(name):
    time.sleep(3)
    print(f'欢迎访问{name}主页')

def timer(func):                  # 第2步,执行该程序,此时 func = index1
    def inner():                  # 第5步,执行该程序
        start_time = time.time()  # 第6步,记录开始时间
        func()                    # 第7步,执行index1()
        end_time = time.time()    # 第10步,记录结束时间
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner                  # 第3步,将inner()返回给函数调用者f

f = timer(index1)                 # 第1步,程序到此,执行timer函数
f()                               # 第4步,执行f(),也就是inner()

# 输出结果:
欢迎访问雨落主页
此函数的执行效率为2.005129098892212

将上面的inner函数在套一层最外面的函数timer,然后将里面的inner函数名作为最外面的函数的返回值,这样简单的装饰器就写好了

代码执行到这一行:f = timer(index) 先执行 timer(index) 执行timer函数,将index函数名传给了func形参。内层函数inner不执行,inner函数返回给f变量。所以我们执行f() 就相当于执行inner闭包函数

14.3 带返回值的装饰器

现在这个代码,完成了最初版的装饰器,但是还是不够完善,因为你被装饰的函数index可能会有返回值,如果有返回值,你的装饰器也应该不影响,但是现在:

import time
def index():
    time.sleep(2)
    print('欢迎访问雨落主页')
    return '访问成功'

def timer(func):
    def inner():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

index = timer(index)
print(index())

# 输出结果:
欢迎访问雨落主页
此函数的执行效率为2.005070924758911
None

加上装饰器之后,他的返回值为None,这是因为现在的index不是函数名index,这index实际是inner函数名。所以index() 等同于inner() 你的 '访问成功'返回值应该返回给谁,应该返回给index,这样才做到开放封闭,实际返回给了func,所以你要更改一下你的装饰器代码,让其返回给外面的index函数名,所以:你应该这么做:

import time
def index():
    time.sleep(2)
    print('欢迎访问雨落主页')
    return '访问成功'

def timer(func):
    def inner():
        start_time = time.time()
        ret = func()
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
        return ret
    return inner

index = timer(index)
print(index())          # 等同于print(inner())

# 输出结果:
欢迎访问雨落主页
此函数的执行效率为2.0050370693206787
访问成功

借助于内层函数inner,你将func的返回值,返回给了inner函数的调用者也就是函数外面的index,这样就实现了开放封闭原则,index返回值,确实返回给了'index'

14.4 被装饰函数带参数的装饰器

到目前为止,你的被装饰函数还是没有传参呢?按照我们的开放封闭原则,加不加装饰器都不能影响你被装饰函数的使用

import time
def index1():
    time.sleep(2)
    print('欢迎访问雨落主页')
    return '访问成功'

def index2(name):
    time.sleep(3)
    print(f'欢迎访问{name}主页')

def timer(func):
    def inner():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

# 要想timer装饰index2函数怎么做?
index2 = timer(index2)
index2('庆言')

# 输出结果:
报错

上面那么做,显然报错了,为什么? 你的index2这个变量是谁?是inner,index2('庆言')实际是inner('庆言'),但是你的'庆言'这个实参应该传给index2函数,实际却传给了inner,所以我们要通过更改装饰器的代码,让其将实参'庆言'传给index2

import time
def index1():
    time.sleep(2)
    print('欢迎访问雨落主页')
    return '访问成功'

def index2(name):
    time.sleep(3)
    print(f'欢迎访问{name}主页')

def timer(func):             # func = index2
    def inner(name):
        start_time = time.time()
        func(name)           # index2(name) == index2('庆言')
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

# 要想timer装饰index2函数怎么做?
index2 = timer(index2)
index2('庆言')

# 输出结果:
欢迎访问庆言主页
此函数的执行效率为3.005120038986206

这样就实现了,不过现在被装饰函数的形参只是有一个形参,如果要是多个,且可以接受不定数参数的形参接受他们,这样就要想到*args,**kwargs

import time
def index1():
    time.sleep(2)
    print('欢迎访问博客园主页')
    return '访问成功'

def index2(name,age):
    time.sleep(3)
    print(name,age)
    print(f'欢迎访问{name}主页')

def timer(func):                   # func = index2
    def inner(*args,**kwargs):     # 函数定义时,*代表聚合
        start_time = time.time()
        func(*args,**kwargs)       # 函数的执行时,*代表打散
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

index2 = timer(index2)
index2('庆言',18)

# 输出结果:
庆言 18
欢迎访问庆言主页
此函数的执行效率为3.005236864089966

14.5 标准版装饰器

刚刚我们已经知道一个装饰器是可以这样写的:

def timer(func):                   # func = index2
    def inner(*args,**kwargs):     # 函数定义时,*代表聚合
        start_time = time.time()
        func(*args,**kwargs)       # 函数的执行时,*代表打散
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

index2 = timer(index2)
index2('庆言',18)

每次执行index2之前你要写上一句:index2 = timer(index2),这样是比较麻烦的,Python给我们提供了一个简化机制,用一个很简单的符号去代替这一句话:

--------- 装饰器在上 ---------

def timer(func):      # func = home
    def inner(*args,**kwargs):
        start_time = time.time()
        func(*args,**kwargs)
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner


--------- 函数在下 ---------

@timer                # 作用等同于index2 = timer(index2)
def index2(name,age):
    time.sleep(3)
    print(name,age)
    print(f'欢迎访问{name}主页')

index2('庆言',18)

至此标准版的装饰器就是这个样子:

def wrapper(func):
    def inner(*args,**kwargs):
        '''执行被装饰函数之前的操作'''
        ret = func
        '''执行被装饰函数之后的操作'''
        return ret
    return inner

14.6 带参数的装饰器

装饰器其实就是一个闭包函数,再说简单点就是两层的函数。那么是函数,就应该具有函数传参功能

login_status = {
    'username': None,
    'status': False,}

def auth(func):
    def inner(*args,**kwargs):
        if login_status['status']:
            ret = func()
            return ret
        username = input('请输入用户名:').strip()
        password = input('请输入密码:').strip()
        if username == '雨落' and password == '123':
            login_status['status'] = True
            ret = func()
            return ret
    return inner

上面的装饰器,不要打开,他可以不可以再套一层

login_status = {
    'username': None,
    'status': False,}

def auth(x):
    def auth2(func):
        def inner(*args,**kwargs):
            if login_status['status']:
                ret = func()
                return ret
            username = input('请输入用户名:').strip()
            password = input('请输入密码:').strip()
            if username == '雨落' and password == '123':
                login_status['status'] = True
                ret = func()
                return ret
        return inner
    return auth

举例说明:雨落:绑定的是微信账号密码,庆言:绑定的是qq的账号密码;现在要完成的就是该装饰器要分情况去判断账号和密码,不同的函数用的账号和密码来源不同。但是你之前写的装饰器只能接受一个参数就是函数名,所以你写一个可以接受参数的装饰器

login_status = {
    'username': None,
    'status': False, }
def auth2(func):
    def inner(*args, **kwargs):
        if login_status['status']:
            ret = func()
            return ret
        if 'WeChat':
            username = input('请输入用户名:').strip()
            password = input('请输入密码:').strip()
            if username == 'yuluo' and password == '123':
                login_status['status'] = True
                ret = func()
                return ret
        elif 'QQ':
            username = input('请输入用户名:').strip()
            password = input('请输入密码:').strip()
            if username == 'qingyan' and password == '123':
                login_status['status'] = True
                ret = func()
                return ret
    return inner

@auth2
def YL():
    print('欢迎来到雨落的网站')

@auth2
def QY():
    print('欢迎来到庆言的网站')

解决方式如下:

login_status = {
    'username': None,
    'status': False, }

def auth(x):
    def auth2(func):
        def inner(*args, **kwargs):
            if login_status['status']:
                ret = func()
                return ret
            
            if x == 'WeChat':
                username = input('请输入用户名:').strip()
                password = input('请输入密码:').strip()
                if username == 'yuluo' and password == '123':
                    login_status['status'] = True
                    ret = func()
                    return ret
            elif x == 'QQ':
                username = input('请输入用户名:').strip()
                password = input('请输入密码:').strip()
                if username == 'qingyan' and password == '123':
                    login_status['status'] = True
                    ret = func()
                    return ret
        return inner
    return auth2

@auth('WeChat')
def YL():
    print('欢迎来到雨落的网站')

@auth('QQ')
def QY():
    print('欢迎来到庆言的网站')

注释:@auth('WeChat')分两步:

  • 第一步先执行@auth('WeChat')函数,得到返回值auth2
  • 第二步@与auth2结合,形成装饰器@auth2然后在依次执行
文章来源:https://blog.csdn.net/GDYuLuo/article/details/135218209
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。