Python 装饰器 (Decorator)
参考视频: Bilibili - Python装饰器
1. 理解函数作为对象
在 Python 中,函数是一等公民 (First-class Citizen),这意味着函数可以像其他任何对象(如整数、字符串、列表)一样被对待:
可以被赋值给一个变量。
可以作为参数传递给另一个函数。
可以作为另一个函数的返回值。
这个特性是实现装饰器的基础。
xdef double(arg):
return arg * 2
def triple(arg):
return arg * 3
# 将函数 `double` 作为参数传递给 `calc_number`
def calc_number(func, arg: int):
return func(arg)
print(calc_number(double, 5))
print(calc_number(triple, 5))
输出:
xxxxxxxxxx
10
15
xxxxxxxxxx
# 一个返回函数的函数(高阶函数)
def get_multiplier_func(n):
def wrapper(arg):
return n * arg
return wrapper
# `double` 现在是一个由 `get_multiplier_func` 创建的函数对象
double = get_multiplier_func(2)
triple = get_multiplier_func(3)
print(double(10))
print(triple(10))
输出:
xxxxxxxxxx
20
30
2. 函数装饰器
装饰器本质上是一个可调用对象 (callable),它接收一个函数作为输入,并返回一个新的函数(或可调用对象)作为输出。它允许我们在不修改原函数代码的情况下,为函数增加额外的功能。
语法糖 @
的工作原理如下:
xxxxxxxxxx
def my_func():
pass
这行代码完全等价于:
xxxxxxxxxx
def my_func():
pass
my_func = decorator(my_func)
基础函数装饰器
xxxxxxxxxx
def simple_decorator(func):
"""一个简单的装饰器,仅在加载时打印信息。"""
print(f"--- Decorating function '{func.__name__}' ---")
return func
def double(arg):
return 2 * arg
print(double(5))
输出:
xxxxxxxxxx
--- Decorating function 'double' ---
10
注意,"Decorating function..." 这句话在程序加载(定义函数)时就会打印,而不是在调用 double(5)
时。
带有内部包装函数的装饰器
为了在每次调用被装饰函数时都执行某些操作(如计时、日志记录),我们需要在装饰器内部定义一个包装函数 wrapper
,并返回这个包装函数。
*args
和 **kwargs
用于确保装饰器可以处理任意参数的函数。
xxxxxxxxxx
import time
def timer_decorator(func):
"""一个计算函数执行时间的装饰器。"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs) # 调用原始函数
end_time = time.time()
print(f"Function '{func.__name__}' took {end_time - start_time:.6f} seconds to execute.")
return result
return wrapper
def slow_function(delay):
time.sleep(delay)
return f"Slept for {delay} seconds."
print(slow_function(1))
输出:
xxxxxxxxxx
Function 'slow_function' took 1.001234 seconds to execute.
Slept for 1 seconds.
带参数的装饰器
如果想让装饰器本身接收参数,例如 @my_decorator(arg)
,我们需要再嵌套一层函数。
@timer_decorator(iterations=1000)
的执行过程等价于:
xxxxxxxxxx
# 1. 调用最外层函数,传入装饰器参数
decorator_with_args = timer_decorator(iterations=1000)
# 2. 返回的内部函数(真正的装饰器)作用于被装饰的函数
slow_function = decorator_with_args(slow_function)
xxxxxxxxxx
import time
def timer_decorator_with_args(iterations: int):
"""
一个带参数的装饰器,用于多次执行函数并计算总时间。
"""
def actual_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
for _ in range(iterations):
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function '{func.__name__}' ran {iterations} times, total time: {end_time - start_time:.6f}s")
return result
return wrapper
return actual_decorator
iterations=10000) (
def double(x):
return 2 * x
print(double(3))
输出:
xxxxxxxxxx
Function 'double' ran 10000 times, total time: 0.001500s
6
3. 类装饰器
装饰器不仅可以是函数,也可以是类。这在需要维护状态(如计数)时特别有用。
类作为装饰器
要让一个类能作为装饰器,它必须实现 __init__
和 __call__
这两个魔术方法。
__init__(self, func)
: 在装饰时调用,接收被装饰的函数func
作为参数。__call__(self, *args, **kwargs)
: 在每次调用被装饰后的函数时执行。
xxxxxxxxxx
class CallCounter:
"""一个用类实现的装饰器,用于计算函数被调用的次数。"""
def __init__(self, func):
print(f"--- Decorating {func.__name__} with CallCounter ---")
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"Function '{self.func.__name__}' has been called {self.call_count} times.")
return self.func(*args, **kwargs)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
greet("Bob")
greet("Charlie")
输出:
xxxxxxxxxx
--- Decorating greet with CallCounter ---
Function 'greet' has been called 1 times.
Hello, Alice!
Function 'greet' has been called 2 times.
Hello, Bob!
Function 'greet' has been called 3 times.
Hello, Charlie!
可以看到,greet
变量现在实际上是一个 CallCounter
的实例,但它仍然可以像函数一样被调用。
装饰一个类
装饰器也可以用来装饰整个类。这允许我们修改或增强类的行为。
示例1:给类添加属性
xxxxxxxxxx
def add_version(version):
"""一个装饰器工厂,用于给类添加 __version__ 属性。"""
def class_decorator(cls):
print(f"Decorating class '{cls.__name__}' to add version '{version}'.")
cls.__version__ = version
return cls
return class_decorator
version="1.0.2") (
class MyAPIClient:
pass
print(f"MyAPIClient version is: {MyAPIClient.__version__}")
输出:
xxxxxxxxxx
Decorating class 'MyAPIClient' to add version '1.0.2'.
MyAPIClient version is: 1.0.2
示例2:包装类的方法
这个例子展示了如何通过装饰器包装一个类,并修改其某个方法的行为。
xxxxxxxxxx
def log_method_calls(cls):
"""
一个类装饰器,用于包装类的所有非魔术方法,在调用前后打印日志。
"""
class Wrapper:
def __init__(self, *args, **kwargs):
# 创建原始类的实例
self.wrapped_instance = cls(*args, **kwargs)
def __getattr__(self, name):
# 获取原始实例的属性(通常是方法)
original_attr = getattr(self.wrapped_instance, name)
# 如果是可调用方法,则进行包装
if callable(original_attr):
def method_wrapper(*args, **kwargs):
print(f"--- Calling method: {name} ---")
result = original_attr(*args, **kwargs)
print(f"--- Finished method: {name} ---")
return result
return method_wrapper
else:
# 如果是普通属性,直接返回
return original_attr
return Wrapper
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
calc = Calculator()
calc.add(10, 5)
calc.subtract(10, 5)
输出:
xxxxxxxxxx
--- Calling method: add ---
--- Finished method: add ---
--- Calling method: subtract ---
--- Finished method: subtract ---
注意: calc
对象的类型现在是 log_method_calls.<locals>.Wrapper
,而不是 Calculator
。这种方式虽然强大,但也更复杂,有时直接使用继承或元类是更好的选择。
附:Python中几个内置的类装饰器
@staticmethod
: 将一个方法转换为静态方法。静态方法不接收隐式的第一个参数(self
或cls
),它就像一个定义在类命名空间内的普通函数。@classmethod
: 将一个方法转换为类方法。类方法的第一个参数是类本身,通常命名为cls
。@property
: 将一个方法转换为只读属性。可以让你像访问属性一样调用一个方法,而不需要加括号。
0 评论:
发表评论