文章
合集Python 核心与面向对象第 5 / 5 篇

Python 元编程:描述符、反射、动态类创建、元类与类装饰器

元编程(Metaprogramming)是指编写操作程序自身代码的技术,在运行时动态修改类和对象的行为。Python 的描述符、反射、动态类创建、元类和类装饰器都属于元编程的范畴。

描述符

描述符是一个实现了 __get__/__set__/__delete__ 协议的对象,可以接管另一个类的属性访问逻辑,是 property、classmethod、staticmethod 等内置功能的底层实现。

前置知识:python-oop-advanced → 「property」

一、前置知识

描述符是 Python 属性访问机制的底层钩子。要理解描述符,先搞清楚 Python 找到一个属性的完整步骤。

假设有:

class Person:
    age = 10          # 类属性

p = Person()
print(p.age)

Python 解释器会按以下顺序查找 p.age:

【Step 1】数据描述符(Data Descriptor)
         检查 type(p).age 即 Person.age 是不是一个"数据描述符"
         判断标准:该对象同时定义了 __get__ + __set__(或 __delete__)
         → 如果是,直接调用 Person.age.__get__(p, Person),查找结束
         → 优先级最高,实例字典说了不算

【Step 2】实例字典(instance.__dict__)
         检查 p.__dict__ 里有没有 'age'
         → 如果有,直接返回 p.__dict__['age'],查找结束

【Step 3】非数据描述符(Non-Data Descriptor)
         检查 Person.age 是不是一个"非数据描述符"
         判断标准:该对象只定义了 __get__,没有 __set__
         → 如果是,调用 Person.age.__get__(p, Person),查找结束

【Step 4】类字典与普通类属性
         检查 Person.__dict__ 里有没有 'age'
         → 如果有,直接返回(如普通的 10)

【Step 5】父类 MRO 链
         按 MRO 顺序去父类里重复 Step 1~4

【Step 6】__getattr__
         如果全部没找到,且类定义了 __getattr__(self, name),调用它兜底

二、定义

一个类实例(注意:是实例,不是类本身),如果作为另一个类的属性被挂载,且这个实例实现了以下任意一个方法,它就是描述符。

class MyDescriptor:
    def __get__(self, instance, owner): ...      # 读属性时触发
    def __set__(self, instance, value): ...      # 写属性时触发(可选)
    def __delete__(self, instance): ...          # del 时触发(可选)
    def __set_name__(self, owner, name): ...     # 类创建时触发(可选,3.6+)
  1. 描述符必须是一个类的实例。
  2. 这个实例必须被赋值给另一个类的类属性(不是实例属性)。
  3. 它必须实现至少 __get__、__set__、__delete__ 中的一个。

三、描述符协议

下面四个函数称为"描述符协议"(Descriptor Protocol),函数名是固定的(双下划线是 Python 的协议约定)。

1. __set_name__(self, owner, name)

格式:固定。必须是这个签名,不能改参数名(Python 按位置传参)。

作用:自动告诉你 " 你被挂在哪个类的哪个属性名上 "。

class Validator:
    def __set_name__(self, owner, name):
        # owner: Person 类对象本身(不是实例)
        # name:  字符串,如 "age"
        self.name = name
        self.storage_name = f"_{name}"  # 构造 "_age"

class Person:
    age = Validator()   # ← 类创建时,Python 自动调用:
                        # Validator().__set_name__(Person, 'age')

2. __get__(self, instance, owner)

格式:固定签名。

参数:

  • self:描述符实例本身(就是 Validator() 这个对象)。
  • instance:访问者。如果用户写 p.age,instance 就是 p;如果用户写 Person.age(类访问),instance 是 None。
  • owner:宿主类。永远是 Person,不管是实例访问还是类访问。

返回值:任意。通常返回属性值;如果 instance is None,通常返回描述符自身(方便调试)。

def __get__(self, instance, owner):
    if instance is None:
        return self          # 类访问时返回描述符对象本身
    # 从实例字典中读取存储的值
    return instance.__dict__.get(self.storage_name)

3. __set__(self, instance, value)

格式:固定签名。

参数:

  • self:描述符实例本身。
  • instance:被赋值的实例。用户写 p.age = 25 时,instance 就是 p。
  • value:用户赋的值,这里是 25。

为什么它决定了 " 数据描述符 " 身份? 只要一个描述符实现了 __set__(或 __delete__),它就是数据描述符,在查找链中优先级高于实例字典。

def __set__(self, instance, value):
    # 校验逻辑...
    # 存储到实例字典,但用影子属性名避免递归
    instance.__dict__[self.storage_name] = value

4. __delete__(self, instance)

格式:固定签名。

参数:

  • self:描述符实例本身。
  • instance:被删除属性的实例。用户写 del p.age 时,instance 就是 p。

四、配套工具函数

函数作用和点语法的等价关系返回值
getattr(obj, 'name')读取属性obj.name属性值
getattr(obj, 'name', default)读取属性,找不到不报错obj.name(有 try/except 兜底)属性值或 default
setattr(obj, 'name', val)设置属性obj.name = valNone
hasattr(obj, 'name')检查有没有这个属性try: obj.name; except: FalseTrue / False
delattr(obj, 'name')删除属性del obj.nameNone

为什么描述符存数据时偏爱 __dict__ 而不是 setattr?

因为 setattr(instance, 'age', value) 如果 'age' 恰好是数据描述符,会再次触发 __set__,导致递归。而 instance.__dict__[self.storage_name] = value 直接写底层字典,不触发任何描述符协议,是安全的。

五、描述符工作的完整流程图

【定义阶段】
你定义一个类,实现 __get__ / __set__ / __delete__ / __set_name__
    ↓
【挂载阶段】
将该类的实例赋值给另一个类的类属性(如 Person.age = MyDescriptor())
    ↓
Python 自动调用 __set_name__(Person, 'age'),描述符知道自己叫什么
    ↓
【访问阶段】用户写 p.age
    ↓
Python 按查找链检查:
    1. 是数据描述符?→ 调用 __get__(p, Person),结束
    2. 实例 __dict__ 有?→ 直接返回(非数据描述符被短路)
    3. 是非数据描述符?→ 调用 __get__(p, Person),结束
    4. 普通类属性?→ 直接返回
    5. 父类查找...
    6. __getattr__ 兜底
  • 函数名固定:必须是 __get__、__set__、__delete__、__set_name__,这是 Python 解释器硬编码的协议名。
  • 参数个数和顺序固定:解释器按位置传参,不能改。
  • 内部逻辑自定义:校验规则、存储方式、返回值,全部由你决定。

一个例子

假设你在写一个 Person 类,要求 age、height、weight 都必须在合理范围内:

class Person:
    def __init__(self, age, height, weight):
        # 校验 age
        if not (0 <= age <= 150):
            raise ValueError("age must be in [0, 150]")
        self._age = age
        
        # 校验 height(重复的逻辑,不同的变量)
        if not (0 <= height <= 300):
            raise ValueError("height must be in [0, 300]")
        self._height = height
        
        # 校验 weight(再次重复)
        if not (0 <= weight <= 500):
            raise ValueError("weight must be in [0, 500]")
        self._weight = weight

    # 为了外部能访问,还得写 property
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not (0 <= value <= 150):
            raise ValueError("age must be in [0, 150]")
        self._age = value

    # height、weight 的 getter/setter 还要再写两套……

问题在于:

  1. 校验逻辑重复:每个属性都是 if not (min <= x <= max),却要写 3 遍。
  2. 存储属性名混乱:_age、_height、_weight,一旦改名要改很多地方。
  3. 难以扩展:如果再加 salary、score,你得把这套模板再抄两遍。

描述符的本质是把属性的访问逻辑抽离成一个独立的、可复用的类。它不依附在某个属性上,而是一个独立的类实例,被挂载到类的属性上,拦截对该属性的所有访问。

class Range:
    """范围限制描述符:可复用到任意属性"""
    def __init__(self, min_val, max_val):
        self.min_val = min_val
        self.max_val = max_val
    
    def __set_name__(self, owner, name):
        # Python 3.6+ 特性:描述符被绑定到类属性时,自动拿到属性名
        self.name = name               # "age"
        self.storage_name = f"_{name}"  # 在实例上存储为 "_age"
    
    def __get__(self, instance, owner):
        # instance: 访问属性的那个实例(如 p = Person(); p.age 中的 p)
        # owner:    实例所属的类(Person 类本身)
        if instance is None:
            return self  # 如果是类访问(Person.age),返回描述符本身
        return getattr(instance, self.storage_name, None)
    
    def __set__(self, instance, value):
        # value: 用户赋的值
        if not (self.min_val <= value <= self.max_val):
            raise ValueError(f"{self.name} must be in [{self.min_val}, {self.max_val}]")
        setattr(instance, self.storage_name, value)


class Person:
    # 看这里:三个属性都挂上了同一个描述符类的不同实例
    age    = Range(0, 150)
    height = Range(0, 300)
    weight = Range(0, 500)
    
    def __init__(self, age, height, weight):
        # 赋值时会自动触发 Range.__set__
        self.age = age
        self.height = height
        self.weight = weight


# ==================== 使用 ====================
p = Person(age=25, height=175, weight=70)
print(p.age)        # 25  (触发 __get__)

p.age = 200         # ValueError: age must be in [0, 150] (触发 __set__,校验失败)

当你写 p.age = 25 时,Python 的查找顺序是:

  1. 发现 Person.age 是一个数据描述符(定义了 __set__)
  2. 直接调用 Person.age.__set__(p, 25),绕过实例的 __dict__
  3. 描述符内部把值存到 p._age

当你写 print(p.age) 时:

  1. 发现 Person.age 是数据描述符
  2. 调用 Person.age.__get__(p, Person)
  3. 返回 p._age

这就是描述符能"接管"属性访问的原因,它的优先级高于实例自己的 __dict__。

反射

反射(Reflection)是 Python 运行时探查和修改对象的能力。你不需要在写代码时知道对象有什么属性,程序跑起来后可以自己问、看、改。

getattr/setattr 在描述符中的使用见本篇「描述符」

一、痛点:不用反射,代码会写成什么样?

假设你要写一个通用序列化器,把任意对象转成字典:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Dog:
    def __init__(self, breed):
        self.breed = breed

# 不用反射:每个类都要写死
def serialize_person(obj):
    return {"name": obj.name, "age": obj.age}   # 只能处理 Person

def serialize_dog(obj):
    return {"breed": obj.breed}                  # 只能处理 Dog

问题很明显:来一个类就要写一个函数,永远无法通用。

用反射:运行时扫描对象有哪些属性,直接读取。

二、Python 反射的工具函数

这四个函数是属性反射的基础,你之前学描述符时见过,但当时是在描述符内部用。现在从外部使用者视角重新理解。

函数作用等价点语法返回值
hasattr(obj, 'name')问:你有这个属性吗?try: obj.name; except: FalseTrue / False
getattr(obj, 'name')读:给我这个属性的值obj.name属性值,或抛 AttributeError
getattr(obj, 'name', default)安全读:没有就返回默认值obj.name(有兜底)属性值或 default
setattr(obj, 'name', val)写:把这个值设给该属性obj.name = valNone
delattr(obj, 'name')删:删掉这个属性del obj.nameNone

区别:点语法(obj.name)的属性名必须是代码里写死的标识符;而这四个函数接收字符串,可以在运行时动态决定操作哪个属性。

三、完整示例:通用序列化器(反射实战)

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Dog:
    def __init__(self, breed, name):
        self.breed = breed
        self.name = name


def serialize(obj):
    """
    通用序列化:运行时扫描对象所有属性,不需要提前知道类定义
    """
    result = {}

    # dir(obj) 返回对象的所有属性名列表(字符串)
    for attr_name in dir(obj):
        # 过滤掉魔法方法和私有属性(双下划线开头)
        if attr_name.startswith('_'):
            continue

        # hasattr 确认不是方法/函数,而是数据属性
        # 或者直接用 getattr 取值,失败就跳过
        try:
            value = getattr(obj, attr_name)
            # 只序列化"数据"(不是函数/方法)
            # 用 callable 再次做一道保险(过滤方法与函数)
            if not callable(value):
                result[attr_name] = value
        except Exception:
            pass

    return result


# ========== 运行 ==========
p = Person("Tom", 25)
d = Dog("Golden", "Buddy")

print(f"Person 序列化: {serialize(p)}")
# {'name': 'Tom', 'age': 25}

print(f"Dog 序列化: {serialize(d)}")
# {'breed': 'Golden', 'name': 'Buddy'}

发生了什么?

  • dir(p) 运行时返回 ['age', 'name', '__class__', ...]
  • getattr(p, 'age') 运行时动态读取,不需要提前知道 Person 有 age
  • 同一个函数 serialize 通吃所有类,因为信息是运行时从对象身上"反射"出来的。

四、动态方法调用:根据字符串执行方法

这是反射在 Web 框架、RPC、命令模式中的常见用法。

class Calculator:
    def add(self, a, b):
        return a + b
    
    def sub(self, a, b):
        return a - b
    
    def mul(self, a, b):
        return a * b


def call_method(obj, method_name, *args):
    """
    根据字符串方法名,动态调用对象的方法
    """
    # 1. 先问:你有这个方法吗?
    if not hasattr(obj, method_name):
        raise AttributeError(f"{obj.__class__.__name__} 没有方法 '{method_name}'")
    
    # 2. 拿到方法对象(它是一个 bound method)
    method = getattr(obj, method_name)
    
    # 3. 确认它是可调用的
    if not callable(method):
        raise TypeError(f"'{method_name}' 不是方法")
    
    # 4. 执行
    return method(*args)


# ========== 运行 ==========
calc = Calculator()

# 根据用户输入/配置文件/网络请求中的字符串来调用
operations = [("add", 3, 5), ("sub", 10, 4), ("mul", 2, 6)]

for op_name, a, b in operations:
    result = call_method(calc, op_name, a, b)
    print(f"calc.{op_name}({a}, {b}) = {result}")

五、类型内省:type、isinstance、callable

反射也包括运行时检查对象是什么类型。

class A: pass
class B(A): pass

obj = B()

# 1. 对象是什么类型?
print(type(obj))           # <class '__main__.B'>
print(type(obj).__name__)  # 'B'(字符串,可用于日志/路由)

# 2. 是不是某个类型的实例?
print(isinstance(obj, B))  # True
print(isinstance(obj, A))  # True(继承链也算)

# 3. 能不能调用?
print(callable(obj))       # False(obj 本身不是函数)
print(callable(obj.__init__))  # True(方法可调用)

# 4. 类之间的关系
print(issubclass(B, A))    # True

六、inspect 模块:更深入的反射

当内置的四个函数不够用时,inspect 提供签名级别的反射能力。

import inspect

class MyClass:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age
    
    def greet(self, msg="Hello"):
        return f"{msg}, {self.name}"

# 1. 看类有哪些方法
print(inspect.getmembers(MyClass, predicate=inspect.isfunction))

# 2. 看方法的参数签名
sig = inspect.signature(MyClass.greet)
print(sig)   # (self, msg='Hello')
for name, param in sig.parameters.items():
    print(f"  参数 {name}: 默认值={param.default}")

# 3. 看对象的源码(仅限 .py 文件,交互式不行)
print(inspect.getsource(MyClass.greet))

应用场景:

  • Web 框架(Flask/FastAPI):自动从函数签名提取参数名、类型注解,生成 API 文档。
  • 依赖注入:根据 __init__ 的参数列表,自动装配对象。
  • ORM:根据模型字段类型,自动生成数据库列。

动态创建类

type 是所有类的元类,可以用它在运行时动态创建类:

# class 语法创建
class Dog:
    species = "Canis"
    def bark(self): print("汪汪")

# 等价的动态创建
def bark(self): print("汪汪")

Dog = type(
    "Dog",                        # 类名
    (object,),                    # 父类元组
    {"species": "Canis", "bark": bark}  # 属性和方法字典
)

d = Dog()
d.bark()   # 汪汪

应用:ORM 字段自动化

def make_model(name, fields: dict):
    """根据字段描述动态生成数据模型类"""
    def __init__(self, **kwargs):
        for field in fields:
            setattr(self, field, kwargs.get(field))

    def __repr__(self):
        attrs = ", ".join(f"{k}={getattr(self, k)!r}" for k in fields)
        return f"{name}({attrs})"

    return type(name, (object,), {"__init__": __init__, "__repr__": __repr__})

User = make_model("User", {"name": str, "age": int})
u = User(name="Alice", age=20)
print(u)   # User(name='Alice', age=20)

元类

元类(Metaclass)一句话:

元类是 type 的子类,控制"类"这个对象是如何被生产出来的。

你之前已经知道 type() 能动态创建类。元类就是:把 type() 这个内置工厂,换成你自己改装过的版本。

层级类比作用
实例一辆具体的车my_car = Car()
类造车工厂class Car 定义了怎么造车
元类造工厂的设备控制工厂本身的建造规则

自定义元类

# 1. 定义一个元类:继承自 type
class AutoTag(type):
    def __new__(cls, name, bases, namespace):
        # name: 类名(字符串)
        # namespace: 类体里的所有属性和方法(字典)
        
        # 在类出生之前,强行给它塞一个属性
        namespace["label"] = f"auto-{name}"
        
        # 调用真正的 type 来完成类的创建
        return super().__new__(cls, name, bases, namespace)

# 2. 使用元类
class Dog(metaclass=AutoTag):
    pass

# 3. 结果:Dog 类天生自带了 label
print(Dog.label)  # auto-Dog

d = Dog()
print(d.label)    # auto-Dog

发生了什么?

  • 解释器读到 class Dog(...) 时,发现指定了 metaclass=AutoTag
  • 于是它不直接调用 type(),而是调用 AutoTag.__new__()
  • 在类真正诞生之前,我们在字典里塞了一个 label
  • 最终 Dog 类自带了 label 属性

__init_subclass__(Python 3.6+,推荐替代方案)

大多数元类的使用场景(子类注册、属性约束)都可以用 __init_subclass__ 实现,避免元类冲突问题:

class Plugin:
    _registry = {}

    def __init_subclass__(cls, plugin_name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if plugin_name:
            Plugin._registry[plugin_name] = cls

class UpperPlugin(Plugin, plugin_name="upper"):
    def process(self, text): return text.upper()

class ReversePlugin(Plugin, plugin_name="reverse"):
    def process(self, text): return text[::-1]

print(Plugin._registry)
# {'upper': <class 'UpperPlugin'>, 'reverse': <class 'ReversePlugin'>}

# 使用
name = "upper"
plugin = Plugin._registry[name]()
print(plugin.process("hello"))   # HELLO

元类 vs __init_subclass__ vs 类装饰器:

技术作用时机适用场景
类装饰器类创建后修改单个类的行为
__init_subclass__子类定义时子类注册、属性约束(推荐优先考虑)
元类类创建时(最早)需要深度干预类创建过程的复杂场景

实际应用:Django ORM 的 Model(用元类自动收集字段)、ABC(用元类实现抽象方法检查)。

__init_subclass__ 是元类的轻量替代,ABC 的抽象检查也依赖元类

类装饰器

一、装饰器是什么

想象你有一个普通的灯泡(原始函数),现在你想给它加一个灯罩(附加功能),同时灯泡本身还能正常发光。

装饰器 = 不改变灯泡内部构造,在外面套一个壳,让它拥有额外功能。

二、前置知识:函数是"一等公民"

在 Python 里,函数就是普通的对象,可以像整数、字符串一样被传来传去:

def greet():
    return "Hello"

# 1. 函数可以赋值给变量
say = greet
print(say())  # Hello

# 2. 函数可以当参数传给另一个函数
def execute(func):
    return func()

print(execute(greet))  # Hello

# 3. 函数可以作为另一个函数的返回值
def outer():
    def inner():
        return "inner"
    return inner

result = outer()
print(result())  # inner

三、装饰器的核心原理(不用 @ 语法)

先忘掉 @,看最原始的手动版本。假设你有一个简单的函数:

def add(a, b):
    return a + b

现在你想在不修改 add 代码的前提下,让它每次执行前打印一句"开始计算":

def log_wrapper(func):
    # 定义一个内部函数,负责"包装"原函数
    def inner(a, b):
        print(f"开始执行 {func.__name__}")
        result = func(a, b)  # 调用真正的 add
        print(f"执行完毕,结果是 {result}")
        return result
    return inner  # 返回这个包装好的函数

手动使用装饰器:

# 把 add 丢进 log_wrapper,得到一个"增强版"的 add
add = log_wrapper(add)

# 现在调用 add,实际上走的是 inner()
print(add(1, 2))

输出:

开始执行 add
执行完毕,结果是 3
3

发生了什么?

  1. log_wrapper 接收了原来的 add 函数
  2. 它内部定义了一个新函数 inner,在调用 add 前后加了日志
  3. 返回这个 inner
  4. 我们用返回的 inner 覆盖了原来的 add 名字

所以 add(1, 2) 实际上调用的是 inner(1, 2),而 inner 内部又调用了真正的 add。

四、@ 语法糖:简化写法

上面的 add = log_wrapper(add) 写起来啰嗦。Python 提供 @ 符号作为简写:

@log_wrapper
def add(a, b):
    return a + b

# 完全等价于:add = log_wrapper(add)

@decorator 放在函数定义前面,就是自动帮你做 函数 = 装饰器(函数) 这件事。

五、一个更实用的例子:计时器

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        cost = time.time() - start
        print(f"{func.__name__} 耗时: {cost:.4f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.5)
    return "done"

slow_function()

这里用了 *args, **kwargs,为了让包装函数能接收任意参数,不管被装饰的函数签名是什么。

六、装饰器的本质

@decorator
def func():
    pass

等价于:

def func():
    pass
func = decorator(func)

三步:

  1. 装饰器本身是一个函数(接收函数作为参数)
  2. 装饰器内部定义一个新函数(在调用原函数前后加料)
  3. 装饰器返回这个新函数,替换原来的名字

七、类装饰器

基于刚才函数装饰器的基础,类装饰器就一句话:函数装饰器接收函数、返回函数;类装饰器接收类、返回类。

def 装饰器(cls):
    # cls 就是被装饰的那个类
    cls.新属性 = "xxx"      # 给类加点东西
    return cls              # 返回这个类(或一个新类)

@装饰器
class Dog:
    pass

print(Dog.新属性)  # xxx

@ 放在 class 前面时,解释器做的是 Dog = 装饰器(Dog)。

这和 @timer 放在 def 前面完全对称,只是把函数换成了类。

八、一个应用:单例模式

def singleton(cls):
    instance = None
    
    def get_instance(*args, **kwargs):
        nonlocal instance
        if instance is None:
            instance = cls(*args, **kwargs)
        return instance
    
    # 把类的构造入口换成 get_instance
    cls.__new__ = lambda cls, *a, **k: get_instance(*a, **k)
    return cls

@singleton
class Database:
    def __init__(self):
        print("连接数据库")

db1 = Database()
db2 = Database()
print(db1 is db2)  # True,永远是同一个

函数装饰器的写法见 python-core-mechanisms → 「装饰器」

OOP 基础见 → python-oop-basics OOP 进阶见 → python-oop-advanced 设计模式见 → python-design-patterns