Python高级之装饰器

装饰器

【一】装饰器介绍

装饰器的由来

  • 软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。
    • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
    • 对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
  • 软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃
    • 而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

装饰器的基本定义

装饰器是 Python 中一种用于修改或扩展函数行为的高级技术。装饰器本质上是一个函数,它接受一个函数作为输入,并返回一个新的函数,通常用于在不修改原始函数代码的情况下添加新的功能或行为。

装饰器的语法使用 @decorator 这样的语法糖,将装饰器应用到函数上。在应用装饰器后,原始函数会被包裹或修改,从而获得额外的功能。

下面是一个简单的装饰器示例:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

# 调用被装饰后的函数
say_hello()

在这个例子中,my_decorator 是一个装饰器函数,它接受一个函数 func 作为参数,并返回一个新的函数 wrapperwrapper 函数在调用原始函数 func 前后打印一些信息。

通过使用 @my_decorator 语法糖,我们将 say_hello 函数传递给装饰器,实际上相当于 say_hello = my_decorator(say_hello)。当调用 say_hello() 时,实际上调用了被装饰后的 wrapper 函数。

装饰器的特点:

  1. 接受函数作为参数: 装饰器是一个接受函数作为输入的函数。
  2. 返回新函数: 装饰器通常返回一个新的函数,该函数通常包装了原始函数,并添加了额外的功能。
  3. 使用 @ 语法: 使用 @decorator 语法糖可以方便地应用装饰器。

常见应用场景:

  1. 日志记录: 记录函数的调用信息,参数和返回值。
  2. 性能分析: 计算函数执行时间等性能指标。
  3. 权限检查: 检查用户是否有权限执行特定函数。
  4. 缓存: 缓存函数的输出,避免重复计算。
  5. 异常处理: 包装函数,捕获异常并进行处理。

【二】装饰器的分类

【0】调用装饰器

def 装饰器(func):
    def inner():
        函数体内容
        return 返回的内容
    return inner   # 装饰器返回内部执行代码块的内存地址
def 函数体():
    函数内容
函数名 = 装饰器(函数名)   # 通过函数名重新赋值内存地址
函数名()   # 增加了装饰器内部函数的功能,且继承了原函数的功能,并可以通过同样的变量名调用

【1】无参装饰器

  • 无参装饰器和有参装饰器的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。
# 通过参数 func 接收外部的函数地址
def wrapper(func):
    def inner():
        # 第一部分:执行外部传入的函数之前执行的代码
        '''...'''
        # 第二部分:执行外部传入的函数地址(res接受外部函数的返回值,如果有返回值的情况下)
        result = func()
        # 第三部分:执行外部传入的函数之后执行的代码
        '''...'''
        return result
    return inner

# 被装饰函数需要传参数时:
# 通过参数 func 接收外部的函数地址
def wrapper(func):
    def inner(*args, **kwargs):   ### 参数传递至此 ###
        # 第一部分:执行外部传入的函数之前执行的代码
        '''...'''
        # 第二部分:执行外部传入的函数地址(res接受外部函数的返回值,如果有返回值的情况下)
        result = func(*args, **kwargs)
        # 第三部分:执行外部传入的函数之后执行的代码
        '''...'''
        return result
    return inner

【2】有参装饰器

有参数的装饰器是指在装饰器外部可以传递一些参数,以影响装饰器的行为。这样的装饰器通常有两层嵌套,外层接收参数,内层接收函数。下面是一个简单的有参装饰器的例子:

def my_decorator(param):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Decorator received parameter: {param}")
            # Decorator received parameter: Hello, I'm a parameter.
            # 语法糖的参数param可以被装饰器内部的函数调用
            result = func(*args, **kwargs)  # Hello! # 执行原来的say_hello的函数
            print(f"Function {func.__name__} was called.")  # Function say_hello was called.
            return result

        return wrapper

    return decorator


# 使用有参语法糖
# @my_decorator(param="Hello, I'm a parameter.")
def say_hello():
    print("Hello!")  # 没有返回值:None

a=my_decorator(param='最外层语法糖的内容')
say_hello = a(say_hello)
say_hello()
'''
Decorator received parameter: 最外层语法糖的内容
Hello!
Function say_hello was called
'''

在这个例子中,my_decorator 是一个有参装饰器。它实际上返回一个装饰器函数 decorator,而 decorator 函数则返回最终的包装函数 wrapper。外部传递的参数 param 影响了装饰器内部的行为。

当使用 @my_decorator("Hello, I'm a parameter.") 语法糖来装饰 say_hello 函数时,实际上相当于执行了 say_hello = my_decorator("Hello, I'm a parameter.")(say_hello)

运行 say_hello() 时,将执行 wrapper 函数,同时输出装饰器接收到的参数以及函数的调用信息。

有参装饰器非常灵活,可以根据外部传递的参数来定制装饰器的行为,使得装饰器更具通用性和可配置性。

【三】装饰器语法糖

【0】语法糖的概念

  • 语法糖(Syntactic Sugar)是一种编程语言的语法结构,它并不提供新的功能,而是提供了一种更方便、更易读、更简洁的语法形式。这种语法形式并不改变语言的表达能力,但使得代码更加易于理解和书写。
  • 在编程中,语法糖的存在旨在使得代码更加简洁,减少冗余,提高可读性。这些语法糖通常是通过编译器或解释器在底层转化为更基础的语法结构,而不是引入新的语法规则。

以下是一些常见的 Python 语法糖的例子:

  1. 列表推导式:

    # 常规写法
    squares = []
    for x in range(10):
        squares.append(x**2)
    
    # 使用列表推导式
    squares = [x**2 for x in range(10)]
    
  2. 字典推导式:

    # 常规写法
    squares_dict = {}
    for x in range(10):
        squares_dict[x] = x**2
    
    # 使用字典推导式
    squares_dict = {x: x**2 for x in range(10)}
    
  3. 装饰器:

    # 常规写法
    def my_decorator(func):
        def wrapper():
            print("Something is happening before the function is called.")
            func()
            print("Something is happening after the function is called.")
        return wrapper
    
    def say_hello():
        print("Hello!")
    
    say_hello = my_decorator(say_hello)
    
    # 使用装饰器语法糖
    @my_decorator
    def say_hello():
        print("Hello!")
    

这些例子展示了语法糖的概念:它们提供了更简洁和直观的方式来表达相同的逻辑,使得代码更加优雅和易读。

【1】无参语法糖@wrapper和有参语法糖@wrapper()

  • 无参语法糖
import time


def home(name):
    time.sleep(5)
    print('Welcome to the home page', name)


# 定义外层函数
def wrapper(func): 
    # 定义内层函数:通过参数接收外部的值
    def inner(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        stop_time = time.time()
        print('run time is %s' % (stop_time - start_time))

    return inner


# 第一种调用方式
# 这里调用外层函数 wrapper 返回了内层函数的地址 inner
home = wrapper(home)
# 而内层函数其实是 inner(*args, **kwargs)
# 所以我们可以传参数进去
home("user")


# 第二种调用方式:语法糖
@wrapper
def index():
    time.sleep(5)
    print('Welcome to the index page')


index()
  • 有参语法糖
    • 有参语法糖可以理解为先执行语法糖最外层的函数,然后再添加@形成语法糖
    • @my_decorator(param)就等于my_decorator(param) == 返回的值decorator-----@decorator
    • 有参语法糖的作用就是将参数传入到装饰器内作为一个局部变量交由装饰器内部使用
def my_decorator(param):  # 不限定为只有一个参数,如果参数很多可以传入**kwargs1,但记得需要与装饰器内部的做区分
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Decorator received parameter: {param}")
            # Decorator received parameter: Hello, I'm a parameter.
            # 语法糖的参数param可以被装饰器内部的函数调用
            result = func(*args, **kwargs)   # Hello! # 执行原来的say_hello的函数
            print(f"Function {func.__name__} was called.")   # Function say_hello was called.
            return result

        return wrapper
    
    return decorator

# 使用有参装饰器
@my_decorator(param="Hello, I'm a parameter.")
def say_hello():
    print("Hello!")   # 没有返回值:None


# 调用被装饰后的函数
print(say_hello())
# Decorator received parameter: Hello, I'm a parameter.
# Hello!
# Function say_hello was called.
# None

【2】语法糖嵌套

image-20230911102529749

  • 继承顺序是从下向上集成

  • 执行顺序是从上向下执行

@check_login  # check_login 的inner = check_balance的inner
@check_balance  # check_balance的inner = get_balance()
def get_balance(*args, **kwargs):
# 多层语法糖使用
def desc_a(func):
    print(f'111')
    def inner(*args, **kwargs):
        print(f'222')
        res = func(*args, **kwargs) # 真正的add函数
        print(f'333')
        return res
    return inner

def desc_b(func):
    print(f'444')
    def inner(*args, **kwargs):
        print(f'555')
        res = func(*args, **kwargs)# desc_a 的inner
        print(f'666')
        return res
    return inner
@desc_b
@desc_a
def add():
    print(f'777')
add()  # 111 -- 444 -- 555 -- 222 -- 777 -- 333 -- 666

'''多层语法糖相当于'''
add = desc_a(add)
add = desc_b(add)
add()  # 111 -- 444 -- 555 -- 222 -- 777 -- 333 -- 666

【3】多层语法糖练习1

# 多层语法糖练习
user_data = {'username': 'dream', 'password': '521'}
bank_data = {'dream': {'pay_pwd': '521', 'balance': 1000}}
# 先验证登录
# 再验证 输入的金额 --- 符合数字 / 余额充足
def check_login(func):
    def inner(*args, **kwargs):
        username = input("请输入用户名:").strip()
        password = input("请输入密码:").strip()
        if user_data['username'] == username:
            if user_data['password'] == password:
                return func(username=username)
            else:
                return f"密码错误!"
        else:
            return f'用户名错误!'

    return inner


def check_balance(func):
    def inner(*args, **kwargs):
        pay_pwd = input("请输入支付密码:").strip()
        if bank_data[kwargs.get('username')]['pay_pwd'] == pay_pwd:
            balance = input("请输入取款金额:").strip()
            if not balance.isdigit():
                return f"金额必须是数字"
            balance = int(balance)
            if bank_data[kwargs.get('username')]['balance'] < balance:
                return f"你哪有那么多钱!"
            else:
                return func(username=[kwargs.get('username')], balance=balance)
        else:
            return f'支付密码错误!'

    return inner

# 取款函数里面
@check_login  # check_login 的inner = check_balance的inner
@check_balance  # check_balance的inner = get_balance()
def get_balance(*args, **kwargs):
    new_balance = bank_data[kwargs.get('username')]['balance'] - kwargs.get('balance')
    bank_data[kwargs.get('username')]['balance']=new_balance
    return f"取款成功,已提款{kwargs.get('balance')}现在你的余额为{new_balance}"


print(get_balance())

【4】多层语法糖练习2(三层语法糖打印顺序)

# 题目
# 判断七句print执行顺序
def outter1(func1):
    print('加载了outter1')
    def wrapper1(*args, **kwargs):
        print('执行了wrapper1')
        res1 = func1(*args, **kwargs)
        return res1
    return wrapper1


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


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


@outter1
@outter2
@outter3
def index():
    print('from index')
index()
# 答案
'''
加载了outter3
加载了outter2
加载了outter1
执行了wrapper1
执行了wrapper2
执行了wrapper3
from index
'''

image-20230911110234274

【四】装饰器形成的思路

【1】引入[为函数添加计时的功能]

  • 如果想为下述函数添加统计其执行时间的功能
import time

def index():
    time.sleep(3)
    print('Welcome to the index page’)
    return 200

index() #函数执行

【2】直接计时

  • 遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样
import time


def index():
    time.sleep(3)
    print("Welcome to the index page")
    return 200


start_time = time.time()
index()  # 函数执行
stop_time = time.time()
print('run time is %s' % (stop_time - start_time))

【3】函数作为参数

  • 考虑到还有可能要统计其他函数的执行时间
    • 于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用
    • 我们可以使用参数的形式传入
import time


def index():
    time.sleep(3)
    print("Welcome to the index page")
    return 200


def wrapper(func):  # 通过参数接收外部的值
    start_time = time.time()
    res = func()
    stop_time = time.time()
    print('run time is %s' % (stop_time - start_time))
    return res


wrapper(index)
  • 但之后函数的调用方式都需要统一改成
wrapper(index)
wrapper(其他函数)
  • 这便违反了不能修改被装饰对象调用方式的原则
    • 于是我们换一种为函数体传值的方式,即将值包给函数

【4】将值包给函数

import time


def index():
    time.sleep(3)
    print("Welcome to the index page")
    return 200


def timer(func):  # 通过参数接收外部的值
    def wrapper():
        start_time = time.time()
        res = func()
        stop_time = time.time()
        print('run time is %s' % (stop_time - start_time))

    return inner


wrapper = timer(index)
wrapper()
  • 这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能
  • 只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index
index=timer(index)  #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index
index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index
  • 至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。

【5】问题:带参数会报错

  • 但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常
import time


def home(name):
    time.sleep(5)
    print('Welcome to the home page', name)


def wrapper(func):  # 通过参数接收外部的值
    def inner():
        start_time = time.time()
        res = func()
        stop_time = time.time()
        print('run time is %s' % (stop_time - start_time))

    return inner


home = wrapper(home)
home()
'''
Traceback (most recent call last):
  File "E:\PythonProjects\def_func.py", line 322, in <module>
    home()
  File "E:\PythonProjects\def_func.py", line 314, in inner
    res = func()
TypeError: home() missing 1 required positional argument: 'name'
'''

【6】有参装饰器

  • 之所以会抛出异常,是因为home(egon)调用的其实是wrapper(egon),而函数wrapper没有参数。
    • wrapper函数接收的参数其实是给最原始的func用的
    • 为了能满足被装饰函数参数的所有情况,便用上*args+**kwargs组合
    • 于是修正装饰器timer如下
import time


def home(name):
    time.sleep(5)
    print('Welcome to the home page', name)


# 定义外层函数
def wrapper(func):
    # 定义内层函数:通过参数接收外部的值
    def inner(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        stop_time = time.time()
        print('run time is %s' % (stop_time - start_time))

    return inner


# 这里调用外层函数 wrapper 返回了内层函数的地址 inner
home = wrapper(home)
# 而内层函数其实是 inner(*args, **kwargs)
# 所以我们可以传参数进去
home("Dream")

【7】装饰器练习

# 看电影
# 用户信息字典 --- 定义用户名 和 年龄
# 大于 18 岁 看电影
# 小于 18 岁  18禁

# 取钱 ---> 登录(装饰器验证登录)
# 看电影 ==》 验证年龄(装饰器验证年龄)
user_data_dict = {'dream': 19, 'hope': 17}


# 装饰器模板
def outer(func):
    def inner():
        '''这里写调用 func 函数之前的逻辑'''
        res = func()
        '''这里写调用 func 函数之后的逻辑'''
        return res

    return inner


def watch():
    ...


def see():
    ...
user_dict = {'user': 17, 'dream': 99}

def outer(func):
    username = input("请输入用户名:")

    def inner():
        if user_dict[username] >= 18:
            return func()
        else:
            return "尚未成年,该电影你还不可以看哟~"
    return inner

def watch():
    return "你已经是成年人了,可以来看了"

watch = outer(watch)
print(watch())

【五】伪装,修复装饰器

  • 装饰器的修复技术就是为了让装饰器伪装的更像
def outter1(func1):
    print('加载了outter1')

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

    return wrapper1

# 未将装饰器装给函数时
def index():
    print('from index')

print(index)  # <function index at 0x000001E592991A20>
# 当为函数添加装饰器时
@outter
def index():
    print('from index')

# 当直接打印时可以看到是 outter1.<locals>.wrapper1 而不是函数index
print(index)  # <function outter1.<locals>.wrapper1 at 0x000002417AF41B40>

【1】查看装饰器help

  • 可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性
import time


# 定义外层函数
def wrapper(func):
    # 定义内层函数:通过参数接收外部的值
    def inner(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        stop_time = time.time()
        print('run time is %s' % (stop_time - start_time))

    return inner


@wrapper
def home(name):
    '''
    home page function
    :param name: str
    :return: None
    '''
    time.sleep(5)
    print('Welcome to the home page', name)


print(help(home))
'''
打印结果:

Help on function ###wrapper### in module __main__:

wrapper(*args, **kwargs)

None
'''
  • 通过help函数来查看home函数文档注释,却打印了wrapper的文档注释

【2】伪装装饰器functools.wraps

  • wraps 是 Python 中 functools 模块提供的一个装饰器,主要用于解决装饰器可能引发的一个问题:装饰器会改变被装饰函数的一些元信息(metadata),比如函数的名称、文档字符串等。使用 wraps 装饰器可以保留原始函数的这些元信息。

具体来说,wraps 的作用有以下几点:

  1. 保留函数名称: 装饰器会替换原始函数,如果不使用 wraps,那么被装饰后的函数的名称可能会变成装饰器函数的名称。使用 wraps 可以确保被装饰函数的名称不受影响。
  2. 保留文档字符串: 装饰器可能会导致被装饰函数失去原始的文档字符串。使用 wraps 可以确保被装饰函数继续具有正确的文档字符串。
  3. 保留模块信息: 除了名称和文档字符串外,wraps 还有助于保留原始函数的模块信息,确保被装饰后的函数与原始函数在同一模块中。

示例代码:

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper

@my_decorator
def say_hello():
    """A simple function that prints 'Hello!'"""
    print("Hello!")

# 调用被装饰后的函数
say_hello()

# 打印函数元信息
print(f"Function name: {say_hello.__name__}")
print(f"Function docstring: {say_hello.__doc__}")

在这个例子中,如果不使用 wrapssay_hello 函数的名称可能会变成 wrapper,并且文档字符串也可能会丢失。使用了 @wraps(func)say_hello 保留了正确的名称和文档字符串。

from functools import wraps


def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper docstring."""
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result

    return wrapper


@my_decorator
def example_function():
    """Original function docstring."""
    print("Inside the original function.")


# 获取帮助信息
help(example_function)

# Help on function wrapper in module __main__:
# 
# wrapper(*args, **kwargs)
#     Wrapper docstring.
def my_decorator(func):
    # @wraps(func)  # 如果不使用wraps
    def wrapper(*args, **kwargs):
        """Wrapper docstring."""
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result

    return wrapper


@my_decorator
def example_function():
    """Original function docstring."""
    print("Inside the original function.")


# 获取帮助信息
help(example_function)
# Help on function wrapper in module __main__:
# 
# wrapper(*args, **kwargs)  # 此处的文档信息就是wrapper装饰器的文档信息了
#     Wrapper docstring.
posted @ 2023-12-13 15:51  Lea4ning  阅读(17)  评论(0编辑  收藏  举报