装饰器是Python中最具代表性的语法特性之一。它让你能在不修改原函数代码的前提下,以声明的方式增强或改变函数/类的行为。而@语法糖则是让这种能力变得优雅易读的关键。
1.基础回顾:函数是一等公民
理解装饰器之前,必须牢记Python中函数就是普通对象:
python
def greet(name):
return f"Hello, {name}"
# 1. 可以赋值给变量
g = greet
print(g("World")) # Hello, World
# 2. 可以存储在容器中
funcs = [greet, len]
print(funcs[0]("Python")) # Hello, Python
# 3. 可以作为参数传递
def apply(func, arg):
return func(arg)
print(apply(greet, "Decorator")) # Hello, Decorator
# 4. 可以在函数内部定义并返回(闭包)
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times3 = make_multiplier(3)
print(times3(10)) # 30
装饰器本质上就是一种接收函数,返回新函数的可调用对象,它借用了闭包和高阶函数的特性。
2.装饰器原理与@语法糖
语法糖@的核心等价式是 func = decorator(func)
先看一个没有语法糖的装饰器
假设我们要打印函数执行的耗时:
python
import time
def timer(func):
"""一个简单的计时装饰器"""
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
def slow_square(n):
time.sleep(0.1)
return n * n
# 手动装饰
slow_square = timer(slow_square)
print(slow_square(5))
# 输出:
# slow_square took 0.100123s
# 25
timer就是一个典型的装饰器:接收函数func,返回一个增强后的新函数wrapper。
语法糖@的本质
@decorator仅仅是一种语法糖(Syntactic Sugar),即让代码更易读,更美观,而不改变底层执行逻辑。编辑器遇到:
python
@timer
def slow_square(n):
time.sleep(0.1)
return n * n
完全等价于:
python
def slow_square(n):
time.sleep(0.1)
return n * n
slow_square = timer(slow_square)
注意:装饰动作发生在函数定义时,而不是调用时。导入模块时,装饰器代码就会立即执行。
3.通用装饰器:支持任意参数
为了装饰任意签名(参数列表)的函数,我们使用 *args,**kwargs 来转发所有参数:
python
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 5)
# 输出:
# Calling add with args=(3, 5), kwargs={}
# add returned 8
保留元信息:functools.wraps
直接返回 wrapper 会丢失原函数的名称,文档字符串,参数列表等元信息:
python
print(add.__name__) # wrapper (我们希望是 add)
print(add.__doc__) # None
这会给调试和文档生成造成困扰。标准做法是使用 functools.wraps,它会把原函数元信息复制到 wrapper 上:
python
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""Return the sum of a and b."""
return a + b
print(add.__name__) # add
print(add.__doc__) # Return the sum of a and b.
永远记得给装饰器内部的 wrapper 加上@wraps(func)。
4.带参数的装饰器
有时我们希望装饰器本身接收参数,例如 @repeat(3)让函数执行三次。这需要三层嵌套:
python
def repeat(n):
"""装饰器工厂:返回真正的装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hi(name):
print(f"Hi, {name}!")
say_hi("Alice")
# 输出三遍 Hi, Alice!
执行过程解析:
1.repeat(3)返回 decorator 函数。
2.@decorator 相当于 say_hi = decorator(say_hi)。
3.decorator(say_hi)返回最终的 wrapper。
带参装饰器本质上是装饰器工厂------外层函数负责接收配置参数,内层才是真正的装饰器
5.类装饰器
装饰器不一定是函数,如何可调用对象(实现了 call 的类实例)都可以作为装饰器。
基于类的无参数装饰器
python
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} has been called {self.count} times")
return self.func(*args, **kwargs)
@CountCalls
def greet():
print("Hello!")
greet() # greet has been called 1 times
greet() # greet has been called 2 times
@CountCalls 等价于 greet = CountCalls(greet),创建的实例自身就是装饰后的可调用对象。
带参数的类装饰器
python
class Retry:
def __init__(self, max_attempts, delay=0):
self.max_attempts = max_attempts
self.delay = delay
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == self.max_attempts:
raise
time.sleep(self.delay)
return wrapper
@Retry(max_attempts=3, delay=1)
def unstable_network_call():
# 模拟失败
raise ConnectionError("No network")
return "data"
@Retry(max_attempts = 3,delay = 1)相当于先创建实例 Retry(3,1),然后该实例对函数进行调用: unstable_network_call = Retry(3,1)(unstable_network_call)。
6.执行顺序
同一个函数可以被多个装饰器修饰,它们按由下而上的顺序应用:
python
@deco1
@deco2
@deco3
def func():
pass
等价于:
python
func = deco1(deco2(deco3(func)))
函数定义时的执行顺序是从最靠近函数的装饰器开始(自下而上)。
但在调用时,每一层 wrapper 的执行顺序恰好相反:最外层装饰器的迁至代码先执行,然后逐层进入,最后执行原函数;返回时原路返回。
从下到上装饰,从内到外执行的顺序