函数之闭包函数、装饰器

函数之闭包函数、装饰器

在说明闭包函数和装饰器之前,我们先补充一下,函数名要怎么用。

函数名的多种用法

函数名做变量名

  1. 函数名有等同变量名的功能

    def func():
        print('from func')
    
    print(func)  # <function func at 0x000002558C1471F0>
    '''
    后面是存放这个函数代码的内层地址
    其实函数名就可以看做变量名,只是它绑定的数据地址是一段函数体代码
    '''
    res = func  # 将变量的绑定传递给另一个变量
    res()  # res也和函数名一样,可以加括号调用函数功能了
    

    所以本质上,函数名也是变量名的一种。

  2. 函数名可以做实参传给函数

    既然函数名是变量名,那么函数自然可以像变量一样作为参数传入函数了

    def func():
        print('from func')
        
    def index(func_name):
        print('go to index')
        func_name()
        print('from index')
        
    index(func)
    
    """运行结果
    go to index
    from func
    from index
    """
    

    从运行结果可以清晰的看到,func的代码是在index函数体代码执行的,
    说明真的将函数名通过参数的方式传入了函数中

  3. 函数名可以做返回值被输出

    def func1():
        def func2():
            print('from func2')
        return func2
    
    func_name = func1()  # func1运行结束返回了func2的函数名
    func_name()  # from func2
    

    在这里函数名被作为返回值返回出来,仔细分析,我们实际上做到了:

    在全局中调用了func1局部名称空间中的函数名func2

    这一点,在后续的闭包函数和装饰器中有很好的应用,这里先埋个伏笔。

函数名存放于容器类型之中(如字典)

结合函数名是本质是变量名的特点,我们可以对分支结构做很好的优化。

当一个py文件有很多的分支,每个分支的功能相对独立,可以被封装为一个个小函数,我们之前是这么操作的:

def func1():
    print('func1')
def func2():
    print('func2')
def func3():
    print('func3')
def func4():
    print('func4')

    
# 之前的分支结构,用if-elif来分支
print('1:注册|2:登录|3:删除|4:查看')
choice = input('输入功能对应数字:')
if choice == '1':
    func1()
elif choice == '2':
    func2()
elif choice == '3':
    func3()
elif choice == '4':
    func4()
else:
    print('没有相关功能')

很显然,这样的分支结构不仅写起来繁琐,而且扩展起来也十分的麻烦,每扩展一次,elif就要再写一次,加入新的功能。

所以我们将功能函数作为变量存储到字典中,就可以大幅减少分支结构带给我们的麻烦:

func_dict = {
    '1': func1,
    '2': func2,
    '3': func3,
    '4': func4,
}  # 代号与函数的对应关系
print('1:注册|2:登录|3:删除|4:查看')
choice = input('输入功能对应数字:')
# 分支结构变成了搜索字典
if choice in func_dict:
    func_dict.get(choice)()  # 从字典中取到函数名套上括号就可以使用了
else:
    print('没有相应的功能代号')

闭包函数

闭包函数是指:

  • 定义在函数体内部的函数
  • 内部的函数用到了外部函数的变量
  • 满足上述描述的函数,内层函数叫闭函数,外层函数叫包函数
def outer():  # 外层函数
    x = 100
    def inner():  # 内层函数
        print(x)  # 内层函数用到了外层函数的变量

闭包函数的意义在于它是第二种对函数进行传参的方式,且这种传参方式有其独特的优点。

# 原本的传参方式
def func(wifi_name, wifi_pwd):
    print(f'wifi名称是:{wifi_name}|wifi密码是{wifi_pwd}')


func('leethon的WiFi', 123)
func('leethon的WiFi', 123)
func('leethon的WiFi', 123)
func('lalisa的WiFi', 321)
# 每次调用,我们都需要往函数传实参。

# 闭包函数传参方式
def func(wifi_name, wifi_pwd):
    def inner():
        print(f'wifi名称是:{wifi_name}|wifi密码是{wifi_pwd}')
    return inner

lee_wifi = func('leethon的WiFi', 123)
lee_wifi()
lee_wifi()
lee_wifi()
lisa_wifi = func('lalisa的WiFi', 321)
lisa_wifi()
lee
# 只有第一次调用,我们需要传参数,在参数不变的情况下,无需再次传参

像当我们再调用函数时拥有一套常见的配置作为参数传入,用闭包函数的方法就会很方便。

装饰器理论推导

装饰器能在调用者不改变调用方式和不改动原函数体代码的情况下,给函数添加功能,他满足:

  • 不改变原函数的调用方式
  • 不改动原函数的函数体代码
  • 新功能独立于它装饰的函数,新功能可以装饰多个函数

以下面的需求为例,我们来盘一盘怎么才能得到一个装饰器

import time  # 导入时间模块,用这个模块可以用一些时间相关的功能

def index():
    print('index in')
    time.sleep(2)  # 程序等待2秒
    print('index out')

index()
# index原本的功能是in 等两秒 然后out

给这个函数增加功能,统计这个函数的运行时间。

装饰器雏形

  1. 在函数体代码外直接添加代码

    start = time.time()  # 时间戳,是一个绝对的时间概念
    index()
    end = time.time()
    print(f'程序运行的时间是{end - start}')  # 结束时间 - 开始时间得到了函数运行时间
    

    这样我们就在函数不变的情况下,添加了功能,但是添加的功能必须写在调用内容的前后,每次调用并不方便,我们应该需要函数把这个添加的功能保存下来。

  2. 外层嵌套函数封装原功能和新功能

    def outer():
        start = time.time()  
        index()
        end = time.time()
        print(f'程序运行的时间是{end - start}') 
        
    outer()  # 只要调用outer就能新旧功能一起用了
    

    这个新添加的功能绑定死给了index,其他的函数要想用这个新功能就得再写一个新函数嵌套。

  3. 传入参数更改函数

    def outer(func):
        start = time.time()  
        func()
        end = time.time()
        print(f'程序运行的时间是{end - start}') 
        
    outer(index)  # 运行了这一句调用新功能和旧功能
    

    我们虽然添加了新的功能,也实现了一定的兼容性,但是改变了调用方式,依旧不满足装饰器的定义。

  4. 第二种传参方式传入函数名

    def outer(func):
        def inner():
            start = time.time()  
        	func()
        	end = time.time()
        	print(f'程序运行的时间是{end - start}') 
        return inner
    
    res = outer(index)  # 通过闭包函数拿到了inner,
    res()  # inner运行既新添了功能,内部被装饰的函数也可以被替换
    '''但是调用方式还是产生了一定的变化,这时破局点就在赋值语句'''
    index = outer(index)  # 将这个添加了新功能的新函数名(被修饰好的函数)赋值给原函数名
    '''我们先在等号右边得到了inner函数名,再赋给左边'''
    index()  # 符合1.不改变原函数|2.不改变调用方式|3.装饰了新的功能
    

    至此,我们已经得到了修饰器的雏形了,但是仍然存在一些兼容性的缺陷,我们需要继续讨论。

装饰器完全体

上一小节,我们提到装饰器雏形有些兼容性的缺陷,是什么呢,该如何解决

  • 装饰器雏形只能装饰无参函数

    # 如果想要以下有参函数也被装饰,用原本的装饰器就不行了
    # 装饰器雏形
    '''
    def outer(func):
        def inner():
            start = time.time()  
        	func()
        	end = time.time()
        	print(f'程序运行的时间是{end - start}') 
        return inner
    '''
    # 想被装饰的有参函数
    def waste_time(n):
        time.sleep(n)  # 提前导入过了time模块,n是几就等待几秒
    
    # 没有被装饰的函数运行
    waste_time(2)  # 等了两秒
    # 装饰语句
    waste_time = outer(waste_time)
    # 再尝试调用装饰好的有参函数
    waste_time(1)  # 报错:没有要求参数,却给了1个参数
    

    也就是说,我们所写的装饰器雏形,只能装饰无参函数。

    想装饰有一个参数的函数就得写成下面的形式。

    def outer(func):
        def inner(a):  # 这里能接收一个位置函数
            start = time.time()  
        	func(a)  # 这里再原封不动还给要装饰的有参函数
        	end = time.time()
        	print(f'程序运行的时间是{end - start}') 
        return inner
    

    同样的,这样写,这个新的装饰器也只能装饰恰好有一个参数的函数罢了。

    那我们的装饰器,对被装饰的函数的参数个数要求太高了。

    为了解决这一问题,我们要用到可变长参数

    def outer(func):
        def inner(*args, **kwargs):  # 这里能接收所有的位置参数和关键字参数
            start = time.time()  
        	func(*args, **kwargs)  # 这里再原封不动还给要装饰的有参函数
        	end = time.time()
        	print(f'程序运行的时间是{end - start}') 
        return inner
    
    # 试一下效果
    def waste_time(n):
        print(n)
        time.sleep(2)
        
    def func(a, b):
        print(a, b)
        time.sleep(1)
        
        
    waste_time(1)  # 执行了原有功能
    func(2, 3)  # 执行了原有功能
    waste_time = outer(waste_time)
    func = outer(func)
    waste_time(1)  # 在打印了1的基础上,统计了时间约等于2秒
    func(2, 3)  # 在打印了2,3的基础上,统计了时间约等于1秒
    

    使用上述装饰器已经解决了参数个数不定的问题,无论有多少参数,以位置实参还是关键字实参的形式传,都可以按照原本函数的传参方式去传,不会影响调用方式。

  • 装饰器雏形不能装饰有返回值的函数

    如果被装饰的函数有返回值,在装饰后会不会发生变化?

    答案是会发生变化,如下

    def outer(func):
        def inner(*args, **kwargs):  # 这里能接收所有的位置参数和关键字参数
            start = time.time()  
        	func(*args, **kwargs)  # 这里再原封不动还给要装饰的有参函数
        	end = time.time()
        	print(f'程序运行的时间是{end - start}') 
        return inner
    
    def func(a,b,c):
        print(a,b,c)
        time.sleep(3)  # 等待三秒
        return a+b+c
    	
    print(func(1,2,3))  # 返回6
    func = outer(func)  # 装饰语句
    print(func(1,2,3))  # 返回None
    

    被装饰后的函数返回了none,和原本不一样,

    也就是说虽然调动的方式没变,也执行了原本的功能,也添加了新的功能,但是返回值变了。

    原因就是,我们本质上是用装饰器中的inner替换了原函数,但是inner是没有返回值的,所以返回了none。所以应该将inner添加return使它有一个返回值,且等于原函数的返回值。

    def outer(func):
        def inner(*args, **kwargs):  # 这里能接收所有的位置参数和关键字参数
            start = time.time()  
        	res = func(*args, **kwargs)  # 接收原函数的返回值
        	end = time.time()
        	print(f'程序运行的时间是{end - start}') 
            return res  # 给inner一个返回值,就是原函数的返回值
        return inner
    
    
    # 试一下
    def func(a,b,c):
        print(a,b,c)
        time.sleep(3)  # 等待三秒
        return a+b+c
    print(func(1,2,3)) # 返回6
    func = outer(func)  # 装饰语句
    print(func(1,2,3))  # 返回6
    

装饰器模板

至此,我们就可以在不改动原函数,不改动调用方式,不改动传参方式,不改动返回值的情况下,给原函数添加新的功能了。

上面的一切理论最终得到的结论就是一个模板:

def outer(func):
    def inner(*args, **kwargs): 
		# print('新添加功能代码前')
    	res = func(*args, **kwargs)  
		# print('新添加功能代码后')
        return res  
    return inner

装饰器语法糖

每次转换的语句func = outer(func)比较麻烦,而且实际写代码时由于位置不固定也十分的乱。

python提供了一个简单语法,来代替这一条语句:

@outer  # 紧贴着要被装饰的函数前,代表func = outer(func)
def func():
	"""被装饰函数"""
    pass

多层语法糖

有时候,我们在加装一个装饰器后,还会继续加装饰器。

那时,我们就会在函数上面贴上好几个装饰器,如以下:

@outer1 # 装饰器1
@outer2 # 装饰器2
@outer3 # 装饰器3
def index():
    print('from index')

形成了一种多层语法糖的结构,在加装了多个语法糖时,实际的执行顺序是先加装最贴近被装饰函数的装饰器,如上述例子中,会先用outer3装饰index,然后用outer2再装饰,最后outer1再装饰。

拓展:多层语法糖的运行过程

来分析以下的例子,我们向index加装了三个装饰器,这些代码的7条打印语句会按什么顺序执行呢?

def outer1(func1):
    print('加载了outter1')

    def wrapper1(*args, **kwargs):
        print('执行了wrapper1')
        res1 = func1(*args, **kwargs)
        return res1

    return wrapper1


def outer2(func2):
    print('加载了outter2')

    def wrapper2(*args, **kwargs):
        print('执行了wrapper2')
        res2 = func2(*args, **kwargs)
        return res2

    return wrapper2


def outer3(func3):
    print('加载了outter3')

    def wrapper3(*args, **kwargs):
        print('执行了wrapper3')
        res3 = func3(*args, **kwargs)
        return res3

    return wrapper3


@outer1  # index = outer1(加载了outer3的index即wrapper2)>>>wrapper1——>index
@outer2  # outer2(加载了outer3的index即wrapper3)>>>wrapper2
@outer3  # outer3(原本的index)>>>wrapper3
def index():
    print('from index')


index()

上述代码执行顺序:

  1. 定义了三个装饰器,只检查装饰器的语法,不执行函数体代码
  2. 定义了index,按照outer3、outer2、outer1的顺序装饰index
    1. index作为实参传入outer3,开始运行outer3代码,打印加载outer3,
    2. 定义wrapper3并返回了wrapper3
    3. wrapper3被outer2装饰,作为实参传入outer2,开始运行outer2代码,打印加载outer2,
    4. 定义wrapper2并返回了wrapper2
    5. wrapper2被outer1装饰,作为实参传入outer1,开始运行outer1,打印加载outer1,
    6. 定义并返回了wrapper1,并将wrapper1赋值给index,index()相当于调用wrapper1()
  3. 执行index(),相当于调用wrapper1()
    1. 开始执行wrapper1,先打印,再执行func1(参数)
    2. func1形参被传入的是wrapper2,相当于调用wrapper2
    3. 开始执行wrapper2,先打印,再执行func2
    4. func2形参被传入的是wrapper3,
    5. 开始执行wrapper3,先打印‘执行wrapper3’,再执行func3
    6. func3形参被传入了index,开始执行index,打印‘from index’
    7. index执行结束,返回wrapper3,wrapper3也随之执行结束,逐级返回,结束了每级的调用。

image

有参装饰器

上文所用到的装饰器,是无参装饰器,而还有一种装饰器叫有参装饰器。

无参装饰器中,新加装的功能代码不能从外界接收参数。

# 无参装饰器模板
def outer(func):
    def inner(*args, **kwargs):
        print(a, b)  # 现在我们想在这个装饰器中传入参数
        res = func(*args, **kwargs)
        return res
    return inner

问题来啦,a和b变量从哪里获取?

image

  1. 位置1:在inner的形参传入

    不行,inner是最终替换被装饰函数的,这样会改变被装饰函数的调用方式。

  2. 位置2:在outer的函数体内定义a,b

    这样的变量定义后就被写死了,不行

  3. 位置3:在outer的形参传入a和b

    理论上可以通过index = outer(index, a, b)来执行,但是这样就没法使用语法糖了,因为@outer只能传入紧贴下方的函数名做形参。

所以我们就要在外层再套一层函数来闭包式传参了:

def outer_plus(a, b):
    def outer(func):
        def inner(*args, **kwargs):
            print(a, b)  # 现在我们想在这个装饰器中传入参数
            res = func(*args, **kwargs)
            return res

        return inner
    return outer

# 这样我们就可以往装饰器里面传入参数了
# 并且可以使用语法糖升级版
@outer_plus(1, 2)
def func(bla, blabla):
    print('from func')

这个有参装饰器的语法糖的运行规则如下:

  • 函数名(参数)的组合优先执行

    outer_plus(1, 2)传入实参,在outer_plus的函数体中定义了outer函数,并作为返回值返回

  • outer作为返回值返回后,组成了@outer语法糖

    也就是将紧贴下方的func传入outer,定义了inner并返回了inner,其中所有的参数都可以从各级名称空间拿到。

    最终用inner替换了原本的func。

无参与有参装饰器模板

# 无参装饰器
def outer(func):
    def inner(*args, **kwargs):
        # 新追加功能
        res = func(*args, **kwargs)
        return res
    return inner

# 有参装饰器
def outer(装饰器自己的参数)
    def inner(func):
        def wrapper(*args, **kwargs):
            # 新追加功能
            res = func(*args, **kwargs)
            return res
        return wrapper
    return inner
装饰器修复技术

语法介绍:help内置函数

def index():
    """
    我是index,我有很强大的功能
    """
    pass


help(index)
'''运行结果
Help on function index in module __main__:

index()
    我是index,我有很强大的功能
'''

help会帮助我们拿到函数的名字以及注释信息,但是在我们使用装饰器时,因为被装饰的函数被替换成wrapper函数,导致我们的注释信息被覆盖了。

def outer(func):
    def wrapper(*args, **kwargs):
        '''我是wrapper,我喜欢换'''
        res = func(*args, **kwargs)
        return res
    return wrapper

@outer
def index():
    """我是index,我有很强大的功能"""
    pass

help(index)

'''运行结果
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)
    我是wrapper,我喜欢换
'''

可以看到注释和函数名都被替换了,为了防止装饰器对原函数的注释信息进行覆盖,我们可以利用别人写好的一个功能来对此进行修复:

from functools import wraps  # 导入别人的功能

def outer(func):
    @wraps(func)  # 本质是个有参装饰器
    def wrapper(*args, **kwargs):
        '''我是wrapper,我喜欢换'''
        res = func(*args, **kwargs)
        return res
    return wrapper

@outer
def index():
    """我是index,我有很强大的功能"""
    pass


help(index)
"""运行结果
Help on function index in module __main__:

index()
    我是index,我有很强大的功能
"""
# 相当于使我们的装饰器替换的函数更像原函数了

练习

练习1

对以下四个功能添加登录认证的装饰器,在使用这些功能的时候,必须要经过登录认证,认证成功后能使用所有功能,并不再需要再次认证。register|login|transfer|withdraw

user = None


def login_auth(func):
    def inner(*args, **kwargs):
        global user
        if not user:
            username = input('用户名:').strip()
            password = input("密码:").strip()
            if username == 'leethon' and password == '123':
                user = 'leethon'
                print('登录成功')
            else:
                print('用户名或者密码错误,请重新输入')
                return
        res = func(*args, **kwargs)
        return res

    return inner


@login_auth
def register():
    print('--注册功能--')


@login_auth
def login():
    print('--登录功能--')


@login_auth
def transfer():
    print('--转账功能--')


@login_auth
def withdraw():
    print('--提款功能--')


func_dict = {
    '1': register,
    '2': login,
    '3': transfer,
    '4': withdraw,
    '0': quit
}
while True:
    print('0:退出|1:注册|2:登录|3:转账|4:提款')
    choice = input('请输入你的功能代码:')
    if choice in func_dict:
        func_dict.get(choice)()
    else:
        print('请输入正确的功能代码')
练习2

使用有参装饰器编写多种用户登录校验策略

# 用户字典存临时用户
user_dict = {}
# 在文件存了年费会员的信息userinfo.txt
auth_dict = dict.fromkeys(['admin_user', 'temp_user', 'vip_user'],None)
# 每次启动程序将认证三种认证设为没有认证

# 装饰器编写
def login_auth(mode):
    def inner(func):
        def wrapper(*args, **kwargs):
            # 管理员登录后可以进行注册功能和管理功能
            if mode == '管理员登录':
                if not auth_dict.get('admin_user'):
                    print('此功能需要管理员权限,请先登录')
                    username = input('管理员的用户名:').strip()
                    password = input("管理员的密码:").strip()
                    if username == 'leethon' and password == '123':
                        auth_dict['admin_user'] = 'leethon'
                        print('管理员登录成功')
                    else:
                        print('用户名或者密码错误,请重新输入')
                        return
                res = func(*args, **kwargs)
                return res
            # 需要临时卡片才能进入会所
            elif mode == '用户字典校验':
                # 其他功能临时会员或年费会员都可以使用
                if not auth_dict.get('temp_user') or not auth_dict.get('vip_user'):
                    # 临时会员程序结束后就消失
                    if not user_dict:
                        print('还没有注册临时用户')
                        return
                    print('此功能需要临时卡,请先登录')
                    username = input('临时用户名:').strip()
                    password = input("临时密码:").strip()
                    if username in user_dict and password == user_dict.get(username):
                        auth_dict['temp_user'] = username
                        print('临时会员登录成功')

                    else:
                        print('用户不存在或密码不正确')
                        return
                res = func(*args, **kwargs)
                return res
            # 需要年费会员才能开通特殊服务
            elif mode == '本地文件校验':
                if not auth_dict.get('vip_user'):
                    # 打开文件校验密码
                    print('此功能需要vip权限,请先登录')
                    username = input('vip账户名:').strip()
                    password = input("至尊密码:").strip()
                    with open(rf'userinfo.txt', 'r', encoding='utf8')as f:
                        for line in f:
                            name, pwd = line.strip().split('|')
                            if name == username and pwd == password:
                                auth_dict['vip_user'] = username
                                print('年费用户登录成功')
                                break
                        else:
                            print('用户不存在或密码不正确')
                            return
                res = func(*args, **kwargs)
                return res

        return wrapper

    return inner


@login_auth('管理员登录')
def register():
    order = input('输入序号,选择注册【1临时|2年费】会员:')
    username = input('你要注册的用户名:').strip()
    password = input("输入注册的密码:").strip()
    if order == '1':
        user_dict[username] = password
        print(f'临时会员{username}注册成功')
    elif order == '2':
        with open(rf'userinfo.txt', 'a', encoding='utf8')as f:
            f.write(f'{username}|{password}\n')
            print(f'年费会员{username}注册成功')
    else:
        print('输入了错误的设置')


@login_auth('管理员登录')
def admin():
    print('--管理功能--')


@login_auth('用户字典校验')
def let_in():
    print('--进入会所--')


@login_auth('本地文件校验')
def serve():
    print('--捏脚捶背--')


func_dict = {
    '1': register,
    '2': admin,
    '3': let_in,
    '4': serve,
    '0': quit
}
while True:
    print('0:退出|1:注册|2:登录|3:进店|4:特殊服务')
    choice = input('请输入你的功能代码:')
    if choice in func_dict:
        func_dict.get(choice)()
    else:
        print('请输入正确的功能代码')
作业3
"""
1.先编写校验用户身份的装饰器
2.然后再考虑如何保存用户登录状态
3.再完善各种需求
"""
user_data = {
    '1': {'name': 'jason', 'pwd': '123', 'access': ['1', '2', '3']},
    '2': {'name': 'kevin', 'pwd': '321', 'access': ['1', '2']},
    '3': {'name': 'oscar', 'pwd': '222', 'access': ['1']}
}
hashmap = {v.get('name'): k for k, v in user_data.items()}
print(hashmap)
login_user_index = None


def login_auth(mode):
    def inner(func):
        def wrapper(*args, **kwargs):
            global login_user_index

            # 如果登录过了,就直接校验权限,并执行功能
            if not login_user_index:
                # 选择想用的功能时要求登录
                username = input('用户名:').strip()
                password = input('密码:').strip()
                # 校验密码
                if username in hashmap:
                    user_index = hashmap.get(username)
                    user_dict = user_data.get(user_index)
                    if user_dict.get('pwd') == password:
                        # 保存登录状态
                        login_user_index = hashmap.get(username)
                        print('登录成功')
                    else:
                        print('账户或者密码错误')
                        return

            # 核验用户权限
            if login_user_index:
                if mode in user_data.get(login_user_index).get('access'):
                    res = func(*args, **kwargs)
                    return res
                print('抱歉,你没有此功能的权限')

        return wrapper

    return inner


@login_auth('1')
def func1():
    """func1"""
    print('func1')


@login_auth('2')
def func2():
    """func2"""
    print('func2')


@login_auth('3')
def func3():
    """func3"""
    print('func3')


func_dict = {
    '0': quit,
    '1': func1,
    '2': func2,
    '3': func3,
}
while True:
    choice = input('功能序号:').strip()
    if choice in func_dict:
        func_dict.get(choice)()


posted @   leethon  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示