Python 的「元编程」双雄:装饰器让你不改函数代码就给函数加能力(日志、缓存、计时——Java 课第 11 章 GUI 回调的进阶版),魔法方法让你自定义类融入语言语法(第 6 章鸭子类型的引擎)。这两章合起来,解释了 Python 框架(Flask、pytest、dataclass)的全部魔法,是从「会用 Python」到「看懂 Python 生态」的分水岭。
先看动机:给一批函数统一加「计时」功能,笨办法每个函数里插计时代码(重复);装饰器把「壳」抽出来:
import time, functools
def timer(func): # ① 接收函数
@functools.wraps(func) # ③ 保留原函数的名字和文档
def wrapper(*args, **kwargs): # ② 定义壳:任意参数
start = time.perf_counter()
result = func(*args, **kwargs) # 调用原函数
print(f"{func.__name__} 用时 {time.perf_counter() - start:.4f}s")
return result
return wrapper # 返回壳
@timer # 语法糖:等价 slow_sum = timer(slow_sum)
def slow_sum(n):
return sum(range(n))
slow_sum(10**7) # slow_sum 用时 0.2314s
拆解语法糖:@timer 就是「把下面的函数作为参数传给 timer,把返回值贴回原名」(第 3 章「函数是一等公民」的直接应用)。壳里用 *args, **kwargs 透传任意参数(第 3 章)、functools.wraps 保住原函数元信息(不加的话 slow_sum.__name__ 变成 "wrapper",调试灾难)。读第三方框架的装饰器时,永远用这三步拆:收函数 → 定义壳 → 返回壳。
带参数的装饰器(三层套娃):@retry(times=3) 是「先调 retry(times=3) 得到真正的装饰器,再套」——「装饰器工厂」。读法慢一点,结构同上多一层。
缓存(@functools.cache):
@functools.cache # 标准库自带,自动 memoize
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
fib(200) # 瞬间出(朴素递归是指数级,数据结构课第 9 章)
一行装饰器把指数递归变线性(记忆化),这是「装饰器改变算法复杂度」的极端案例。
注册器(框架的核心机制):
ROUTES = {}
def route(path): # 装饰器工厂
def deco(func):
ROUTES[path] = func # 注册:函数定义时被收进路由表
return func
return deco
@route("/hello")
def hello(): return "hi"
# Flask 的 @app.get("/") 就是这个机制:import 时自动收集路由
重试(生产刚需):
def retry(times=3, delay=1):
def deco(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception:
if i == times - 1: raise
time.sleep(delay)
return wrapper
return deco
第 10 章爬虫直接用它。三个例子一个结论:装饰器把「横切关注点」(计时、缓存、重试、鉴权)从业务代码里剥离——软件工程课第 5 章(AOP 思想)的 Python 实现。
魔法方法(dunder methods)是双下划线包裹的钩子,语言语法在背后调它们。第 6 章见过 __init__,这里补全家常用款:
class Vector:
def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): # repr(v) 与 REPL 显示(print 调 __str__)
return f"Vector({self.x!r}, {self.y!r})"
def __add__(self, other): # v1 + v2 → 调这里
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other): # v1 == v2
return (self.x, self.y) == (other.x, other.y)
def __len__(self): # len(v)
return 2
def __getitem__(self, i): # v[0]、v[1](还能切片)
return (self.x, self.y)[i]
def __iter__(self): # for x in v(第 8 章协议)
yield from (self.x, self.y)
v = Vector(1, 2) + Vector(10, 20) # Vector(11, 22):运算符重载!
运算符重载让自定义类型自然融入语法(v1 + v2 而不是 v1.add(v2)),Python 的 str/数字运算全靠它。纪律:重载要符合直觉(+ 就该是「相加」,别用 + 干「合并用户」这种怪事;成语原则:像内建类型一样行事)。
几个高频成员:__repr__ vs __str__(repr 给开发者看(精确),str 给用户看(好看),只写一个就写 repr)、__hash__ 与 __eq__(要当字典键必须成对重写,Java 课第 8 章同款契约)、__enter__/__exit__(with 协议,第 5 章)、__call__(实例可调用,装饰器类的形态)。
dataclass(第 6 章)的真相此刻揭晓:它就是「自动生成 __init__/__repr__/__eq__ 等魔法方法」的装饰器——本章两个主题在标准库里合流。
Java 习惯「先写 getter/setter 以备将来」(第 7 章的封装),Python 说:先直接用属性,将来需要拦截时用 property 无缝升级:
class Circle:
def __init__(self, r):
self._r = r # 单下划线:内部约定
@property
def area(self): # 读:c.area 像属性,实际是函数
return 3.14159 * self._r ** 2
@property
def r(self): return self._r
@r.setter
def r(self, value): # 写:c.r = 5 会经过这里(可校验)
if value <= 0: raise ValueError("半径必须为正")
self._r = value
c = Circle(2)
print(c.area) # 12.57(没有括号!)
c.r = 5 # 触发 setter 校验
调用方代码零改动(还是 c.area),实现从「属性」换成「计算 + 校验」——封装(Java 课第 7 章)与简洁(Pythonic)的两全。这就是「Python 不需要 getter/setter」的底气:升级路径已经铺好。
@count_calls 装饰器:统计函数被调用次数,提供 func.calls 查询;用它包装第 8 章的素数生成器内部函数,观察调用分布。@retry(times=3, delay=0.5)(9.2 完整版),构造一个「前两次失败第三次成功」的函数测试;打印每次重试的日志。t1 + t2(返回新对象)、t1 == t2、abs(t)(取摄氏绝对值)、bool(t)(零度以下 False);写测试验证所有语法糖。