装饰器
装饰器(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 内部通过 partial 和 update_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}")