人人都会AI编程

28.1 元编程:元类、装饰器、描述符高级应用

更新时间:2026-07-12

元编程,简单来说,就是编写能够操作代码的代码——在运行时创建或修改类、函数、属性等语言构件。Python 在语言层面提供了三种核心元编程工具:装饰器(修改可调用对象行为)、描述符(控制属性访问)和元类(控制类的创建过程)。它们让我们能够在不侵入业务逻辑的前提下,横向地为系统注入通用能力,比如日志、缓存、权限校验、数据验证等。

这一节不再重复基础语法,而是聚焦这些工具在实际项目中真正值得使用的高级模式


装饰器的高级应用

装饰器本质上是一个接受函数并返回新函数的可调用对象。它的价值在于将横切关注点(如计时、日志、缓存、重试)从业务代码中剥离,让函数职责更单一。

1. 带参数的装饰器:实现可配置的行为

普通的装饰器是 @decorator,但如果需要在装饰时传入参数(比如重试次数、超时时间),就需要再包装一层

import functools
import time

def retry(max_attempts=3, delay=1):
    """让函数在失败时自动重试"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=5, delay=0.5)
def unstable_network_call(url):
    # 可能因网络波动而失败
    ...

@retry(max_attempts=5) 相当于 retry(max_attempts=5)(unstable_network_call)。这一层函数闭包让装饰器可以携带“配置参数”,在实际项目中非常常用。

2. 类装饰器:批量增强类的功能

装饰器也可以作用于类,常用于自动注册子类、添加属性、或统一修改类方法。例如,在一个插件系统中,可以用类装饰器自动注册所有插件:

# 类装饰器:自动注册到全局插件字典
plugins = {}

def register(name):
    def decorator(cls):
        plugins[name] = cls
        return cls
    return decorator

@register("csv_exporter")
class CSVExporter:
    def export(self, data): ...

@register("json_exporter")
class JSONExporter:
    def export(self, data): ...

相比通过元类来收集子类,类装饰器更轻量、意图更明显,是很多框架的常用手法(如 Flask 的路由 @app.route 本质上也是一种注册装饰器)。

3. 装饰器实现缓存与幂等性

利用 functools.lru_cache 可以轻松为纯函数添加缓存,但如果你需要更精细的控制(例如基于对象状态、带过期时间的缓存),可以自定义装饰器:

import time
from functools import wraps

def cache_with_timeout(seconds=60):
    """缓存函数结果,指定过期时间(生产环境可改用Redis)"""
    def decorator(func):
        cache = {}
        @wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            if key in cache:
                val, timestamp = cache[key]
                if time.time() - timestamp < seconds:
                    return val
            result = func(*args, **kwargs)
            cache[key] = (result, time.time())
            return result
        return wrapper
    return decorator

描述符的高级应用

描述符是实现了 getsetdelete 中任意一个方法的对象。Python 中最常见的描述符就是 @property,但描述符的真正威力在于复用属性访问逻辑,比如类型检查、延迟加载、数据验证。

1. 可复用的类型验证描述符

假设多个类都需要“必须为整数”的属性,我们可以创建一个描述符类,重复应用而不用在每个类里重复写 getter/setter:

class IntegerField:
    def __init__(self, name):
        self.name = name   # 属性名称,避免在 __set__ 中通过字典存储

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} must be an integer")
        instance.__dict__[self.name] = value

class Person:
    age = IntegerField("age")   # 复用验证逻辑

p = Person()
p.age = 30      # 正常
p.age = "30"    # 抛出 TypeError

注意,这里将值存储在实例的 dict 中,而不是描述符自身,避免不同实例共享数据。Django/Peewee 的 ORM 字段定义正是基于类似的描述符思想。

2. 惰性加载属性(Lazy Loading)

某些属性计算成本高,且可能用不上。我们可以用描述符实现“只计算一次,之后缓存”的惰性属性:

class LazyAttribute:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.func(instance)
        instance.__dict__[self.name] = value   # 缓存到实例字典,下次直接绕过描述符
        return value

class HeavyData:
    @LazyAttribute
    def computed_value(self):
        print("正在计算...")   # 只会输出一次
        return sum(range(1_000_000))

data = HeavyData()
print(data.computed_value)   # 触发计算
print(data.computed_value)   # 从 __dict__ 直接读取,无输出

相比 @cached_property(也是描述符实现),自己动手能加深理解,并能根据需求加入过期或失效逻辑。

3. 利用弱引用处理属性缓存

描述符缓存对象的常见坑是内存泄漏——如果属性持有对实例的强引用,会导致实例无法被垃圾回收。解决方法是在描述符内部使用 weakref

import weakref

class CachedAttribute:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__
        self.cache = weakref.WeakKeyDictionary()   # 实例作为弱引用键

    def __get__(self, instance, owner):
        if instance is None:
            return self
        if instance not in self.cache:
            self.cache[instance] = self.func(instance)
        return self.cache[instance]

元类的高级应用

元类的本质是类的类,它控制类的创建过程。绝大多数情况下你不需要元类,但当其他方案(类装饰器、继承)都不够优雅时,元类能以最隐蔽的方式自动修改类。

1. 注册所有子类(替代类装饰器的集中管理)

假设你需要构建一个命令系统,所有命令类应自动注册到字典中。如果命令很多,每个类手动加装饰器容易遗漏。此时用元类可以让规则变成“默认行为”:

class CommandMeta(type):
    commands = {}
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != "BaseCommand":   # 不注册基类
            mcs.commands[name.lower()] = cls
        return cls

class BaseCommand(metaclass=CommandMeta):
    def execute(self): ...

class CopyCommand(BaseCommand): ...
class MoveCommand(BaseCommand): ...

print(CommandMeta.commands)  # {'copycommand': <class ...>, 'movecommand': <class ...>}

Django 的 ORM 中 ModelBase 就是利用元类收集所有定义的模型字段并生成对应的 SQL 映射。

2. 自动添加方法或属性

如果有一批类需要统一的辅助方法(如 to_dictto_json),直接在元类的 new 中往 namespace 添加即可,避免子类重复定义。

class SerializableMeta(type):
    def __new__(mcs, name, bases, namespace):
        def to_dict(self):
            return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
        namespace.setdefault('to_dict', to_dict)
        return super().__new__(mcs, name, bases, namespace)

class MyModel(metaclass=SerializableMeta):
    name = 'example'
    value = 42

obj = MyModel()
print(obj.to_dict())   # {'name': 'example', 'value': 42}

3. 实现单例模式

元类可以让单例控制逻辑集中在 call 方法(即实例化时的拦截点)上,比在 new 或模块全局变量更优雅:

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class DatabasePool(metaclass=SingletonMeta):
    def __init__(self, url):
        self.url = url

这样 DatabasePool("url1") 无论调用多少次都返回同一个实例。


谨慎使用元编程

元编程能力强大,但也是一把双刃剑。过度使用装饰器可能导致“洋葱皮”般的调用栈,隐藏真实逻辑;滥用描述符会使属性访问行为难以预测;元类更是常常让代码变得晦涩。遵循以下原则可以趋利避害:

  • 首选普通函数、继承这些显式方式,只有在确实需要横切多个不相关模块时才考虑元编程。
  • 装饰器是元编程中最容易理解的手段,优先用它;描述符次之;元类是最后选项。
  • 一定要为装饰器包裹的函数加上 functools.wraps,否则函数签名和文档会丢失。
  • 编写描述符时尽量文档化其行为,让类的使用者清楚这个属性的特殊性。

合理的元编程可以让框架和库的接口变得极其简洁,让你的代码从“能用”走向“优雅”。理解这三种工具的内在原理,你就真正跨入了 Python 高阶开发者的行列。