Python元类与描述符协议详解

一、理解Python的类创建机制

在Python中,类本身也是对象。当解释器遇到 class 语句时,它会调用元类来创建类对象。默认的元类是 type。

# 这两种方式等价
class MyClass:
x = 10
def method(self):
return self.x

MyClass = type('MyClass', (), {'x': 10, 'method': lambda self: self.x})

type 的调用签名是 type(name, bases, namespace):
- name: 类名字符串
- bases: 基类元组
- namespace: 类的属性字典


二、元类基础

元类是"类的类",控制类的创建过程。自定义元类需要继承 type:

class MetaLogger(type):
def __new__(mcs, name, bases, namespace):
print(f"正在创建类: {name}")
print(f"基类: {bases}")
print(f"属性: {list(namespace.keys())}")
cls = super().__new__(mcs, name, bases, namespace)
return cls

def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
cls._created_at = __import__('time').time()

class Animal(metaclass=MetaLogger):
def speak(self):
pass

# 输出:
# 正在创建类: Animal
# 基类: ()
# 属性: ['__module__', '__qualname__', 'speak']

元类中的关键方法:
- __new__: 创建类对象(在类体执行完毕后调用)
- __init__: 初始化类对象
- __call__: 控制类的实例化过程


三、元类的实际应用

3.1 单例模式

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 Database(metaclass=SingletonMeta):
def __init__(self, host='localhost'):
self.host = host
print(f"连接到 {host}")

db1 = Database('server1') # 输出: 连接到 server1
db2 = Database('server2') # 不输出,返回已有实例
print(db1 is db2) # True

3.2 接口强制实现

class InterfaceMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)

# 跳过接口基类本身
if bases:
for base in bases:
required = getattr(base, '_required_methods', [])
for method_name in required:
if method_name not in namespace:
raise TypeError(
f"类 {name} 必须实现方法: {method_name}"
)
return cls

class Serializable(metaclass=InterfaceMeta):
_required_methods = ['serialize', 'deserialize']

class JSONData(Serializable):
def serialize(self):
return json.dumps(self.__dict__)

def deserialize(self, data):
self.__dict__.update(json.loads(data))

# 下面会抛出 TypeError: 类 BadData 必须实现方法: deserialize
# class BadData(Serializable):
# def serialize(self):
# pass

3.3 自动注册

class PluginRegistry(type):
plugins = {}

def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # 不注册基类
plugin_name = namespace.get('name', name.lower())
mcs.plugins[plugin_name] = cls
return cls

@classmethod
def get_plugin(mcs, name):
return mcs.plugins.get(name)

class Plugin(metaclass=PluginRegistry):
pass

class JSONPlugin(Plugin):
name = 'json'
def process(self, data):
return json.dumps(data)

class XMLPlugin(Plugin):
name = 'xml'
def process(self, data):
return to_xml(data)

# 通过名称获取插件
handler = PluginRegistry.get_plugin('json')()
result = handler.process({'key': 'value'})


四、__init_subclass__ — 轻量级替代方案

Python 3.6 引入了 __init_subclass__,提供了不使用元类就能钩入子类创建的方式:

class ValidatedModel:
def __init_subclass__(cls, table_name=None, **kwargs):
super().__init_subclass__(**kwargs)
cls._table_name = table_name or cls.__name__.lower()
cls._fields = {}

for key, value in cls.__annotations__.items():
if hasattr(value, '__origin__'): # 泛型类型
cls._fields[key] = value
else:
cls._fields[key] = value

def validate(self):
for field_name, field_type in self._fields.items():
value = getattr(self, field_name, None)
if value is not None and not isinstance(value, field_type):
raise TypeError(
f"{field_name} 应为 {field_type.__name__},"
f"实际为 {type(value).__name__}"
)

class User(ValidatedModel, table_name='users'):
name: str
age: int
email: str

user = User()
user.name = "Alice"
user.age = "not a number" # 类型错误
user.validate() # 抛出 TypeError

大多数场景下,__init_subclass__ 比元类更简单、更易理解。


五、描述符协议

描述符是实现了特定协议的对象,用于自定义属性访问行为。描述符协议包含:

- __get__(self, obj, objtype=None): 获取属性时调用
- __set__(self, obj, value): 设置属性时调用
- __delete__(self, obj): 删除属性时调用
- __set_name__(self, owner, name): 类创建时调用,获取属性名

根据实现的方法不同,描述符分为:
- 数据描述符:实现了 __get__ 和 __set__(或 __delete__)
- 非数据描述符:只实现了 __get__

数据描述符的优先级高于实例字典,非数据描述符的优先级低于实例字典。


六、描述符实战

6.1 类型验证描述符

class Typed:
def __init__(self, expected_type):
self.expected_type = expected_type

def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'

def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)

def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"{self.name} 必须是 {self.expected_type.__name__} 类型,"
f"得到 {type(value).__name__}"
)
setattr(obj, self.private_name, value)

class Point:
x = Typed(float)
y = Typed(float)

def __init__(self, x, y):
self.x = x
self.y = y

p = Point(1.0, 2.0) # 正常
p = Point(1, 2) # TypeError: x 必须是 float 类型

6.2 带范围验证的描述符

class Bounded:
def __init__(self, min_val=None, max_val=None):
self.min_val = min_val
self.max_val = max_val

def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'

def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)

def __set__(self, obj, value):
if self.min_val is not None and value < self.min_val:
raise ValueError(f"{self.name} 不能小于 {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"{self.name} 不能大于 {self.max_val}")
setattr(obj, self.private_name, value)

class Temperature:
celsius = Bounded(min_val=-273.15, max_val=1000)

def __init__(self, celsius):
self.celsius = celsius

@property
def fahrenheit(self):
return self.celsius * 9/5 + 32

t = Temperature(25) # 正常
t = Temperature(-300) # ValueError: celsius 不能小于 -273.15

6.3 惰性计算属性

class LazyProperty:
def __init__(self, func):
self.func = func

def __set_name__(self, owner, name):
self.name = name

def __get__(self, obj, objtype=None):
if obj is None:
return self
value = self.func(obj)
# 将计算结果存入实例字典,下次直接从字典获取
setattr(obj, self.name, value)
return value

class DataAnalyzer:
def __init__(self, data):
self.data = data

@LazyProperty
def statistics(self):
"""耗时计算,只在首次访问时执行"""
print("计算统计数据...")
return {
'mean': sum(self.data) / len(self.data),
'max': max(self.data),
'min': min(self.data),
'count': len(self.data)
}

analyzer = DataAnalyzer([1, 2, 3, 4, 5])
print(analyzer.statistics) # 输出"计算统计数据..."后返回结果
print(analyzer.statistics) # 直接返回缓存结果,不再计算

注意:Python 3.8+ 提供了 functools.cached_property 实现相同功能。

6.4 观察者模式描述符

class Observable:
def __init__(self, initial_value=None):
self.value = initial_value
self.callbacks = []

def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'

def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, self.value)

def __set__(self, obj, value):
old_value = getattr(obj, self.private_name, self.value)
setattr(obj, self.private_name, value)
for callback in self.callbacks:
callback(obj, self.name, old_value, value)

def on_change(self, callback):
self.callbacks.append(callback)
return callback

class Config:
debug_mode = Observable(False)
log_level = Observable('INFO')

def log_change(obj, attr, old, new):
print(f"配置变更: {attr} 从 {old!r} 改为 {new!r}")

Config.debug_mode.on_change(log_change)
Config.log_level.on_change(log_change)

config = Config()
config.debug_mode = True # 输出: 配置变更: debug_mode 从 False 改为 True
config.log_level = 'DEBUG' # 输出: 配置变更: log_level 从 'INFO' 改为 'DEBUG'


七、属性查找顺序

理解描述符需要掌握Python的属性查找顺序:

1. 数据描述符(类及其MRO中定义的,有 __get__ 和 __set__)
2. 实例的 __dict__
3. 非数据描述符和其他类属性

obj.attr 的查找过程:
type(obj).__mro__ 中查找 attr
-> 如果找到且是数据描述符 -> 调用 __get__
-> 否则检查 obj.__dict__['attr']
-> 否则如果类属性是非数据描述符 -> 调用 __get__
-> 否则返回类属性本身
-> 都没找到 -> 调用 __getattr__(如果定义了)
-> 抛出 AttributeError

这解释了为什么 property(数据描述符)能拦截属性赋值,而普通方法(非数据描述符)不会阻止同名实例属性的创建。


八、组合使用元类与描述符

class Field:
def __init__(self, field_type, required=True, default=None):
self.field_type = field_type
self.required = required
self.default = default

def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'

def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, self.default)

def __set__(self, obj, value):
if value is not None and not isinstance(value, self.field_type):
try:
value = self.field_type(value)
except (TypeError, ValueError):
raise TypeError(
f"{self.name}: 期望 {self.field_type.__name__},"
f"无法转换 {type(value).__name__}"
)
setattr(obj, self.private_name, value)


class ModelMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)

# 收集所有 Field 描述符
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
fields[key] = value
cls._fields = fields

return cls


class Model(metaclass=ModelMeta):
def __init__(self, **kwargs):
for name, field in self._fields.items():
if name in kwargs:
setattr(self, name, kwargs[name])
elif field.required and field.default is None:
raise ValueError(f"缺少必填字段: {name}")

def to_dict(self):
return {name: getattr(self, name) for name in self._fields}

def __repr__(self):
fields = ', '.join(f'{k}={v!r}' for k, v in self.to_dict().items())
return f"{self.__class__.__name__}({fields})"


class User(Model):
name = Field(str)
age = Field(int)
email = Field(str)
role = Field(str, required=False, default='user')

user = User(name='Alice', age=30, email='alice@example.com')
print(user) # User(name='Alice', age=30, email='alice@example.com', role='user')
print(user.role) # 'user'

user.age = '25' # 自动转换为 int
print(user.age) # 25


九、选择建议

何时使用元类:
- 需要在类创建时修改类的结构
- 需要自动注册所有子类
- 需要强制所有子类遵循某种约定
- 框架级别的抽象(ORM、序列化框架等)

何时使用描述符:
- 需要自定义单个属性的访问行为
- 多个类需要相同的属性验证逻辑
- 实现惰性计算、缓存、类型检查等属性级功能

何时使用 __init_subclass__:
- 需要在子类创建时执行简单的注册或验证
- 不需要完全控制类创建过程
- 希望代码更简单易懂

总结:元类和描述符是Python对象模型的高级特性,它们赋予开发者极大的灵活性来定制类的行为。在日常开发中,描述符比元类更常用,因为它解决的问题更具体。而元类通常出现在框架设计中,用于提供声明式的API。理解这些机制有助于读懂Django、SQLAlchemy等框架的源码,也能在需要时设计出同样优雅的抽象。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐