Python 装饰器与元编程实战
装饰器是 Python 最优雅也最容易被误解的特性之一。它本质上是一个接收函数并返回新函数的高阶函数,却能让你在不修改原代码的前提下给函数加上日志、缓存、权限校验等横切逻辑。本文从最基础的函数装饰器讲起,逐步深入到带参数装饰器、类装饰器,最后触及元编程与元类的核心机制。
一、装饰器到底是什么
很多人第一次见到 @decorator 语法时会觉得它是某种魔法。其实它只是语法糖:@decorator 写在函数定义上方,等价于在函数定义之后执行 func = decorator(func)。也就是说,装饰器接收原函数作为参数,返回一个新函数替换掉原函数。
下面这段代码用最直白的方式展示了装饰器的等价关系:
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__} 前")
result = func(*args, **kwargs)
print(f"调用 {func.__name__} 后")
return result
return wrapper
@my_decorator
def greet(name):
return f"你好,{name}"
# 等价于:
# greet = my_decorator(greet)
print(greet("世界"))
# 输出:
# 调用 greet 前
# 调用 greet 后
# 你好,世界
关键点在于 wrapper 函数用 *args, **kwargs 接收任意参数,再透传给原函数,这样装饰器就可以套在任何签名的函数上而不会报错。
greet.__name__ 会变成 "wrapper" 而不是 "greet"。用 functools.wraps 装饰 wrapper 就能保留原函数元信息,这在调试和生成文档时非常重要。
二、带参数的装饰器
当你需要给装饰器本身传参数时(比如 @repeat(times=3)),就需要再包一层。原理是:@repeat(times=3) 先执行 repeat(times=3),它返回一个真正的装饰器,然后这个装饰器再去装饰目标函数。
import functools
def repeat(times=2):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("嗨")
say_hi()
# 打印三行「嗨」
三层嵌套结构从外到内分别是:参数层、装饰器层、包装层。理解了这个三层结构,你就掌握了 Python 装饰器的核心模式。
三、实际场景:日志与缓存装饰器
装饰器最常见的用途是横切关注点。下面是两个可以直接用的实战例子。
日志装饰器:自动记录函数调用的参数和耗时,不需要在每个函数里手写 print。
import time, functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[{func.__name__}] 参数={args} 耗时={elapsed:.4f}s")
return result
return wrapper
@log_call
def fetch_data(url):
time.sleep(0.1)
return {"url": url, "data": "ok"}
fetch_data("https://api.example.com")
# [fetch_data] 参数=('https://api.example.com',) 耗时=0.1002s
缓存装饰器:functools.lru_cache 是 Python 内置的缓存装饰器,对于计算密集且参数重复的函数非常有效。
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(50)) # 极快返回,因为之前的调用结果被缓存了
print(fib.cache_info())
# CacheInfo(hits=49, misses=51, maxsize=128, currsize=51)
四、类装饰器
装饰器不仅能装饰函数,还能装饰类。类装饰器接收一个类作为参数,返回一个新类替换原类。它常用于给类统一添加日志、单例模式、属性校验等行为。
import functools
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 User:
def __init__(self, name, age):
self.name = name
self.age = age
print(User("小明", 25))
# User(name='小明', age=25)
类装饰器还可以用来实现单例模式:
def singleton(cls):
instances = {}
@functools.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 Config:
def __init__(self):
self.debug = True
c1 = Config()
c2 = Config()
print(c1 is c2) # True
五、元类初探:控制类的创建
元类是 Python 元编程的终极武器。普通的类是创建对象的模板,而元类是创建类的模板。你可以把它理解为:类是元类的实例。默认情况下 Python 用 type 作为所有类的元类,但你可以继承 type 来自定义类的创建过程。
class UpperAttrMeta(type):
def __new__(cls, name, bases, namespace):
# 把类中所有非下划线开头的属性名转成大写
new_namespace = {}
for key, value in namespace.items():
if not key.startswith("_"):
new_namespace[key.upper()] = value
else:
new_namespace[key] = value
return super().__new__(cls, name, bases, new_namespace)
class MyClass(metaclass=UpperAttrMeta):
name = "测试"
version = "1.0"
print(MyClass.NAME) # 测试
print(MyClass.VERSION) # 1.0
# MyClass.name 会报 AttributeError,因为属性名被大写了
元类最典型的应用场景是 ORM 框架(如 Django 的 Model):在类创建时自动把字段定义转换成数据库列映射。但日常业务开发中,大部分需求用装饰器就能解决,元类属于"知道就行、慎用"的高级特性。
六、常见坑与调试技巧
坑一:忘记 functools.wraps。这会导致所有被装饰函数的 __name__ 都变成 wrapper,调试时堆栈信息完全混乱。养成习惯,每个 wrapper 上面都加 @functools.wraps(func)。
坑二:装饰器在导入时就执行。因为 @decorator 在函数定义时就被调用了,如果你在装饰器里做了重活(连接数据库、发网络请求),它会在模块导入时立刻执行,拖慢启动。把重活放到 wrapper 内部。
坑三:堆叠装饰器的顺序。@a 在 @b 上面,等价于 func = a(b(func)),先执行 b 再执行 a。搞反顺序会导致日志输出不符合预期。
def a(func):
def w(*args, **kw):
print("a 前")
r = func(*args, **kw)
print("a 后")
return r
return w
def b(func):
def w(*args, **kw):
print("b 前")
r = func(*args, **kw)
print("b 后")
return r
return w
@a
@b
def hello():
print("hello")
hello()
# 输出:
# a 前
# b 前
# hello
# b 后
# a 后
七、总结
装饰器是 Python 里"小语法、大威力"的典型代表。一个 @ 符号背后,是高阶函数、闭包和语法糖的精巧组合。从给函数加日志,到用元类控制类的创建,理解装饰器的执行顺序和本质机制,你就能写出更优雅、更可维护的 Python 代码。记住三个原则:用 wraps 保留元信息、把重活放到 wrapper 里、能不用元类就不用元类。