Python【进阶】闭包和装饰器

一、闭包

1.什么是闭包?

  • 在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。
  • 闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。
  • 在给定函数被多次调用的过程中,这些私有变量能够保持其持久性

2.形成闭包的三个条件(缺一不可)

  • 必须有一个内嵌函数(函数里定义的函数)——这对应函数之间的嵌套
  • 内嵌函数必须引用一个定义在闭合范围内(外部函数里)的变量——内部函数引用外部变量
  • 外部函数必须返回内嵌函数——必须返回那个内部函数

换句话说:

  • 其实,闭包的概念很简单:一个可以引用在函数闭合范围内变量的函数。即"内部函数",只有那个内部函 数才有所谓的__closure__属性。

3.闭包的原理

  • 形成闭包之后,闭包函数会获得一个非空的__closure__属性(对比我们最后的函数test,test是一 个不具备闭包的函数,它的__closure__属性是None),这个属性是一个元组

  • 元组里面的对象为cell对象,而访问cell对象的cell_contents属性则可以得到闭包变量的当前值 (即上一次调用之后的值)。

  • 而随着闭包的继续调用,变量会再次更新

  • 所以可见,一旦形成闭包之后,python确实会将__closure__和闭包函数绑定作为储存闭包变量的场所

4.闭包的好处

  • 闭包不是必须的。
  • 没了闭包,python的功能一点不会被影响
  • 有了闭包,只是提供给你一种额外的解决方案。

二、装饰器

1. 什么是装饰器

  • 器 : 就是工具

  • 装饰 : 就是添加新功能

函数也是对象,也可以当做参数传递

'总结一句话来说' : 就是定义一个函数, 用该函数去为其他函数添加新功能

2. 装饰器有什么用

  • 你可以考虑在装饰器中置入通用功能的代码来降低程序复杂度。例如,可以用装饰器来:
  • 引入日志
  • 增加计时逻辑来检测性能
  • 给函数加入事务的能力
  • 权限控制

3.为何要使用装饰器

1.开放封闭原则

  • 针对上线的功能对拓展是开放的
  • 但是对修改源代码以及调用方式是封闭的

2.在实际应用场景原因

  • 对上线软件的拓展功能, 我们必须提供扩展的可能性
  • 软件的源代码以及调用方式都应该避免被修改, 否则上线出现BUG之后不容易回滚

3.装饰器与被装饰的对象均可以是任意可调用的对象

  • 装饰器----->函数
  • 被装饰的对象----->函数

4.无参装饰器的优化历程 (助于理解)

1.无参装饰器

需求 : 为 index 函数加上计时功能

import time  # time模块
#time.time() 返回的是时间戳,从1970年1月1日凌晨到现在的秒数
def index():
    time.sleep(1)
    print('from index')

index()
  • 版本一 : 直接在函数体内部设置计时
import time

def index():
    start=time.time()     #开始时间
    time.sleep(1)         #睡"1"秒
    print('from index')
    stop=time.time()      #结束时间
    print('run time is %s' %(stop - start))  #运行时间

index()
🦀🦀结果 : 没有修改调用方式,但修改了源代码
  • 版本二 : 在调用函数的阶段进行计算时间
import time

def index():
    time.sleep(1)
    print('from index')

start=time.time()
index()
stop=time.time()
print('run time is %s' %(stop - start))
🦀🦀结果 : 没有修改调用方式,没有修改源代码,但代码冗余
  • 版本三 : 使用闭函数的方式在"wrapper"函数里面调用"index"
import time

def index():
    time.sleep(1)
    print('from index')

def wrapper():
    start=time.time()
    index()
    stop=time.time()
    print('run time is %s' %(stop - start))

wrapper()
🦀🦀结果 : 解决了代码冗余的问题,但把"index"写死了,只能计算"index"的运行时间
  • 版本四 : 将"index"当做参数传入
import time

def index():
    time.sleep(1)
    print('from index')
    
def index2():
    time.sleep(1)
    print('from index2')

def wrapper(f):
    start=time.time()
    f()  # 函数index的内存地址()
    stop=time.time()
    print('run time is %s' %(stop - start))

wrapper(index)   # wrapper(函数index的内存地址)
wrapper(index2)  # wrapper(函数index2的内存地址)
🐳🐳结果 : 在上一个版本的基础上把被装饰的对象写活了,可以装饰任何无参函数,但还是改变了调用方式
  • 版本五 : 优化版本
import time

def index():
    time.sleep(1)
    print('from index')

def outter(f):      # f=函数index的内存地址
    def wrapper():
        start=time.time()
        f()         # 函数index的内存地址()
        stop=time.time()
        print('run time is %s' %(stop - start))
    return wrapper

index=outter(index)  # outter(函数index的内存地址)
index()              #👆偷梁换柱法,让使用者感觉不到变化,之前怎么使用"index",现在还是怎么使用
🐳🐳结果 : 没有改变调用方式,没有改变源代码,还实现了功能

2.无参装饰器的雏形

以上版本对于不需要参数传入的函数使用没有问题, 但对有参数传入函数进行装饰就会报错

  • 基于上面的版本进行进一步的优化
def home(name):
    time.sleep(2)
    print('home page,welecome %s' %name)

def outter(f):          # f=函数home的内存地址
    def wrapper(name):  # 设置形参可进行传值
        start=time.time()
        f(name)         # 函数home的内存地址(name)
        stop=time.time()
        print('run time is %s' %(stop - start))
    return wrapper

home=outter(home)       # outter(函数home的内存地址)
home("egon")            # 偷梁换柱
🐳🐳结果 : 设置形参后可以进行传值,但也就只能对"home"这个函数进行装饰了,写死了!
  • 优化版本 : 可以传入任意的参数, 也可以不传参数, 利用可变长参数(*args)与(**kwargs)来实现
  • 前面函数参数已经对可变长参数(*args)与(**kwargs)进行了详细介绍,可以参考
import time

def index():
    time.sleep(1)
    print('from index')

def home(name):
    time.sleep(2)
    print('home page,welecome %s' %name)
    
def outter(f):                # f=函数home的内存地址
    def wrapper(*args,**kwargs):
        start=time.time()
        f(*args,**kwargs)     # 函数home的内存地址(name)
        stop=time.time()
        print('run time is %s' %(stop - start))
    return wrapper

index=outter(index)            # outter(函数index的内存地址)
index()                        # 偷梁换柱

home=outter(home)              # outter(函数home的内存地址)
home("egon")                   # 偷梁换柱
🐳🐳结果 : 实现了可以装饰无参和有参,并且遵循了开放封闭原则,但是还有一个问题:就是没有返回值!
  • 最终优化版本 : 设置返回值
import time

def index():
    time.sleep(1)
    print('from index')

def home(name):
    time.sleep(2)
    print('home page,welecome %s' %name)
    return 123                #有返回值的

def outter(f):                # f=函数home的内存地址
    def wrapper(*args,**kwargs):
        start=time.time()
        res=f(*args,**kwargs)  # 函数home的内存地址(name),并接收返回值
        stop=time.time()
        print('run time is %s' %(stop - start))
        return res             # 设置返回值
    return wrapper

index=outter(index)
home=outter(home)

res=index()
print(res)  # None

res=home("egon")
print(res)  # 123
🐳🐳结果 : 完美!

5.无参装饰器的最终结果及应用

1.🍭无参装饰器语法糖

🍭最精简的装饰器, 将需要的功能往里面添加就行
def outter(func):
    def wrapper(*args,**kwargs):
        res=func(*args,**kwargs)
        return res
    return wrapper

@outter  #被装饰函数的正上方单独一行
def test():
    pass

2.计算时间示例

🐳"@"的作用就是将"index"的内存地址传递到"timmer"中👉"func=index"
import time
def timmer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()        #开始时间
        res=func(*args,**kwargs)
        stop_time=time.time()         #结束时间
        print(stop_time-start_time)   #使用时间
        return res
    return wrapper

@timmer # index=timmer(index)
def index():
    time.sleep(1)
    print('welcome to index page')
    return 122

@timmer # home=timmer(home)
def home(name):
    time.sleep(2)
    print('welcome %s to home page' %name)

index()       # 调用该怎么调还是怎么调
home('egon')  # 调用该怎么调还是怎么调

3.认证用户示例

🐳实现的功能就是用户登入了才能使用"index"或者"home"
import time
current_user={
   'username':None, #判断用户是否已经登入,有值就是已登入,"None"就是未登入
    }

def auth(func):
    def wrapper(*args,**kwargs):
        if current_user['username']:        # 这里开始判断了
            print('已经登陆过了')
            res=func(*args,**kwargs)        #如果已经登入就直接运行被装饰的函数
            return res

        uname=input('用户名>>: ').strip()    # 如果未登入则输密码
        pwd=input('密码>>: ').strip()
        if uname == 'egon' and pwd == '123':
            print('登陆成功')
            current_user['username']=uname
            res=func(*args,**kwargs)         
            return res
        else:
            print('用户名或密码错误')
    return wrapper

@auth  #index=auth(index)
def index():
    time.sleep(1)
    print('welcome to index page')
    return 122

@auth  #index=auth(index)
def home(name):
    time.sleep(2)
    print('welcome %s to home page' %name)

index()
home('egon')

4.多个装饰器组合使用示例

  • 多个装饰器组合使用时, 加载顺序是自下而上, 而执行顺序是自上而下的(下面第七大段会详细解析)
🐳实现用户认证,还可以计算程序运行的时间
##############1.用户认证装饰器##################
import time
dic = {
    'username': None,
}

def login(func):
    def wrapper(*args, **kwargs):
        if dic['username']:
            print('已登入')
            res = func(*args, **kwargs)
            return res

        name = input('姓名>>:').strip()
        pwd = input('密码>>:').strip()
        if name == 'song' and pwd == '123':
            print('登入成功')
            dic['username'] = name
            res = func(*args, **kwargs)
            return res
        else:
            print('用户名或密码错误')
    return wrapper

###############2.计算时间装饰器###################
def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('时间%s'%(stop_time-start_time))
        return res
    return wrapper

################3.开始装饰###################

🐳有前后顺序,这里计算 login()+index() 的时间(也就是加上了认证的时间)
@timer    # timer在前
@login
def index():
    time.sleep(1)
    print('欢迎宋海星')
    return 125

🐳只计算程序的运行时间(正确顺序)
@login
@timer    #这里只计算 home() 的时间
def home(name):
    time.sleep(2)
    print('欢迎%s' %name)

index()
home('hai')

6.有参装饰器的实现

1.🍭有参装饰器语法糖

def outter2(x):
    def outter(func):
        def wrapper(*args,**kwargs):
            res=func(*args,**kwargs)
            return res
        return wrapper
    return outter


@outter2(11)  # 被装饰函数的正上方单独一行
def hhh():
    pass

2.有参装饰器示例

  • 需求 : 如果我们的装饰器也需要参数传入呢? 这该如何解决
🐳观察下面,装饰器函数内部需要传入一个值"xxx",从哪里来?
def outter(func):
    def wrapper(*args,**kwargs):
        if xxx == "login":
            res=func(*args,**kwargs)
            return res
        elif xxx == "login2":
            print("22222")
        elif xxx == "login3":
            print("33333")
    return wrapper
  • 很简单! 只需要在外面再包一层就行了
🐳简单语法
def auth(xxx):  #再包一层
    def outter(func):
        def wrapper(*args,**kwargs):
            if xxx == "login":
                res=func(*args,**kwargs)
                return res
            elif xxx == "login2":
                print("22222")
            elif xxx == "login3":
                print("33333")
        return wrapper
    return outter  #同时也要有返回值
  • 示例实现
🐳通过不同的选择进行不同的身份认证(比如商家认证,买家认证等等)
import time
dic = {
    'username': None,
}

def auth(engine):
    def auth2(func):
        def wrapper(*args, **kwargs):
            if engine == 'login1':           #如果传入的是"login1",就进入这种身份的认证
                if dic['username']:
                    print('已经登陆过了')
                    res = func(*args, **kwargs)
                    return res

                name = input('用户名>>: ').strip()
                pwd = input('密码>>: ').strip()
                if name == 'egon' and pwd == '123':
                    print('登陆成功')
                    dic['username'] = name
                    res = func(*args, **kwargs)
                    return res
                else:
                    print('用户名或密码错误')


            elif engine == 'login2':       #"login2"这种模式的身份认证
                print('xx认证')
            elif engine == 'login3':       #"login3"
                print('xxxxxx认证')
        return wrapper
    return auth2

@auth('login3')         #以"login3"模式的身份认证来使用"index"
def index():
    time.sleep(1)
    print('欢迎登入')
    return 123
    
@auth('login1')         #以"login1"模式的身份认证来使用"home"
def home(song):
    time.sleep(2)
    print('啧啧啧%s' % song)

index()
home('哈哈')

7.wraps 装饰器

1.wraps 有什么作用

  • 被装饰的函数 的一些属性值赋值给 装饰器函数(wrapper) ,最终让属性的显示更符合我们想看到的 (装的更像)

2.先看未添加 wraps 装饰器时的属性

  • help() : 既查看函数注释文档有查看函数名
  • .name : 查看函数名
  • .doc : 查看函数的注释文档
🐣猜猜打印的是什么情况?
import time
def timmer(func):
    def wrapper(*args,**kwargs):
        "我是wrapper函数注释信息"  # 这里添加了wrapper的注释信息
        start = time.time()
        res = func(*args,**kwargs)
        stop = time.time()
        print(f"运行时间{stop-start}秒")
        return res
    return wrapper

@timmer
def hhh():
    "hhh函数注释信息"     ## 这里添加了hhh的注释信息
    print("欢迎欢迎")

print(hhh.__name__)
# 返回的是 wrapper 的函数名 "wrapper"
print(hhh.__doc__)
# 返回额是 wrapper 的注释信息 "我是wrapper函数注释信息"
print(help(hhh))
'''依然是 wrapper 的注释和名字
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)
    我是wrapper函数注释信息

None
'''
# 这不值得奇怪,因为装饰器的底层原理就是返回了 wrapper 函数,只不过是将其改了下函数名

3.添加 wraps 装饰器

  • 通过添加 wraps 装饰器将原函数 hhh 的属性传给 wrapper 函数
🐣注意与上面示例做对比
import time
from functools import wraps  #导入模块
def timmer(func):
    @wraps(func)  #在 wrapper 的正上方添加wraps装饰器,把原有函数的属性传给wrapper
    def wrapper(*args,**kwargs):
        "我是wrapper函数注释信息"
        start = time.time()
        res = func(*args,**kwargs)
        stop = time.time()
        print(f"运行时间{stop-start}秒")
        return res
    return wrapper

@timmer
def hhh():
    "hhh函数注释信息"
    print("欢迎欢迎")

print(hhh.__name__)
# hhh
print(hhh.__doc__)
# hhh函数注释信息
print(help(hhh))
'''
Help on function hhh in module __main__:

hhh()
    hhh函数注释信息

None
'''
# 可以发现属性已经发生了传递,装的更像了👻

8.叠加多个装饰器的底层原理分析

  • 叠加多个装饰器的加载顺序是自下而上的(了解)
  • 叠加多个装饰器的执行顺序是自上而下的
def outter1(func1):  # func1 = wrapper2的内存地址
    print('============>outter1')
    def wrapper1(*args,**kwargs):
        print('============>wrapper1')
        res1=func1(*args,**kwargs)
        return res1
    return wrapper1

def outter2(func2):  # func2 = wrapper3的内存地址
    print('============>outter2')
    def wrapper2(*args,**kwargs):
        print('============>wrapper2')
        res2=func2(*args,**kwargs)
        return res2
    return wrapper2

def outter3(func3):  # func3 = 被装饰函数也就是index的内存地址
    print('============>outter3')
    def wrapper3(*args,**kwargs):
        print('============>wrapper3')
        res3=func3(*args,**kwargs)
        return res3
    return wrapper3

🍔加载顺序自下而上,先是"outter3"开始将"index"传入自己内部,然后"outter2"把"outter3"的返回值传入自己...
@outter1  # outter1(wrapper2)---->wrapper1的内存地址 (index=wrapper1)
@outter2  # outter2(wrapper3)---->wrapper2的内存地址 (index=wrapper2)
@outter3  # outter3(index)------->wrapper3的内存地址 (index=wrapper3)
def index():
    print('from index')
#只加载不执行输出结果
'''
============>outter3
============>outter2
============>outter1
'''
#加载执行
index()
'''
============>outter3
============>outter2
============>outter1
============>wrapper1
============>wrapper2
============>wrapper3
from index
'''
posted @ 2022-06-09 17:21  小涛lct  阅读(78)  评论(0编辑  收藏  举报