第 7 章 装饰器
7.1 函数是一等公民
在 Python 中,函数和整数、字符串一样,是普通的对象:
def shout(text):
return text.upper()
# 函数可以赋值给变量
yell = shout
print(yell("hello")) # HELLO
# 函数可以存放在数据结构中
funcs = [str.upper, str.lower, str.title]
for f in funcs:
print(f("hello world"))
# HELLO WORLD
# hello world
# Hello World
# 函数可以作为参数传递
def apply(func, value):
return func(value)
print(apply(len, "hello")) # 5
print(apply(sorted, [3, 1, 2])) # [1, 2, 3]
# 函数可以在函数内部定义
def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
print(add5(10)) # 15
7.2 装饰器原理:从手动包装到 @ 语法糖
装饰器本质上就是一个接收函数并返回新函数的函数。
手动包装
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__} 耗时 {elapsed:.4f}s")
return result
return wrapper
def slow_function():
time.sleep(1)
return "done"
# 手动包装
slow_function = timer(slow_function)
slow_function() # slow_function 耗时 1.0012s
@ 语法糖
上面的 slow_function = timer(slow_function) 可以用 @ 简写:
@timer
def slow_function():
time.sleep(1)
return "done"
# 完全等价于:
# slow_function = timer(slow_function)
slow_function() # slow_function 耗时 1.0012s
装饰器的执行流程
def my_decorator(func):
print(f"装饰 {func.__name__}") # 在装饰时执行,不是调用时
def wrapper(*args, **kwargs):
print("调用前")
result = func(*args, **kwargs)
print("调用后")
return result
return wrapper
@my_decorator # 这一行会立即执行 my_decorator(say_hello)
def say_hello(): # 输出: 装饰 say_hello
print("Hello!")
print("---")
say_hello()
# ---
# 调用前
# Hello!
# 调用后
7.3 带参数的装饰器
如果装饰器本身需要参数,需要再嵌套一层:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for _ in range(n):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(3) # 先调用 repeat(3) 得到 decorator,再用 decorator 装饰
def greet(name):
print(f"Hello, {name}!")
return name
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
# 实用:重试装饰器
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"第 {attempt} 次失败: {e},{delay}s 后重试...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unreliable_api():
import random
if random.random() < 0.7:
raise ConnectionError("网络错误")
return "成功"
7.4 functools.wraps 保留元信息
装饰后,函数的 __name__、__doc__ 等属性会丢失:
def timer(func):
def wrapper(*args, **kwargs):
"""wrapper 的文档"""
return func(*args, **kwargs)
return wrapper
@timer
def my_func():
"""my_func 的文档"""
pass
print(my_func.__name__) # wrapper — 丢失了原函数名!
print(my_func.__doc__) # wrapper 的文档 — 丢失了原文档!
用 functools.wraps 解决:
from functools import wraps
def timer(func):
@wraps(func) # 把 func 的元信息复制到 wrapper
def wrapper(*args, **kwargs):
"""wrapper 的文档"""
return func(*args, **kwargs)
return wrapper
@timer
def my_func():
"""my_func 的文档"""
pass
print(my_func.__name__) # my_func ✓
print(my_func.__doc__) # my_func 的文档 ✓
# 还可以通过 __wrapped__ 访问原函数
print(my_func.__wrapped__) # <function my_func at 0x...>
规则:写装饰器时,总是使用 @functools.wraps。
7.5 类装饰器
用类实现装饰器
类只要实现了 __call__ 方法就可以作为装饰器:
from functools import wraps
class CountCalls:
def __init__(self, func):
wraps(func)(self) # 等价于用 @wraps
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"{self.func.__name__} 被调用了 {self.call_count} 次")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # say_hello 被调用了 1 次 \n Hello!
say_hello() # say_hello 被调用了 2 次 \n Hello!
print(say_hello.call_count) # 2
用装饰器装饰类
装饰器不仅能装饰函数,也能装饰类:
def singleton(cls):
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("初始化数据库连接")
db1 = Database() # 初始化数据库连接
db2 = Database() # 不会再初始化
print(db1 is db2) # True
# 给类自动添加方法
def add_repr(cls):
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(3, 4)) # Point(x=3, y=4)
7.6 多个装饰器的叠加顺序
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice"))
# <b><i>Hello, Alice</i></b>
# 执行顺序:从下往上装饰,从外往内执行
# 等价于:greet = bold(italic(greet))
# 调用时:bold 的 wrapper → italic 的 wrapper → greet
7.7 常见实战场景
计时器
from functools import wraps
import time
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[{func.__name__}] {elapsed:.4f}s")
return result
return wrapper
缓存
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_compute(n):
time.sleep(0.1) # 模拟耗时计算
return n ** 2
expensive_compute(5) # 0.1s
expensive_compute(5) # 瞬间返回(缓存命中)
# 查看缓存信息
print(expensive_compute.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
# 清除缓存
expensive_compute.cache_clear()
权限检查
from functools import wraps
def require_auth(role="user"):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("authenticated"):
raise PermissionError("未认证")
if user.get("role") != role and role != "user":
raise PermissionError(f"需要 {role} 权限")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_auth(role="admin")
def delete_user(current_user, user_id):
print(f"删除用户 {user_id}")
admin = {"authenticated": True, "role": "admin"}
user = {"authenticated": True, "role": "user"}
delete_user(admin, 42) # 删除用户 42
# delete_user(user, 42) # PermissionError: 需要 admin 权限
日志记录
from functools import wraps
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.info(f"调用 {func.__name__}({signature})")
try:
result = func(*args, **kwargs)
logger.info(f"{func.__name__} 返回 {result!r}")
return result
except Exception as e:
logger.exception(f"{func.__name__} 抛出异常 {e}")
raise
return wrapper
@log_call
def divide(a, b):
return a / b
divide(10, 3)
# INFO: 调用 divide(10, 3)
# INFO: divide 返回 3.3333333333333335
输入验证
from functools import wraps
def validate_types(**expected_types):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 获取函数参数名
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name, expected_type in expected_types.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"参数 {param_name} 期望 {expected_type.__name__},"
f"实际得到 {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {"name": name, "age": age}
create_user("Alice", 30) # {'name': 'Alice', 'age': 30}
# create_user("Alice", "30") # TypeError: 参数 age 期望 int,实际得到 str
本章小结:装饰器是 Python 最强大的语法特性之一,本质是利用函数作为一等公民的特性,对函数进行包装增强。核心要点:永远使用
@functools.wraps保留元信息、理解多层装饰器的执行顺序、区分装饰函数和装饰类的场景。