装饰器

装饰器(Decorator)是 Python 中一种很有趣且非常强大的工具。简单来说,装饰器本质上就是一个函数,它能让你在不修改原函数代码的情况下,给函数增加新的功能。
参考内容:Python-装饰器

基础知识

闭包

闭包概念:在一个内部函数中,对外部作用域的变量进行引用,并且外部函数的返回值为内部函数,那么内部函数就叫做闭包。

def outer_func(year): def inner_func(month): print(f'year:{year}, month:{month}') return inner_func closure_func = outer_func('2023') closure_func (12)

调用func时,产生了闭包inner_func,其持有外层函数的自由变量year,当函数func的生命周期结束之后,year这个变量依然存在,因为它被闭包引用了,不会被回收。

闭包的特点: 内部函数可以读取外层函数内的局部变量,并让其常驻内存,不会被回收,所以注意内存泄漏

函数也是对象

在 Python 中,函数可以像变量一样被传递、赋值和返回。举个例子:

def greet(name): return f"Hello, {name}!" say_hello = greet print(say_hello("World")) # 输出:Hello, World!

这里我们把函数 greet 赋值给了变量 say_hello,然后通过 say_hello 调用了 greet 函数。这证明了函数在 Python 中是可以像对象一样操作的。

装饰器的概念

装饰器本质上就是一个函数,它能让你在不修改原函数代码的情况下,给函数增加新的功能。

装饰器是一个返回函数的函数。

示例一:装饰器的执行过程

下面,让我们先用一个简单的示例了解装饰器的执行过程:

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) say_hello() # 输出 # Something is happening before the function is called. # Hello! # Something is happening after the function is called.

这里我们定义了一个简易的装饰器 my_decorator,它接收一个函数 func 作为参数,并定义了一个内部函数 wrapper 来包裹 func。在调用 func 前后,wrapper 分别打印了一些额外的信息。

然后,我们把 say_hello 函数传递给 my_decorator 并重新赋值给 say_hello,这样 say_hello 就被装饰了。

简单来说,在上述例子中,我们实现了让函数 say_hello 在执行前后,执行额外的内容。

示例二:装饰器的简易运用

举个例子:如何计算函数的执行时间?

如下,你需要计算 add 函数的执行时间。​​​​​​​

# 函数 def add(a, b): res = a + b return res
import time def add(a, b) start_time = time.time() res = a + b exec_time = time.time() - start_time print("add函数,花费的时间是:{}".format(exec_time)) return res

这个时候,如果你又需要计算减法函数(sub)的时间。不用装饰器的话,你又得重复写一段减法的代码。

这样显得很麻烦,也不灵活,万一计算时间的代码有改动,你得每个函数都要改动。

所以,我们需要引入装饰器。

下列代码的两个函数实现了计算n的阶乘和计算斐波那契数列的第n个数,同时引入了装饰器进行计算函数执行时间

import time # 定义装饰器 def time_calc(func): def wrapper(*args, **kargs): start_time = time.time() f = func(*args,**kargs) exec_time = time.time() - start_time print(f"Function {func.__name__} executed in {exec_time} seconds") return f return wrapper # 使用装饰器 @time_calc def factorial_iterative(n) -> int: # 计算n的阶乘 result = 1 for i in range(1, n + 1): result *= i return result @time_calc def fibonacci_iterative(n): # 计算斐波那契数列的第n个数 if n <= 0: return 0 elif n == 1: return 1 a, b = 0, 1 for _ in range(2, n+1): a, b = b, a + b return b factorial_iterative(20000) fibonacci_iterative(20000)

装饰器的作用:增强函数的功能,确切的说,可以装饰函数,也可以装饰类。

装饰器的原理:函数也是对象,函数可以像变量一样被传递、赋值和返回。

装饰器的基本用法

定义装饰器

装饰器可以传参,也可以不用传参。

定义一个自身不传参的装饰器

自身不传入参数的装饰器(采用两层函数定义装饰器)

def decorator(func): def wrapper(*args,**kargs): # 可以自定义传入的参数 print(func.__name__) # 返回传入的方法名参数的调用 return func(*args,**kargs) # 返回内层函数函数名 return wrapper @decorator def f(): pass f()

定义装饰器 decorator

  • 上述代码定义了一个装饰器 decorator , decorator 接受一个函数 func 作为参数,并返回一个新的函数 wrapper

定义包装函数 wrapper

  • wrapper 函数接受任意数量的位置参数(*args)和关键字参数(**kargs),因此它可以装饰接受任何参数的函数。
  • wrapper 函数内部,首先打印出传入函数的名称(func.__name__),然后调用传入的函数func,并将传入的参数传递给这个函数。最后,decorator 返回 wrapper 函数。

decorator 应用于另一个函数时,实际上是在调用 wrapper 函数,而不是原始函数。通过这种方式,你可以在调用原始函数之前或之后添加额外的逻辑(例如在这个例子中,打印函数名称)。

定义一个自身传参的装饰器

自身传入参数的装饰器(采用三层函数定义装饰器)

def login(text): def decorator(func): def wrapper(*args,**kargs): print(f"{text} -> {func.__name__}") return func(*args,**kargs) return wrapper return decorator @login('this is a parameter of decorator') def f(): pass f()

定义装饰器login

  • login函数接受一个参数text,这个参数将被用于装饰器内部。
  • login函数返回一个装饰器decorator,这是一个闭包,因为它捕获了外部函数的变量text

定义内部装饰器decorator

  • decorator接受一个函数func作为参数,这是将要被装饰的函数。

定义包装函数wrapper

  • wrapper函数接受任意数量的位置参数*args和关键字参数**kargs,这使得它可以装饰接受任何参数的函数。
  • 在wrapper函数内部,首先打印出由login函数传入的text参数和被装饰函数的名称func.__name__,然后wrapper调用原始函数func,并将传入的参数传递给这个函数。

返回包装函数

  • 包装函数 wrapper 返回 func 的调用结果,内部装饰器 decorator 返回 wrapper 函数,这样当装饰器应用于另一个函数时,实际上是在调用 wrapper 函数。

使用装饰器

在上述装饰器格式中,decorator 是我们定义好的装饰器,我们有两种方法将装饰器运用于函数

方法一:直接调用

# 装饰器不传入参数时 f = decorator(函数名) f() # 装饰器传入参数时 f = (decorator(参数))(函数名) f()

如前面所说,在实际给函数 f 运用时,实际上是调用了装饰器 decorator 里的 wrapper 函数,并将 wrapper 赋给 f

方法二:采用语法糖@符号​​​​​​​

Python 提供了一种更简洁的 @ 语法来使用装饰器,效果是一样的,但代码看起来更简洁、更易读。

# 装饰器不传入参数时 @decorator def f(): pass f() # 装饰器传入参数时 @login(参数) def f(): pass f()

系统装饰器

@property

把类内方法当成属性来使用,必须要有返回值,相当于getter

假如没有定义 @func.setter 修饰方法的话,就是只读属性

property 应用场景

在获取、设置和删除对象属性的时候,需要额外做一些工作。比如在游戏编程中,设置敌人死亡之后需要播放死亡动画。

需要限制对象属性的设置和获取。比如用户年龄为只读,或者在设置用户年龄的时候有范围限制。

这时就可以使用 property 工具,它把方法包装成属性,让方法可以以属性的形式被访问和调用。

property() 函数

语法:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

说明:

  • fget 是获取属性值的方法。
  • fset 是设置属性值的方法。
  • fdel 是删除属性值的方法。
  • doc 是属性描述信息。如果省略,会把 fget 方法的 docstring 拿来用(如果有的话)

示例代码;

class Student: def __init__(self): self._age = None def get_age(self): print('获取属性时执行的代码') return self._age def set_age(self, age): print('设置属性时执行的代码') self._age = age def del_age(self): print('删除属性时执行的代码') del self._age age = property(get_age, set_age, del_age, '学生年龄') student = Student() # 注意要用 类名.属性.__doc__ 的形式查看属性的文档字符串 print('查看属性的文档字符串:' + Student.age.__doc__) # 设置属性 student.age = 18 # 获取属性 print('学生年龄为:' + str(student.age)) # 删除属性 del student.age # 代码输出: # 查看属性的文档字符串:学生年龄 # 设置属性时执行的代码 # 获取属性时执行的代码 # 学生年龄为:18 # 删除属性时执行的代码

@property 装饰器

@property 语法糖提供了比 property() 函数更简洁直观的写法。

  • @property 装饰的方法是获取属性值的方法,被装饰方法的名字会被用做 属性名
  • @属性名.setter 装饰的方法是设置属性值的方法。
  • @属性名.deleter 装饰的方法是删除属性值的方法。

以下示例代码与使用 property() 函数版本的代码等价:

class Student: def __init__(self): self._age = None @property def age(self): print('获取属性时执行的代码') return self._age @age.setter def age(self, age): print('设置属性时执行的代码') self._age = age @age.deleter def age(self): print('删除属性时执行的代码') del self._age student = Student() print('查看属性的文档字符串:' + Student.age.__doc__) student.age = 18 print('学生年龄为:' + str(student.age)) del student.age

注意事项

可以省略设置属性值的方法,此时该属性变成只读属性。如果此时仍然设置属性,会抛出异常 AttributeError: can't set attribute

如果报错 RecursionError: maximum recursion depth exceeded while calling a Python object,很可能是对象属性名和 @property 装饰的方法名重名了,一般会在对象属性名前加一个下划线 _ 避免重名,并且表明这是一个受保护的属性。

@staticmethod

staticmethod 修饰过的方法叫静态方法,可以直接通过类调用方法,这样做的好处是执行效率比较高,也可以通过实例调用该方法。

静态方法,不需要表示自身对象的self和自身类的cls参数,就跟使用函数一样。

from datetime import datetime import time TIME_FORMAT_STR = "%Y/%m/%d/ %H:%M:%S" class TimeUtil(): @staticmethod def timestamp_to_utc_str(timestamp: float, format_str=TIME_FORMAT_STR) -> str: """时间戳转utc-0时区的时间""" datetime_obj: datetime = datetime.utcfromtimestamp(timestamp) return datetime_obj.strftime(format_str) @staticmethod def timestamp_to_local_str(timestamp: float, format_str=TIME_FORMAT_STR) -> str: """时间戳转当前本地时区的时间""" datetime_obj: datetime = datetime.fromtimestamp(timestamp) return datetime_obj.strftime(format_str) timeutil = TimeUtil() utc_0 = timeutil.timestamp_to_utc_str(timestamp=time.time()) utc_loacl = timeutil.timestamp_to_local_str(timestamp=time.time()) print(f"utc-0: {utc_0}\nutc-loacl: {utc_loacl}")

@classmethod

classmethod 修饰过的方法叫类方法,可以直接通过类或者实例调用方法

类方法,不需要self参数,但第一个参数需要是表示自身类的cls参数。

利用 @classmethod 实现单例模式

from datetime import datetime class SingletonBase(object): __instance = None @classmethod def get_instance(cls, *arg, **kwargs): if cls.__instance is None: cls.__instance = cls() cls.__instance.init(*arg, **kwargs) return cls.__instance def init(self, *arg, **kwargs): pass class TestMgr(SingletonBase): def init(self, *arg, **kwargs): print(f'arg:{arg}, kwargs:{kwargs}') if __name__ == '__main__': test_mgr = TestMgr.get_instance('hello', 'world', time=datetime.now().strftime("%Y/%m/%d/ %H:%M:%S"))

三种方法比较

实例方法、类方法、静态方法的区别

在定义静态类方法和类方法时,@staticmethod 装饰的静态方法里面,想要访问类属性或调用实例方法,必须需要把类名写上;

@classmethod 装饰的类方法里面,会传一个 cls 参数,代表本类,这样就能够避免手写类名的硬编码。

在调用静态方法和类方法时,实际上写法都差不多,一般都是通过 类名.静态方法()类名.类方法()

也可以用实例化对象去调用静态方法和类方法,但为了和实例方法区分,最好还是用类去调用静态方法和类方法。

使用场景

在定义类的时候,假如不需要用到与类相关的属性或方法时,就用静态方法 @staticmethod

假如需要用到与类相关的属性或方法,然后又想表明这个方法是整个类通用的,而不是对象特异的,就可以使用类方法 @classmethod

class Demo(object): text = "三种方法的比较" def instance_method(self): print("调用实例方法") @classmethod def class_method(cls): print("调用类方法") print(f"在类方法中 访问类属性 text: {format(cls.text)}") print(f"在类方法中 调用实例方法 instance_method: {format(cls().instance_method())}") @staticmethod def static_method(): print("调用静态方法") print(f"在静态方法中 访问类属性 text: {format(Demo.text)}") print(f"在静态方法中 调用实例方法 instance_method: {format(Demo().instance_method())}") if __name__ == "__main__": # 实例化对象 d = Demo() # 对象可以访问 实例方法、类方法、静态方法 # 通过对象访问text属性 print(d.text) # 通过对象调用实例方法 d.instance_method() # 通过对象调用类方法 d.class_method() # 通过对象调用静态方法 d.static_method() # 类可以访问类方法、静态方法 # 通过类访问text属性 print(Demo.text) # 通过类调用类方法 Demo.class_method() # 通过类调用静态方法 Demo.static_method()

@functools.wraps

通过前面的内容,我们已经知道,经过装饰器之后的函数不是原来的函数,虽然原来的函数还存在,但是真正调用的是装饰后生成的新函数。

那岂不是打破了“不能修改原函数”的规则?

是的,看下面的示例:

def auth(permission): def _auth(func): def wrapper(*args, **kwargs): print(f"func name {func.__name__}") return func(*args, **kwargs) return wrapper return _auth @auth("add") def add(a, b): """求和运算""" print(a + b) print(add) print(add.__name__) print(add.__doc__)

输出如下

<function auth.<locals>._auth.<locals>.wrapper at 0x000002B851E19300> wrapper None

为了消除装饰器对原函数的影响,我们需要伪装成原函数,拥有原函数的属性,看起来就像是同一个人一样。

functools为我们提供了便捷的方式,只需这样:

import functools # 注意此处 def auth(permission): def _auth(func): @functools.wraps(func) # 注意此处 def wrapper(*args, **kwargs): print(f"func name {func.__name__}") return func(*args, **kwargs) return wrapper return _auth @auth("add") def add(a, b): """求和运算""" print(a + b) print(add) print(add.__name__) print(add.__doc__)

输出结果

<function add at 0x000001F5F62FFBA0> add 求和运算

functools.wraps 对我们的装饰器函数进行了装饰之后,add 表面上看起来还是 add

functools.wraps 内部通过 partialupdate_wrapper 对函数进行再加工,将原始被装饰函数(add)的属性拷贝给装饰器函数(wrapper)。

装饰器的四种类型

python装饰器的4种类型:函数装饰函数、函数装饰类、类装饰函数、类装饰类

函数装饰函数

def wrapFun(func): def inner(a, b): print('function name:', func.__name__) r = func(a, b) return r return inner @wrapFun def myadd(a, b): return a + b print(myadd(2, 3))

函数装饰类

def wrapClass(cls): def inner(a): print('class name:', cls.__name__) return cls(a) return inner @wrapClass class Foo(): def __init__(self, a): self.a = a def fun(self): print('self.a =', self.a) m = Foo('xiemanR') m.fun()

类装饰函数

class ShowFunName(): def __init__(self, func): self._func = func def __call__(self, a): print('function name:', self._func.__name__) return self._func(a) @ShowFunName def Bar(a): return a print(Bar('xiemanR'))

类装饰类

class ShowClassName(object): def __init__(self, cls): self._cls = cls def __call__(self, a): print('class name:', self._cls.__name__) return self._cls(a) @ShowClassName class Foobar(object): def __init__(self, a): self.value = a def fun(self): print(self.value) a = Foobar('xiemanR') a.fun()

多装饰器

有的时候,我们需要用多个装饰器装饰一个函数,例如:

def wrapFun(func): def inner(*args,**kargs): print(f"装饰器1 before {func.__name__}") f = func(*args,**kargs) print(f"装饰器1 after {func.__name__}") return f return inner def decorator(func): def wrapper(*args,**kargs): print(f"装饰器2 before {func.__name__}") f = func(*args,**kargs) print(f"装饰器2 after {func.__name__}") return f return wrapper @wrapFun @decorator def f(): print("func") f()

上述代码输出

装饰器1 before wrapper 装饰器2 before f func 装饰器2 after f 装饰器1 after wrapper

在上述代码中,f 被 @wrapFun@decorator 装饰,我们可以将装饰器的执行过程理解成 包装 函数: 装饰器会将函数包裹起来,再剥离

被装饰的函数f,首先被内层的 @decorator 包裹起来,此时的 f 变为了 @decorator 的包装函数 wrapper ,之后,外层 @wrapFun 再将 wrapper 包裹起来,变为 inner

因此,实际运行的函数为最外层装饰器 @wrapFun 的包装函数 inner,该函数接收到的是内层的包装函数 wrapper,内层包装函数再接收 f 。

可以想象成装饰器将f包裹起来:

| 外层before | 内存before | func | 内层after | 外层after |
———————————执行顺序———————————>

↓外层 @wrapFun ↓内层 @decorator def f(): print("func")

use case

使用装饰器实现指令集

例如在 Agent 设计中,我们希望,当我们输入带有 “/command” 的命令句式时,能执行系统代码而非直接与 Agent 对话。但是如果使用 if - else 结构,或者自行维护 command dict,不方便后续管理。

于是,我们可以使用创建一个命令处理类,使用装饰器对函数进行统一指令注册:

import sys class CommandHandler: """命令处理器类""" def __init__(self, assistant): self.assistant = assistant self.commands = {} # 自动注册所有带有 command 装饰器的方法 for name in dir(self): method = getattr(self, name) if hasattr(method, '_command_name'): self.commands[method._command_name] = method @staticmethod def command(cmd_name): """命令注册装饰器""" def decorator(func): func._command_name = cmd_name return func return decorator def handle(self, command): """处理用户输入的命令""" command = command.strip() cmd_func = self.commands.get(command) if cmd_func: cmd_func() return True return False @command("/token") def show_token_usage(self): """显示token用量""" if usage := self.assistant.get_usage(): print("用量数据:") for k, v in usage.items(): print(f" {k}: {v}") else: print("暂无用量数据") @command("/history") def show_history(self): """显示对话历史""" print("对话历史:") for msg in self.assistant.chat_history: print(f" {msg.__class__.__name__}: {msg}") @command("/help") def show_help(self): """显示帮助信息""" print("可用命令:") for cmd, func in sorted(self.commands.items()): doc = func.__doc__.strip() if func.__doc__ else "无描述" print(f" {cmd:<10} - {doc}") @command("/clear") def clear_history(self): """清空对话历史""" self.assistant.chat_history = [] print("已清空对话历史") @command("/exit") def exit(self): """退出程序""" print("程序已退出") sys.exit(1)
if __name__ == "__main__": assistant = Chat_Agent(chain=chain, tools=tools) print("助手已启动(输入 '/help' 查看可用命令)") # 初始化命令处理器 cmd_handler = CommandHandler(assistant) while True: user_input = input("\n用户: ") if user_input.startswith('/'): if cmd_handler.handle(user_input): continue response = assistant.chat(user_input) print(f"助手: {response}")

初始化方法

在这段代码中,我们接收了一个 实例化的 Agent 引用并保存,用于函数中的处理,之后,我们定义了一个空字典 self.commands = {} 用于存储命令

通过反射机制,我们实现了自动注册所有命令

  • dir(self) 获取对象的所有属性名
  • getattr(self, name) 获取属性对应的方法对象
  • hasattr(method, '_command_name') 检查方法是否有特殊标记 如果有,将命令名称作为键,方法作为值,存入字典
def __init__(self, assistant): self.assistant = assistant self.commands = {} # 自动注册所有带有 command 装饰器的方法 for name in dir(self): method = getattr(self, name) if hasattr(method, '_command_name'): self.commands[method._command_name] = method

命令注册装饰器

在代码中,我们定义了一个装饰器def command(cmd_name),由于是在类中定义,我们需要标明此为静态方法@staticmethod

装饰器接收一个参数 cmd_name,给被装饰的函数增加一个 _command_name 属性,这个属性会被初始化方法用来识别和注册命令,之后返回装饰器函数

@staticmethod def command(cmd_name): """命令注册装饰器""" def decorator(func): func._command_name = cmd_name return func return decorator

命令处理方法

这个方法负责执行命令,去除用户输入的前后空格,从命令字典中查找对应函数。

def handle(self, command): """处理用户输入的命令""" command = command.strip() cmd_func = self.commands.get(command) if cmd_func: cmd_func() return True return False

/help

该方法读取 commands 字典的键值,再逐一输出每一个 commands 的信息和文档字符串

@command("/help") def show_help(self): """显示帮助信息""" print("可用命令:") for cmd, func in sorted(self.commands.items()): doc = func.__doc__.strip() if func.__doc__ else "无描述" print(f" {cmd:<10} - {doc}")