13. 闭包函数与装饰器

1. 函数名的多种用法

1.1 方法一:函数名可以当成变量名赋值

def f1(x, y):
    print(x + y)

fn = f1
fn(11, 22)  # 33

1.2 方法二:函数名可以作为容器类型的元素

def f1(x, y):
    print(x + y)

func_dict = {'Add': f1}
func_dict.get('Add')(33, 66)  # 99

1.3 方法三:函数名可以作为参数传递给另一个函数使用

def f1(x, y):
    return x + y

def f2(a, b, func):
    return func(a, b) + a + b

result = f2(a=1, b=2, func=f1)
print(result)  # 6

1.4 方法四:函数名可以作为函数的返回值

def f1(x, y):
    return x + y

print(id(f1))

def f2():
    return f1

print(id(f2()))

2. 闭包函数

2.1 概念

外层函数内部定义一个里层函数,并且里层函数用到了外层函数的变量

闭:定义在函数内部的函数

包:内层函数使用了外层函数名称空间中的名字

def outer():
    a = 666
    def inner():
        print('里层函数', a)  # 里层函数使用了外层函数名称空间中的名字
    inner()
outer()

2.2 闭包函数的应用场景

闭包传参方式1:外层函数的变量值固定

def outer():
    a = 666
    def inner():
        print('里层函数', a)  # 里层函数使用了外层函数名称空间中的名字
    return inner

res = outer()
res()  # 里层函数 666        相当于inner()

闭包传参方式2:外层函数的变量值不固定

def outer(name):
    #  当有实参传入时,name=实参
    def inner():
        print('里层函数', name)  # 里层函数使用了外层函数名称空间中的名字
    return inner

res = outer('leomessi')
res()  # 里层函数 leomessi     相当于inner()

3. 装饰器

3.1 基础概念

定义:

在不改变被装饰对象原有的“调用方式”“内部代码”的情况下,给被装饰对象添加新的功能

装饰器的原则

对扩展开放,对修改封闭

key word:

名称空间、函数名、闭包函数

装饰器的分类:

无参装饰器、有参装饰器

3.2 推导

需求:计算函数的执行时间

# 预备知识:time模块
import time
print(time.time())  # 时间戳,是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数(不考虑闰秒),用于表示一个时间点。

实现方法1:

没有修改被装饰对象的调用方式,也没修改内部代码,但是可扩展性差

import time

def f1():
    start = time.time()
    time.sleep(3)
    end = time.time()
    print(f'该函数运行耗时{end - start}s')  # 该函数运行耗时3.0012145042419434s
f1()

def f2():
    start = time.time()
    time.sleep(3)
    end = time.time()
    print(f'该函数运行耗时{end - start}s')  # 该函数运行耗时3.01076340675354s
f2()

 

实现方法2:

在很多地方都要计算f1、f2运行时间,就用到了函数

将函数名通过形参传入,封装成函数之后,调用方式变了,不符合装饰器原则

import time
def f1():
    time.sleep(3)
    print('函数f1运行')

def f2():
    time.sleep(3)
    print('函数f2运行')

def cal(func):
    start = time.time()
    func()
    end = time.time()
    print(f'函数{func}运行耗时{end - start}s')

cal(f1)
# 函数f1运行
# 函数<function f1 at 0x000001F911B23E20>运行耗时3.0101499557495117s

cal(f2)
# 函数f2运行
# 函数<function f2 at 0x000001A3B73F3370>运行耗时3.015449047088623s

 

实现方法3:

方法2给函数体直接传参无法实现装饰器,于是采用另一种给函数体传参的方式(闭包函数)

闭包函数的方式外层函数的变量值固定,无法将其它函数名传入,可扩展性差

import time
def f1():
    time.sleep(3)
    print('函数f1运行')

def outer():
    func = f1  # 真正的f1被outer局部名称空间存储了

    def inner():
        start = time.time()
        func()  # 调用了真正的f1函数
        end = time.time()
        print(f'函数{func}运行耗时{end - start}s')
    return inner

res = outer()
res()
# 函数f1运行
# 函数<function f1 at 0x0000025DCD183E20>运行耗时3.0119853019714355s

这种方式也不是真正的装饰器,需要调用res函数才能调用f1函数

 

实现方法4:

为了解决方法3外层函数变量值固定的问题,采用向外层函数传参的方式

将函数名通过形参传入,增强可扩展性

import time
def f1():
    time.sleep(3)
    print('函数f1运行')

def f2():
    time.sleep(3)
    print('函数f2运行')

def outer(func):
    def inner():
        start = time.time()
        func()  # 调用了真正的f1函数
        end = time.time()
        print(f'函数{func}运行耗时{end - start}s')
    return inner

# f1 = outer(f1)  # 左侧的f1就是一个普通的变量名
# f1()  # 看似调用的是f1,其实调用的是inner
# 函数f1运行
# 函数<function f1 at 0x0000021BEC373E20>运行耗时3.0068602561950684s

f2 = outer(f2)  # 左侧的f2就是一个普通的变量名
f2()  # 看似调用的是f2,其实调用的是inner

f1 = outer(f1)实现了装饰器,f1()即可调用原f1函数,右边的f1是原函数名,存到outer的局部名称空间了,

左边的f1只是一个普通的变量名

小结:

outer()---返回值是inner的函数地址---将inner的函数名重命名为f1或f2

f1=outer(f1)---f1就是outer内inner的内存地址

f1()---outer内的inner执行---func()---f1真正的函数内存地址---f1()

3.3 练习

# 练习:
"""
先定义一个存款、取款函数,用户登录过才能存款、取款,没有登录让先登录
不能改变原本存款、取款的调用方式
"""
def deposit():
    print('存款中...')
    return True, '取款完成'

def withdraw():
    print('存款中...')
    return True, '取款完成'

login_dict = {'username': None, 'status': None}

def outer(func):
    def inner():
        # 在运行withdraw功能之前进行验证是否登录
        if login_dict.get('username') is not None:
            res = func()  # 接收真正函数的返回值
            return res  # 条件为真,将真正函数的返回值返回
        else:
            return False, '请先登录'  # 条件为假,让用户先登录
    return inner
withdraw
= outer(withdraw) res2 = withdraw() print(res2) deposit = outer(deposit) res3 = deposit() print(res3)
# 多层验证,先验证用户名,再验证密码
def outer(func):
    def inner():
        # 在运行withdraw功能之前进行验证是否登录成功
        if login_dict.get('username') != 'messi':
            return False, '用户名错误'
        elif login_dict.get('password') != '001':
            return False, '请先登录'  # 密码错误
        else:
            res = func()
            return res
    return inner

withdraw = outer(withdraw)
res2 = withdraw()
print(res2)

3.4 无参装饰器模板

def f1():
    pass

def outer(func):
    def inner():
     执行真正函数之前,可以做的额外操作
# 其它如登录认证等操作一般放在执行真正函数之前 result = func() # 执行真正的函数

执行真正函数之后,可以做的额外操作
     对返回的结果进行处理返回 return result # 返回真正函数的返回值 return inner f1 = outer(f1) f1()

3.5 有参装饰器

引入

login_dict = {'name': 'messi', 'pwd': '001'}

def withdraw(name, money):
    print(f'用户{name}正在取款{money}元')

def outer(func):
    def inner(name, money):
        if login_dict.get('name'):
            return func(name, money)
        else:
            return False, '请先登录'
    return inner

withdraw = outer(withdraw)
withdraw('messi', 10)  # 用户messi正在取款10元

可变长参数,兼容所有函数的参数

login_dict = {'name': 'messi', 'pwd': '001'}

def withdraw(name, money):
    print(f'用户{name}正在取款{money}元')

def transfer(name, to_other, money):
    print(f'用户{name}给{to_other}转账了{money}元')

def outer(func):
    def inner(*args, **kwargs):
        if login_dict.get('name'):
            return func(*args, **kwargs)
        else:
            return False, '请先登录'
    return inner

withdraw = outer(withdraw)
withdraw('messi', 10)  # 用户messi正在取款10元

transfer = outer(transfer) transfer('ronaldo', 'kylian', 20) # 用户ronaldo给kylian转账了20元

3.6 有参装饰器模板

def f1(*args, **kwargs):
    pass

def outer(func):  # func用于接收被装饰的对象(函数)
    def inner(*args, **kwargs):
        print('执行真正函数之前,可以做的额外操作')
        res = func(*args, **kwargs)  # 执行真正的被装饰函数,res的作用是接收原函数被调用后的返回值
        print('执行真正函数之后,可以做的额外操作')
对返回的结果进行处理返回
return res # 返回真正函数的返回值 return inner f1 = outer(f1) f1()

4. 语法糖

4.1 原理:

1.使用的时候放在真正函数的上方

2.语法糖会自动将下面紧挨着的函数名传给  @  后面的函数调用

login_dict = {'name': 'messi', 'pwd': '001'}
def outer(func):
    def inner(*args, **kwargs):
        if login_dict.get('name'):
            return func(*args, **kwargs)
        else:
            return False, '请先登录'
    return inner

@outer  # 相当于:withdraw = outer(withdraw)
def withdraw(name, money):
    print(f'用户{name}正在取款{money}元')

@outer  # 相当于:transfer = outer(transfer)
def transfer(name, to_other, money):
    print(f'用户{name}给{to_other}转账了{money}元')

4.2 多层语法糖

4.2.1 多层有参装饰器

login_dict = {'name': 'messi', 'role': 'admin'}

def login(func):
    # func是withdraw函数的内存地址
    def inner(*args, **kwargs):
        print('验证登录程序')
        if not login_dict.get('name'):
            return '请先登录'
        else:
            return func(*args, **kwargs)
    return inner

def permission(func):
    # func是login的inner函数地址
    def inner(*args, **kwargs):
        print('验证权限程序')
        if login_dict.get('role') != 'admin':
            return '当前权限不能访问该功能'
        return func(*args, **kwargs)
    return inner

def withdraw(name, money):
    print(f'用户{name}正在取款{money}元')

# 必须是登录状态且角色是管理员才能取款
withdraw = login(withdraw)  # =左边是一个普通的变量名,右边是login函数返回值(inner函数内存地址)
withdraw = permission(withdraw)  # 等价于withdraw = permission(login的inner函数地址)
# 上述语句在执行时,
# 第一步:permission(login的inner)
# 第二步:在permission的里层函数中func为login的inner函数,因此进入到login函数中校验登录
# 第三步:进入login函数执行inner函数,校验登录,执行func,func为withdraw函数的内存地址
# 第四步:执行真正的withdraw
withdraw('ronaldo', 20)
# 验证权限程序
# 验证登录程序
# 用户ronaldo正在取款20元
# 上述代码按从上到下的顺序编写,先包login,再包permission,但是执行时先验证权限,再验证登录,因此要将登录和权限互换位置


# #  这种情况下就是先验证登录,再验证权限
# withdraw = permission(withdraw)
# withdraw = login(withdraw)
# withdraw('ronaldo', 20)

 

4.2.2 多层语法糖

多层语法糖的包装顺序是:从下往上包装⬆

                      执行顺序是:从上往下执行⬇

login_dict = {'name': 'messi', 'role': 'admin'}
def login(func):
    # func是withdraw函数的内存地址
    def inner(*args, **kwargs):
        print('验证登录程序')
        if not login_dict.get('name'):
            return '请先登录'
        else:
            return func(*args, **kwargs)
    return inner


def permission(func):
    # func是login的inner函数地址
    def inner(*args, **kwargs):
        print('验证权限程序')
        if login_dict.get('role') != 'admin':
            return '当前权限不能访问该功能'
        return func(*args, **kwargs)
    return inner


@login  # withdraw = login(withdraw)  左边with普通函数名,右边with取自上一步,是permission的inner函数
@permission  # withdraw=permission(withdraw)  左边withdraw普通变量名,右边真正函数,运行结果是permission的inner函数
def withdraw(name, money):
    print(f'用户{name}正在取款{money}元')
# 上述语法糖含义:先包permission,再包login

# 调用函数时,以@login为参考,这里withdraw等价于login(permission中inner函数地址)
withdraw('messi', 10)

 4.3 有参语法糖

有参的有参装饰器

login_dict = {'name': 'messi', 'role': 'admin'}
# 定义一个函数,当传入参数为login时,返回login登录验证函数 # 当传入参数为permission时,返回permission权限验证函数 def choice(status): if status == 'login': def login(func): # func是withdraw函数的内存地址 def inner(*args, **kwargs): print('验证登录程序') if not login_dict.get('name'): return '请先登录' else: return func(*args, **kwargs) return inner return login elif status == 'permission': def permission(func): # func是login的inner函数地址 def inner(*args, **kwargs): print('验证权限程序') if login_dict.get('role') != 'admin': return '当前权限不能访问该功能' return func(*args, **kwargs) return inner return permission def withdraw(name, money): print(f'用户{name}正在取款{money}元') # 以下仅验证登录 # login_a = choice('login') # 拿到login函数 # withdraw = login_a(withdraw) # withdraw('messi', 10) # 验证登录程序 # 用户messi正在取款10元 # 先验证登录再验证权限 login_a = choice('login') permission_a = choice('permission') withdraw = permission_a(withdraw) withdraw = login_a(withdraw) withdraw('ronaldo', 20) # 验证登录程序 # 验证权限程序 # 用户ronaldo正在取款20元

上述代码改写为有参语法糖

login_dict = {'name': 'messi', 'role': 'admin'}

# 定义一个函数,当传入参数为login时,返回login登录验证函数
#            当传入参数为permission时,返回permission权限验证函数
def choice(status):
    if status == 'login':
        def login(func):
            # func是withdraw函数的内存地址
            def inner(*args, **kwargs):
                print('验证登录程序')
                if not login_dict.get('name'):
                    return '请先登录'
                else:
                    return func(*args, **kwargs)
            return inner
        return login
    elif status == 'permission':
        def permission(func):
            # func是login的inner函数地址
            def inner(*args, **kwargs):
                print('验证权限程序')
                if login_dict.get('role') != 'admin':
                    return '当前权限不能访问该功能'
                return func(*args, **kwargs)
            return inner
        return permission

@choice('login')
@choice('permission')
def withdraw(name, money):
    print(f'用户{name}正在取款{money}元')

withdraw('kylian', 20)

  验证登录程序
  验证权限程序
  用户kylian正在取款20元

 

 4.4 装饰器的修复技术

解决内存地址问题

做到和真的一模一样,但是本质其实没有变

 

help可以查看指定函数的注释信息

def outer(func):
    def inner(*args, **kwargs):
        """这是inner函数的注释"""
        result = func(*args, **kwargs)
        return result
    return inner

@outer
def f1():
    """这是f1函数的注释"""
    print('f1函数已运行')

help(f1)  目前的以假乱真还有瑕疵,看到的不是真正函数f1的注释
# Help on function inner in module __main__:
#
# inner(*args, **kwargs)
#     这是inner函数的注释

使用wraps模块,将装饰器中inner函数的内存地址换成了原函数f1的内存地址,实现了和原函数一致的效果

from functools import wraps
def outer(func):
    @wraps(func)  # 将inner的内存地址换成了原函数f1的内存地址,实现了和原函数一致的效果
    def inner(*args, **kwargs):
        """这是inner函数的注释"""
        result = func(*args, **kwargs)
        return result
    return inner

@outer
def f1():
    """这是f1函数的注释"""
    print('f1函数已运行')

help(f1)
# Help on function f1 in module __main__:
#
# f1()
#     这是f1函数的注释

 

posted @ 2024-07-31 00:10  hbutmeng  阅读(9)  评论(0编辑  收藏  举报