装饰器

什么是装饰器

器指的是工具,可以定义成函数
装饰器指的是为其他事物添加额外的东西点缀

合到一起的解释:

装饰器指的定义一个函数,该函数是用来为其他函数增加额外的功能。

为何要用装饰器

引入

每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,让它变得更厚更长,这样一来,它不仅有遮羞功能,还能提供保暖,不过有个问题,这个内裤被我们改造成了长裤后,虽然还有遮羞功能,但本质上它不再是一条真正的内裤了。于是聪明的人们发明长裤,在不影响内裤的前提下,直接把长裤套在了内裤外面,这样内裤还是内裤,有了长裤后宝宝再也不冷了。装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

开放封闭原则

开放:值得是对拓展功能是开放的(在源代码不删减的情况下为其增加功能)

封闭:指的是对修改源代码是封闭的

装饰器就是在不修改被装饰对象源代码以及调用方式的前提下为被装饰对象添加新功能

需求

在不修改index函数的源代码以及调用方式的前提下,为其添加统计运行时间的功能。

import time


def index(x, y):
    time.sleep(3)

尝试方案

尝试解决方案1:失败

问题:没有修改被装饰对象的调用方式,但是修改了源代码

import time


def index(x, y):
    start = time.time()
    time.sleep(3)
    print(f"index {x} {y}")
    stop = time.time()
    print(stop - start)


index(111, 222)
index 111 222
3.004031181335449

尝试解决方案2:失败

问题:没有修改被装饰对象的调用方式,也没有修改其源代码,并且添加了新功能,但是,代码冗余

import time


def index(x, y):
    time.sleep(3)
    print(f"index {x} {y}")


start = time.time()
index(111, 222)
stop = time.time()
print(stop - start)

start = time.time()
index(333, 444)
stop = time.time()
print(stop - start)

start = time.time()
index(555, 666)
stop = time.time()
print(stop - start)
index 111 222
3.005159378051758
index 333 444
3.003142833709717
index 555 666
3.0066633224487305

尝试解决方案3:失败

问题:解决了方案二的代码冗余问题,但带来了一个新的问题,即函数的调用方式被改变了,这样就导致函数写死了,不能为其传参

import time


def index(x, y):
    time.sleep(3)
    print(f"index {x} {y}")


def wrapper():
    start = time.time()
    index(111, 222)
    stop = time.time()
    print(stop - start)


wrapper()
index 111 222
3.0117199420928955

尝试优化

方案3的优化1:失败

问题:将index函数的参数写活了,但只能对index函数使用,一样写死了

import time


def index(x, y, z):
    time.sleep(3)
    print(f"index {x} {y} {z}")


def wrapper(*args, **kwargs):
    start = time.time()
    index(*args, **kwargs)  # 只能对Index使用 写死了
    stop = time.time()
    print(stop - start)


wrapper(333, 444, 555)
index 333 444 555
3.004234552383423

方案3的优化2

在优化1的基础上把被装饰的对象写活了,原来只能装饰index

import time


def index(x, y, z):
    time.sleep(3)
    print(f"{index.__name__} {x} {y} {z}")


def outer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)  # func就是内存index的内存地址
        stop = time.time()
        print(stop - start)
        print(f"内部func的名字实际是 --> {func.__name__}")

    print(f"内部wrapper的内存地址:{id(wrapper)}")
    return wrapper


# index=当初wrapper的内存地址
index = outer(index)
print(f"打印外部index的内存地址:{id(index)},其实就嵌套函数 -- >{index.__name__}")
index(400, 500, 600)
内部wrapper的内存地址:2292528530768
打印外部index的内存地址:2292528530768,其实就嵌套函数 -- >wrapper
wrapper 400 500 600
3.0080955028533936
内部func的名字实际是 --> index
import time

# 定义一个装饰器函数outer,接收一个函数作为参数,并返回一个新的函数wrapper
def outer(func):
    def wrapper(*args, **kwargs):
        start = time.time()  # 记录当前时间作为开始时间
        func(*args, **kwargs)  # 调用传入的函数func,传递*args和**kwargs参数
        stop = time.time()  # 记录当前时间作为结束时间
        print(stop - start)  # 输出函数执行的时间
        print(f"内部func的名字实际是 --> {func.__name__}")  # 输出传入函数的名称 register
    return wrapper  # 返回包装后的函数

# 定义一个函数register,接收两个参数name和age
def register(name, age):
    time.sleep(3)  # 模拟函数执行的耗时操作,暂停3秒
    print(f"{register.__name__} {name} {age}")  # 输出函数名称、name和age参数的值

register = outer(register)  # 使用outer装饰器装饰register函数,返回一个新的函数
register("小满", 3)  # 调用装饰后的register函数,传递"name"和"age"参数

# 这段代码的功能是计算函数register的执行时间,并输出执行时间以及函数的名称。
# outer函数是一个装饰器,用于为register函数添加额外的功能。
# wrapper函数是内部的包装函数,用于记录函数的执行时间和输出函数的名称。
# 在调用register函数时,实际上是调用了outer装饰器返回的wrapper函数,
# 在wrapper函数内部会先记录开始时间,然后调用原函数register,执行register函数的代码,
# 最后记录结束时间,并输出执行时间和函数名称。
# 输出结果将会是register函数的执行时间以及函数名称。
wrapper 小满 3
3.000748872756958
内部func的名字实际是 --> register

image-20231211142256698

方案3的优化3

将wrapper做的跟被装饰的对象一模一样,以假乱真(内存地址不一样)

import time


def outer(func):  # index的内存地址
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print(stop - start)

        return res

    # 偷梁换柱:index 这个名字指向wrapper的内存地址
    return wrapper


def index(x, y):
    time.sleep(3)
    print(f"index {x} {y}")
    return "小满"


index = outer(index)
res = index(666, 777)
print(f"返回值 --> {res}")
print(index)
index 666 777
3.004465103149414
返回值 --> 小满
<function outer.<locals>.wrapper at 0x000001DC7909EF80>

大方向

如果在方案3的基础上不改变函数的调用方式

语法糖(让你开心的语法)

在被装饰对象正上方的单独一行写@装饰器名字

如果不想要装饰器的功能,删除被装饰对象上面@这一行的代码就行

使用装饰器的目的

  1. 为被装饰的对象添加新的功能
  2. 前提:不修改被装饰对象的源代码,不修改调用方式
import time


# 装饰器
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print(stop - start)
        return res
    return wrapper
# 偷梁换柱:home这个名字指向wrapper的内存地址
# 在被装饰对象正上方单独写一行@装饰器的名字
# @timer # index = timer(index) 注释掉,不执行装饰器功能
def index(x, y, z):
    time.sleep(3)
    print(f"index {x} {y} {z}")

@timer  # home = timer(home)
def home(name):
    time.sleep(2)
    print(f"welcome {name} come to home")
    return True

print(home("小满"))
welcome 小满 come to home
2.01141357421875
True

无参装饰器模板

def outer(func):
    def wrapper(*args, **kwargs):
        # 1. 调用原函数
        res = func(*args, **kwargs)  # 调用传入的原函数,传递*args和**kwargs参数
        # 2. 为其增加新功能
        return res  # 返回原函数的执行结果
    return wrapper  # 返回包装后的函数

# outer函数接收一个函数作为参数,并返回一个新的包装函数,用于增加新功能
# wrapper函数是内部的包装函数,用于调用原函数并返回其结果
# *args表示接收任意数量的位置参数,**kwargs表示接收任意数量的关键字参数

一些案例

user_dict = {"小满": 3, "黄忠": 20}


def check_age(func):
    def wrapper(*args, **kwargs):
        name = kwargs.get("name")
        if user_dict.get(name) >= 18:
            res = func(*args, **kwargs)
            return res
        else:
            print(f"亲爱的【{name}】召唤师,年龄太小哦~ 还不能买票 ^_^")
    return wrapper


@check_age
def ticket(name):
    print(f"召唤师【{name}】欢迎使用峡谷购票系统 ^_^")


ticket(name="小满")
ticket(name="黄忠")
亲爱的【小满】召唤师,年龄太小哦~ 还不能买票 ^_^
召唤师【黄忠】欢迎使用峡谷购票系统 ^_^

统计时间装饰器

import time


# 统计时间装饰器
def timer(func):  # func --> 被装饰的对象

    def inner(*args, **kwargs):
        # *args, **kwargs 将接收过来的参数传递给调用的被装饰对象,会得到一个返回值
        # 函数开始执行时间
        start_time = time.time()
        res = func(*args, **kwargs)
        # 函数结束执行时间
        end_time = time.time()

        # func.__name__ -- > 得到函数名称
        print(f"当前函数[{func.__name__}]的执行时间为:[{end_time - start_time}]秒!")
        return res

    return inner


@timer  # timer = inner = timer(func)
def foo():
    time.sleep(3)  # 单位秒


foo()

当前函数[foo]的执行时间为:[3.0148932933807373]秒!

身份认证装饰器

login_auth = None


def login_user():
    global login_auth

    name = input("请输入姓名:").strip()
    password = input("请输入密码:").strip()
    if name == "小满" and password == "123":
        login_auth = name
        print("登录成功!")
    else:
        print("账号或密码错误!")


# 身份认证装饰器
def login(func):
    def inner(*args, **kwargs):
        if login_auth:
            res = func(*args, **kwargs)
            return res
        else:
            print("当前用户未登录,无法玩耍更好玩的功能。")

    return inner


@login
def play():
    print("开始play")


@login
def shopping():
    print("开始购物了")


login_user()
play()
shopping()

请输入姓名:小满
请输入密码:123
登录成功!
开始play
开始购物了
# 如果输入错误了

请输入姓名:大乔
请输入密码:12
账号或密码错误!
当前用户未登录,无法玩耍更好玩的功能。
当前用户未登录,无法玩耍更好玩的功能。

叠加多个装饰器并判断执行

def deco(tag):
    if tag == "timer":
        def timer(func):
        
        
            @wraps(func)
            def wrapper(*args, **kwargs):
                start = time.time()
                res = func(*args, **kwargs)
                end = time.time()
                print(f"{func.__name__}函数执行时间为:{end - start}秒")
                return res
            return wrapper
        return timer
    elif tag == "login_auth":
        def login_auth(func):
            """登录验证装饰器"""

            @wraps(func)
            def wrapper(*args, **kwargs):
                if status_dict['status']:
                    return func(*args, **kwargs)
                else:
                    print("请先登录")
            return wrapper
        return login_auth
    
    
@deco(tag="login_auth")
@deco(tag="timer")
def transfer_account():
    """转账功能"""
    pass

关于装饰器保留被装饰函数的元数据

from functools import wraps

functools.wraps是一个装饰器,用于在装饰器函数中保留被装饰函数的元数据(如函数名、文档字符串、参数列表等)。它通常在自定义装饰器中使用,以确保被装饰函数的属性在装饰后仍然可用。

当你编写一个装饰器时,它会包装一个函数并返回一个新的函数。但是,新函数可能会丢失原始函数的一些属性,例如函数名和文档字符串。为了解决这个问题,你可以使用functools.wraps装饰器来将原始函数的属性复制到新函数上。

from functools import wraps


def outer(func):
    @wraps(func)  # 使用wraps装饰器
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)  # res = index(1, 2)
        return res
    # warpper.__name__ = func.__name__
    # wrapper.__doc__ = func.__doc__
    return wrapper


@outer
def index(x, y):
    """这个是主页功能,实现了"""
    print(x, y)


index(1, 2)  # wrapper(1, 2)
print(index.__name__)
print(index.__doc__)
1 2
index
这个是主页功能,实现了

有参装饰器

知识储备:

由于语法糖@的限制,outer函数只能有一个参数,并且该参数只能用来接收被装饰对象的内存地址

一言以蔽之,有参装饰器的参数是给装饰器内部函数传参的,这个参数不是被调用函数的参数

包的时候是从下往上包,执行的时候从上往下执行

def is_admin(name):
    def wrapper(func):
        def inner(*args, **kwargs):
            pwd = input("输入密码:")
            if name == "admin" and pwd == "admin":
                res = func(*args, **kwargs)
                return res
            else:
                print("账号或者密码不对")

        return inner

    return wrapper


@is_admin("admin")
def add_user(name):
    print(f"管理员[{name}]欢迎使用此功能!")


add_user("小满")

输入密码:admin
管理员[小满]欢迎使用此功能!

山炮玩法(不推荐)

from functools import wraps


def auth(func, db_type):
    @wraps(func)
    def wrapper(*args, **kwargs):
        name = input("输入姓名:").strip()
        password = input("输入密码:").strip()

        if db_type == "file":
            print("基于文件的验证")
            if name == "小满" and password == "123":
                # 正常应该从文件中获取账号和密码
                res = func(*args, **kwargs)
                return res
        elif db_type == "mysql":
            print("基于MySQL的验证")
        elif db_type == "ldap":
            print("基于ldap的验证")
        else:
            print("不支持该db_type")

    return wrapper


# @auth  # 账号和密码来源是文件
def index(x, y):
    print(f"index --> {x} {y}")


# @auth  # 账号和密码来源于数据库
def home(name):
    print(f"home --> {name}")


# @auth  # 账号和密码来源是ldap
def transfer():
    print("transfer")


index = auth(index, "file")
home = auth(home, "mysql")
transfer = auth(transfer, "ldap")

index(1, 2)
home("小满")
transfer()

输入姓名:小满
输入密码:123
基于文件的验证
index --> 1 2
输入姓名:小满
输入密码:123
基于MySQL的验证
输入姓名:小满
输入密码:123
基于ldap的验证

山炮玩法二(不推荐)

from functools import wraps


def auth(db_type):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            name = input("输入姓名:").strip()
            password = input("输入密码:").strip()

            if db_type == "file":
                print("基于文件的验证")
                if name == "小满" and password == "123":
                    # 正常应该从文件中获取账号和密码
                    res = func(*args, **kwargs)
                    return res
            elif db_type == "mysql":
                print("基于MySQL的验证")
            elif db_type == "ldap":
                print("基于ldap的验证")
            else:
                print("不支持该db_type")

        return wrapper

    return deco


deco = auth(db_type="file")
@deco  # 账号和密码来源是文件
def index(x, y):
    print(f"index --> {x} {y}")

deco = auth(db_type="mysql")
@deco  # 账号和密码来源于数据库
def home(name):
    print(f"home --> {name}")

deco = auth(db_type="ldap")
@deco  # 账号和密码来源是ldap
def transfer():
    print("transfer")


index(1, 2)
home("纯二")
transfer()

输入姓名:小满
输入密码:123
基于文件的验证
index --> 1 2
输入姓名:小满
输入密码:123
基于MySQL的验证
输入姓名:小满
输入密码:123
基于ldap的验证

语法糖(推荐)

from functools import wraps


def auth(db_type):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            name = input("输入姓名:").strip()
            password = input("输入密码:").strip()

            if db_type == "file":
                print("基于文件的验证")
                if name == "小满" and password == "123":
                    # 正常应该从文件中获取账号和密码
                    res = func(*args, **kwargs)
                    return res
            elif db_type == "mysql":
                print("基于MySQL的验证")
            elif db_type == "ldap":
                print("基于ldap的验证")
            else:
                print("不支持该db_type")

        return wrapper

    return deco


@auth(db_type="file")  # @deco # index=deco(index) # index=wrapper
def index(x, y):
    print(f"index --> {x} {y}")

@auth(db_type="mysql")  # deco # home=deco(home) # home=wrapper
def home(name):
    print(f"home --> {name}")

@auth(db_type="ldap")  #  @deco #transfer=deco(transfer) # transfer=wrapper
def transfer():
    print("transfer")


index(1, 2)
home("纯二")
transfer()

输入姓名:小满
输入密码:123
基于文件的验证
index --> 1 2
输入姓名:小满
输入密码:123
基于MySQL的验证
输入姓名:小满
输入密码:123
基于ldap的验证

有参装饰器模板

def 有参装饰器(x, y, z):  # 参数不定
    def outer(func):
        def wrapper(*args, **kwargs):
            res = func(*args, **kwargs)
            return res
        return wrapper
    return outer


@有参装饰器(1, y=2, z=3)
def 被装饰对象():
    pass

应用案例

func_dict = {}


# 有参装饰器, x接收的是用户输入功能对应的变化
def outer(x):  # x --> 1
    # 无参装饰器
    def wrapper(func):
        func_dict[x] = func  # {"1": login, "2": register}

        def inner(*args, **kwargs):
            res = func(*args, **kwargs)
            print("func指向 --> ", func.__name__)
            return res

        print("inner指向 --> ", inner.__name__)
        return inner

    print("wrapper指向 -->", wrapper.__name__)
    return wrapper


@outer("1")  # --> @wrapper
def login():
    pass


@outer("2")
def register():
    pass


print(func_dict)

wrapper指向 --> wrapper
inner指向 -->  inner
wrapper指向 --> wrapper
inner指向 -->  inner
{'1': <function login at 0x00000224705BEF80>, '2': <function register at 0x000002247089AF80>}

chatGPT代码解读

这段代码定义了一个字典func_dict,用于存储用户输入的功能对应的函数。

然后定义了一个有参数的装饰器函数outer(x),它接受一个参数x,该参数表示用户输入的功能对应的变化。

outer函数内部,定义了另一个无参数的装饰器函数wrapper(func),它接受一个函数func作为参数。在wrapper函数内部,将传入的func函数以x为键,存储到字典func_dict中。

同时,wrapper函数内部还定义了另一个内部函数inner(*args, **kwargs),它是用来包装被装饰的函数的。inner函数在执行被装饰函数之前,打印出func函数的名称,并返回被装饰函数的结果。

最后,在outer函数内部打印出wrapper函数的名称,并返回wrapper函数。

接下来,通过使用装饰器语法@outer("1")@outer("2"),将装饰器应用于函数loginregister。这相当于执行了以下代码:

login = outer("1")(login)
register = outer("2")(register)

这里的outer("1")返回了wrapper函数,然后将其应用于login函数。同样,outer("2")返回了wrapper函数,并将其应用于register函数。这样,loginregister函数被包装在inner函数中,以便在调用时执行装饰器逻辑。

最后,打印出字典func_dict的内容,可以看到func_dict中存储了用户输入的功能对应的函数。

总结:
这段代码实现了一个装饰器,用于将用户输入的功能对应的函数以字典的形式进行存储。装饰器可以接受参数,并根据参数将被装饰的函数存储到字典中。同时,装饰器还可以在执行被装饰函数之前或之后进行一些额外的逻辑处理。

叠加多个装饰器(了解)

user_dict = {"username": "小满", "password": "123"}


# 验证username的装饰器
def username_auth(func):
    def wrapper(*args, **kwargs):
        if user_dict["username"] == "小满":
            return func(*args, **kwargs)
        else:
            return False, "账号错误"

    return wrapper


# 验证密码的装饰器
def password_auth(func):
    def wrapper(*args, **kwargs):
        if user_dict['password'] == "123":
            return func(*args, **kwargs)
        return False, "密码不对"

    return wrapper


@username_auth
@password_auth
def login():
    return "正常使用了"


result = login()
print(result)

正常使用了

修改username 其他不变

user_dict = {"username": "大乔", "password": "123"}
(False, '账号错误')

修改password其他不变

user_dict = {"username": "小满", "password": "23"}
(False, '密码不对')
def deco1(func1):  # func1 = wrapper2的内存地址
    def wrapper1(*args, **kwargs):
        print("正在运行====>deco1.wrapper1")
        res1 = func1(*args, **kwargs)
        return res1
    return wrapper1

def deco2(func2):  # func2 = wrapper3的内存地址
    def wrapper2(*args, **kwargs):
        print("正在运行====>func2.wrapper2")
        res2 = func2(*args, **kwargs)
        return res2
    return wrapper2

def deco3(x):
    def outer3(func3):  # func3 = 被装饰对象index的内存地址
        def wrapper3(*args, **kwargs):
            print("正在运行====>deco3.outer3.wrapper3")
            res3 = func3(*args, **kwargs)
            return res3
        return wrapper3
    return outer3

# 加载顺序自下而上(了解)
@deco1  # index = deco1(wrapper2的内存地址)              ====> wrapper1的内存地址
@deco2  # index = deco2(wrapper3的内存地址)              ====> wrapper2的内存地址
@deco3(111)  # ====> outer3 ====> index = outer3(index) ====> wrapper3的内存地址
def index(x, y):
    print(f"from index {x} {y}")
    
print(index)
index(1, 2)  # wrapper(1, 2)

image-20231211170950783

思考题(选做)

叠加多个装饰器,加载顺序与运行顺序

@deco1 # deco1.wrapper的内存地址=deco1(deco2.wrapper的内存地址)
@deco2 # deco2.wrapper的内存地址=deco2(deco3.wrapper的内存地址)
@deco3 # deco3.wrapper的内存地址 = deco3(index)
def index(x):
    pass 

偷梁换柱,即将原函数名指向的内存地址偷梁换柱成wrapper函数,所以应该将wrapper做的跟原函数一样才行
偷梁换柱之后:

  1. index的参数是什么样子,wrapper的参数就应该是什么样子。
  2. index返回值是什么,wrapper的返回值就应该是什么样子
  3. index的属性什么样子,wrapper的属性就应该是什么样子
posted @ 2023-12-11 17:12  小满三岁啦  阅读(19)  评论(0编辑  收藏  举报