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+)
- 描述符必须是一个类的实例。
- 这个实例必须被赋值给另一个类的类属性(不是实例属性)。
- 它必须实现至少
__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 = val | None |
hasattr(obj, 'name') | 检查有没有这个属性 | try: obj.name; except: False | True / False |
delattr(obj, 'name') | 删除属性 | del obj.name | None |
为什么描述符存数据时偏爱 __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 还要再写两套……
问题在于:
- 校验逻辑重复:每个属性都是
if not (min <= x <= max),却要写 3 遍。 - 存储属性名混乱:
_age、_height、_weight,一旦改名要改很多地方。 - 难以扩展:如果再加
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 的查找顺序是:
- 发现
Person.age是一个数据描述符(定义了__set__) - 直接调用
Person.age.__set__(p, 25),绕过实例的__dict__ - 描述符内部把值存到
p._age
当你写 print(p.age) 时:
- 发现
Person.age是数据描述符 - 调用
Person.age.__get__(p, Person) - 返回
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: False | True / False |
getattr(obj, 'name') | 读:给我这个属性的值 | obj.name | 属性值,或抛 AttributeError |
getattr(obj, 'name', default) | 安全读:没有就返回默认值 | obj.name(有兜底) | 属性值或 default |
setattr(obj, 'name', val) | 写:把这个值设给该属性 | obj.name = val | None |
delattr(obj, 'name') | 删:删掉这个属性 | del obj.name | None |
区别:点语法(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
发生了什么?
log_wrapper接收了原来的add函数- 它内部定义了一个新函数
inner,在调用add前后加了日志 - 返回这个
inner - 我们用返回的
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)
三步:
- 装饰器本身是一个函数(接收函数作为参数)
- 装饰器内部定义一个新函数(在调用原函数前后加料)
- 装饰器返回这个新函数,替换原来的名字
七、类装饰器
基于刚才函数装饰器的基础,类装饰器就一句话:函数装饰器接收函数、返回函数;类装饰器接收类、返回类。
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