Python装饰器与描述符在ORM中的实现

📅 2026/6/16 3:52:35 ✍️ 编辑团队 👁️ 阅读次数
Python装饰器与描述符在ORM中的实现
Python装饰器与描述符在ORM中的实现ORM框架大量使用描述符来实现字段的声明式定义。理解描述符在ORM中的实际用法能直接指导你写出更简洁的数据访问层。最基础的字段描述符class Field:def __init__(self, name, column_type):self.name nameself.column_type column_typeself.internal_name f_{name}def __get__(self, obj, objtypeNone):if obj is None:return selfreturn getattr(obj, self.internal_name, None)def __set__(self, obj, value):self._validate(value)object.__setattr__(obj, self.internal_name, value)def _validate(self, value):if not isinstance(value, self.column_type):raise TypeError(fExpected {self.column_type.__name__}, got {type(value).__name__})class IntegerField(Field):def __init__(self, name):super().__init__(name, int)class StringField(Field):def __init__(self, name):super().__init__(name, str)class User:id IntegerField(id)name StringField(name)def __init__(self, id, name):self.id idself.name nameu User(1, Alice)print(u.id) # 1print(u.name) # Alice但这个实现有个问题field_name参数重复了。在类定义中既写了变量名id IntegerField(...)又写了构造参数id。自动获取字段名需要元类的配合import inspectclass ModelMeta(type):def __new__(mcls, name, bases, namespace):annotations namespace.get(__annotations__, {})for attr_name, attr_type in annotations.items():if attr_type int:field IntegerField(attr_name)elif attr_type str:field StringField(attr_name)else:continuenamespace[attr_name] fieldreturn super().__new__(mcls, name, bases, namespace)class Base(metaclassModelMeta):passclass User(Base):id: intname: strdef __init__(self, id, name):self.id idself.name name使用类型注解替代重复的字段名声明避免了DRY原则的违反。真实情况更复杂因为ORM字段需要存储很多元数据表名、列名、是否为主键、是否有索引、默认值等。以SQLAlchemy风格的完整实现class Field:_registry {}def __init__(self, column_type, primary_keyFalse, defaultNone, nullableTrue, indexFalse):self.column_type column_typeself.primary_key primary_keyself.default defaultself.nullable nullableself.index indexself.name Noneself.model_class Nonedef contribute_to_class(self, model_class, name):self.name nameself.model_class model_classField._registry.setdefault(model_class, {})[name] selfdef __get__(self, obj, objtypeNone):if obj is None:return selfreturn obj.__dict__.get(self.name, self.default)def __set__(self, obj, value):if value is None and not self.nullable:raise ValueError(f{self.name} cannot be null)obj.__dict__[self.name] valueclass ModelMeta(type):def __new__(mcls, name, bases, namespace):if name Model:return super().__new__(mcls, name, bases, namespace)fields {}for attr_name, attr_value in list(namespace.items()):if isinstance(attr_value, Field):fields[attr_name] namespace.pop(attr_name)cls super().__new__(mcls, name, bases, namespace)cls._fields fieldscls._table_name name.lower()for field_name, field in fields.items():field.contribute_to_class(cls, field_name)return clsclass Model(metaclassModelMeta):def __init__(self, **kwargs):for field_name, field in self._fields.items():if field_name in kwargs:setattr(self, field_name, kwargs[field_name])elif field.default is not None:setattr(self, field_name, field.default() if callable(field.default) else field.default)elif not field.nullable:raise ValueError(f{field_name} is required)def save(self):columns []values []placeholders []for name, field in self._fields.items():value getattr(self, name)columns.append(name)values.append(value)placeholders.append(?)sql fINSERT INTO {self._table_name} ({, .join(columns)}) VALUES ({, .join(placeholders)})# execute_sql(sql, values)print(fSQL: {sql})print(fValues: {values})class IntegerField(Field):def __init__(self, **kwargs):super().__init__(int, **kwargs)class StringField(Field):def __init__(self, max_length255, **kwargs):super().__init__(str, **kwargs)self.max_length max_lengthdef __set__(self, obj, value):if value is not None and len(str(value)) self.max_length:raise ValueError(f{self.name} exceeds max_length of {self.max_length})super().__set__(obj, value)class User(Model):id IntegerField(primary_keyTrue)name StringField(max_length100)email StringField(nullableTrue)user User(id1, nameAlice, emailaliceexample.com)user.save()描述符的__set__方法拦截了属性赋值在OR-M中同时承担类型验证、长度检查和其他约束校验的职责。Field的contribute_to_class方法在元类创建类时调用确保字段知道它属于哪个类和叫什么名字。懒加载Lazy Loading是ORM的另一个关键特性也通过描述符实现class LazyField(Field):def __get__(self, obj, objtypeNone):if obj is None:return selfvalue obj.__dict__.get(self.name)if value is None and not self.nullable and self.name ! id:self._load(obj)return obj.__dict__.get(self.name)def _load(self, obj):# 从数据库加载关联数据print(f懒加载 {self.name} for {obj})obj.__dict__[self.name] self._fetch_from_db(obj)def _fetch_from_db(self, obj):# 模拟数据库查询return floaded_{self.name}_{obj.id}class Profile(Model):user_id IntegerField()bio LazyField(str, nullableTrue)p Profile(user_id1)print(p.bio) # 首次访问触发_DB查询外键关系同样由描述符管理class ForeignKey(Field):def __init__(self, to_model, **kwargs):super().__init__(int, **kwargs)self.to_model to_modelself._related_cache_attr Nonedef __get__(self, obj, objtypeNone):if obj is None:return selffk_value obj.__dict__.get(self.name)if fk_value is None:return Nonecache_key f_cached_{self.name}if cache_key in obj.__dict__:return obj.__dict__[cache_key]related_obj self._fetch_related(fk_value)obj.__dict__[cache_key] related_objreturn related_objdef __set__(self, obj, value):if isinstance(value, Model):obj.__dict__[self.name] value.idobj.__dict__[f_cached_{self.name}] valueelse:obj.__dict__[self.name] valuedef _fetch_related(self, pk):print(f查询关联表: {self.to_model._table_name} WHERE id {pk})# return self.to_model.objects.get(pkpk)return MockModel(pk)class MockModel:def __init__(self, pk):self.id pkclass Post(Model):id IntegerField(primary_keyTrue)title StringField(max_length200)author_id ForeignKey(User)propertydef author(self):return self.author_idpost Post(id1, titleHello)post.author_id User(id1, nameBob)print(post.author.name) # BobForeignkey的__get__实现了关联对象的自动加载和缓存。首次访问时检查__dict__中是否有缓存没有则发起数据库查询查询结果缓存起来后续访问直接返回缓存。__set__接受对象或主键值统一转换成内部存储格式。N1查询问题是ORM最有名的性能坑根源也在描述符的懒加载策略class Author(Model):id IntegerField(primary_keyTrue)name StringField(max_length100)class Book(Model):id IntegerField(primary_keyTrue)title StringField(max_length200)author_id ForeignKey(Author)# N1问题books [Book(idi, titlefBook {i}) for i in range(100)]for book in books:book.author_id i % 10print(book.author.name) # 每个book单独查询一次author这个循环执行了1次查询获取books 100次查询获取authors。加上预加载eager loading可以解决class PrefetchQuerySet:def __init__(self, model):self.model modeldef select_related(self, *fields):self._prefetch_fields fieldsreturn selfdef __iter__(self):for obj in self._fetch_all():for field_name in getattr(self, _prefetch_fields, []):self._prefetch_relation(obj, field_name)yield objdef _prefetch_relation(self, obj, field_name):field self.model._fields.get(field_name)if isinstance(field, ForeignKey):# 批量查询所有关联对象fk_values [getattr(o, field_name) for o in self._cache]related_objects self._batch_fetch(field.to_model, fk_values)for obj in self._cache:fk_value getattr(obj, field_name)related next((r for r in related_objects if r.id fk_value), None)obj.__dict__[f_cached_{field_name}] relateddef _batch_fetch(self, model, ids):ids list(set(ids))print(f批量查询: {model._table_name} WHERE id IN ({, .join(str(i) for i in ids)}))return [MockModel(i) for i in ids]def _fetch_all(self):self._cache [Book(idi, titlefBook {i}) for i in range(100)]return self._cachebooks_qs PrefetchQuerySet(Book).select_related(author_id)for book in books_qs:print(book.author.name) # 只执行1次批量查询select_related使用IN查询一次性获取所有关联对象填回每个对象的缓存。查询次数从1N降为11。描述符在ORM中的另一个重要场景是属性变更追踪。当字段值被修改时ORM需要知道哪些字段被改了以便生成UPDATE语句class TrackedField(IntegerField):def __set__(self, obj, value):original obj.__dict__.get(self.name)super().__set__(obj, value)if original is not None and original ! value:self._mark_dirty(obj)def _mark_dirty(self, obj):if _dirty_fields not in obj.__dict__:obj.__dict__[_dirty_fields] set()obj.__dict__[_dirty_fields].add(self.name)user User(id1, nameAlice)user.name Bobprint(user.__dict__.get(_dirty_fields)) # {name}Dirty tracking是ORM生成增量更新语句的基础。save方法检查_dirty_fields集合只生成被修改字段的update语句减少不必要的数据库写入。描述符与装饰器的结合使用在SQLAlchemy 2.0风格的映射中很常见def mapped_column(*, primary_keyFalse, nullableTrue, defaultNone):def decorator(func):field Field(int if primary_key else str)field.primary_key primary_keyfield.nullable nullablefield.default defaultfunc._field fieldreturn funcreturn decoratorclass DeclarativeMeta(type):def __new__(mcls, name, bases, namespace):cls super().__new__(mcls, name, bases, namespace)cls._fields {}for attr_name in dir(cls):attr getattr(cls, attr_name, None)if hasattr(attr, _field):field attr._fieldfield.name attr_namefield.model_class clscls._fields[attr_name] fieldsetattr(cls, attr_name, field)return clsclass BaseModel(metaclassDeclarativeMeta):passclass Product(BaseModel):mapped_column(primary_keyTrue)def id(self): passmapped_column(nullableFalse)def name(self): pass装饰器在类体中被执行创建一个Field对象并挂载到函数对象上。DeclarativeMeta在创建类时扫描所有带_field的函数将Field对象注册为类的描述符属性。这个模式结合了装饰器的声明式简洁性和描述符的属性拦截能力。