Python描述符:__get__/__set__/__delete__,属性管理利器
描述符:优雅的字段校验解决方案 本文介绍了Python描述符在模型字段校验中的应用,对比了传统@property实现与描述符方式的优劣。核心要点: 问题背景:传统@property方式导致样板代码爆炸、校验逻辑重复、难以维护 描述符优势:通过实现__get__/__set__/__delete__方法,实现声明式字段校验 工作机制:访问或赋值属性时自动触发描述符方法,完成类型检查和值验证 实践应用
描述符:__get__/__set__/__delete__,属性管理利器
——一个老架构师的“别再用 property 堆屎山”血泪忠告:在 KES 模型层里,不懂描述符 = 字段校验逻辑散落 100 个地方!
开场白:你的模型字段还在靠 @property 手动校验?
看看你项目里的这些代码:
class User:
def __init__(self, name, email, age):
self._name = name
self._email = email
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 150:
raise ValueError("年龄必须在 0-150 之间")
self._age = value
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" not in value:
raise ValueError("邮箱格式无效")
self._email = value
# 还有 phone、id_card、amount... 每个字段都要写一套!
问题在哪?
- 样板代码爆炸(每个字段都要写 getter/setter)
- 校验逻辑重复(邮箱校验在 N 个模型里复制粘贴)
- 无法复用(想加新校验规则?改所有模型!)
这不是数据模型,这是在给维护团队挖坑。
在对接 电科金仓 KingbaseES(KES) 这种用于 金融交易、政务系统 的企业级数据库时,字段校验不是“可选项”,而是数据质量的生命线。
今天,咱们就用 描述符(Descriptor) 打造一套 声明式字段校验体系,让模型类 干净得像一张白纸。
一、描述符是什么?先看一个“魔法”示例
没有描述符的世界
class User:
def __init__(self):
self._age = None
@property
def age(self):
return self._age
@age.setter
def age(self, value):
# 校验逻辑...
self._age = value
有描述符的世界
class PositiveInt:
"""正整数描述符"""
def __set_name__(self, owner, name):
self.name = name # 自动获取字段名
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, int):
raise TypeError(f"{self.name} 必须是整数")
if value <= 0:
raise ValueError(f"{self.name} 必须是正数")
obj.__dict__[self.name] = value
class User:
age = PositiveInt() # ← 声明即校验!
user = User()
user.age = 25 # OK
user.age = -5 # ValueError: age 必须是正数
✅ 关键点:
- 描述符是实现了
__get__/__set__/__delete__的类 - 当访问
user.age时,自动调用PositiveInt.__get__ - 当赋值
user.age = 25时,自动调用PositiveInt.__set__
二、描述符三剑客:__get__、__set__、__delete__
| 方法 | 触发时机 | 参数说明 |
|---|---|---|
__get__(self, obj, objtype) |
user.age |
obj=实例, objtype=类 |
__set__(self, obj, value) |
user.age = 25 |
obj=实例, value=新值 |
__delete__(self, obj) |
del user.age |
obj=实例 |
完整的描述符生命周期
class Field:
def __set_name__(self, owner, name):
print(f"字段 {name} 被绑定到 {owner.__name__}")
self.name = name
def __get__(self, obj, objtype=None):
print(f"获取 {self.name} 的值")
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
print(f"设置 {self.name} = {value}")
obj.__dict__[self.name] = value
def __delete__(self, obj):
print(f"删除 {self.name}")
obj.__dict__.pop(self.name, None)
class User:
name = Field()
user = User()
user.name = "Alice" # 设置 name = Alice
print(user.name) # 获取 name 的值 → Alice
del user.name # 删除 name
三、实战:打造 KES 模型字段描述符体系
场景:不同字段需要不同校验规则
# kes_fields.py
class BaseField:
"""基础字段描述符"""
def __set_name__(self, owner, name):
self.name = name
self.owner = owner
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
validated_value = self.validate(value)
obj.__dict__[self.name] = validated_value
def validate(self, value):
"""子类重写此方法"""
return value
class StringField(BaseField):
"""字符串字段"""
def __init__(self, max_length=None, required=True):
self.max_length = max_length
self.required = required
def validate(self, value):
if self.required and value is None:
raise ValueError(f"{self.name} 是必填字段")
if value is None:
return None
if not isinstance(value, str):
raise TypeError(f"{self.name} 必须是字符串")
if self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name} 长度不能超过 {self.max_length}")
return value
class EmailField(StringField):
"""邮箱字段"""
def validate(self, value):
value = super().validate(value)
if value and "@" not in value:
raise ValueError(f"{self.name} 必须是有效的邮箱地址")
return value
class PositiveIntField(BaseField):
"""正整数字段"""
def __init__(self, required=True):
self.required = required
def validate(self, value):
if self.required and value is None:
raise ValueError(f"{self.name} 是必填字段")
if value is None:
return None
if not isinstance(value, int):
raise TypeError(f"{self.name} 必须是整数")
if value <= 0:
raise ValueError(f"{self.name} 必须是正数")
return value
class DecimalField(BaseField):
"""金额字段(适配电科金仓 DECIMAL 类型)"""
def __init__(self, max_digits=10, decimal_places=2, required=True):
self.max_digits = max_digits
self.decimal_places = decimal_places
self.required = required
def validate(self, value):
if self.required and value is None:
raise ValueError(f"{self.name} 是必填字段")
if value is None:
return None
if not isinstance(value, (int, float, str)):
raise TypeError(f"{self.name} 必须是数字或数字字符串")
try:
from decimal import Decimal
decimal_value = Decimal(str(value))
# 检查精度
if decimal_value.as_tuple().exponent < -self.decimal_places:
raise ValueError(f"{self.name} 小数位数不能超过 {self.decimal_places}")
return decimal_value
except Exception as e:
raise ValueError(f"{self.name} 无法转换为有效金额: {e}")
📌 驱动提示:使用最新版电科金仓官方驱动以获得最佳 DECIMAL 类型支持
👉 https://www.kingbase.com.cn/download.html#drive
四、使用:模型类干净得像一张白纸!
定义 KES 模型(只需声明字段类型)
# models.py
from kes_fields import *
class User:
id = PositiveIntField()
name = StringField(max_length=100)
email = EmailField(max_length=255)
age = PositiveIntField(required=False)
class Order:
id = PositiveIntField()
user_id = PositiveIntField()
amount = DecimalField(max_digits=12, decimal_places=2)
status = StringField(max_length=20)
# 使用
user = User()
user.id = 1
user.name = "Alice"
user.email = "alice@example.com"
user.age = 30
# 自动校验!
try:
user.age = -5 # ValueError: age 必须是正数
except ValueError as e:
print(e)
try:
user.email = "invalid-email" # ValueError: email 必须是有效的邮箱地址
except ValueError as e:
print(e)
order = Order()
order.amount = "99.99" # 自动转为 Decimal('99.99')
print(order.amount) # 99.99
✅ 优势:
- 零样板代码(模型类只有字段声明)
- 统一校验逻辑(所有校验在描述符中)
- 易于扩展(加新字段类型只需写新描述符)
五、进阶:描述符 + 元类 = 完整 ORM
自动注册字段信息(用于生成 SQL)
class KESMeta(type):
def __new__(mcs, name, bases, attrs):
# 收集所有描述符字段
fields = {}
for key, value in list(attrs.items()):
if isinstance(value, BaseField):
fields[key] = value
# 移除描述符,避免实例属性冲突
del attrs[key]
# 注入字段信息
attrs["_kes_fields"] = fields
# 生成 __init__
attrs["__init__"] = mcs._make_init(fields)
return super().__new__(mcs, name, bases, attrs)
@classmethod
def _make_init(cls, fields):
def __init__(self, **kwargs):
for field_name, field_obj in fields.items():
if field_name in kwargs:
setattr(self, field_name, kwargs[field_name])
elif field_obj.required:
raise ValueError(f"缺少必填字段: {field_name}")
return __init__
class BaseModel(metaclass=KESMeta):
pass
# 使用
class User(BaseModel):
id = PositiveIntField()
name = StringField(max_length=100)
email = EmailField()
user = User(id=1, name="Alice", email="alice@example.com")
了解 KES 数据类型映射:https://kingbase.com.cn/product/details_549_476.html
六、避坑指南:描述符的 3 个致命陷阱
❌ 陷阱1:描述符必须定义在类级别
# 错误:描述符不能在实例中定义
class User:
def __init__(self):
self.age = PositiveIntField() # ← 这不是描述符!
# 正确:必须在类级别定义
class User:
age = PositiveIntField() # ← 这才是描述符
❌ 陷阱2:__get__ 必须处理 obj is None
# 错误:当通过类访问时会出错
class BadField:
def __get__(self, obj, objtype):
return obj.value # 如果 obj is None,这里会报错!
# 正确:处理类访问
class GoodField:
def __get__(self, obj, objtype):
if obj is None:
return self # 返回描述符本身
return obj.__dict__.get(self.name)
❌ 陷阱3:描述符会覆盖实例属性
class User:
name = StringField()
user = User()
user.name = "Alice"
user.__dict__["name"] = "Bob" # 绕过描述符!
print(user.name) # Bob(但校验被绕过了!)
✅ 解法:始终通过描述符访问,不要直接操作 __dict__
七、特别提醒:电科金仓场景下的最佳实践
-
KES 数据类型精准映射
在 金融核心系统 中,金额、时间、大整数 等字段必须精确映射到 KES 的对应类型。
用描述符确保 Python 类型与 KES 类型严格匹配。 -
生产环境建议:
- 必填字段校验(避免 NULL 插入)
- 长度限制(匹配 KES 表结构)
- 敏感字段加密(如身份证号)
-
扩展性设计:
# 支持 KES 特有类型 class GeometryField(BaseField): """GIS 字段(适配电科金仓 PostGIS 扩展)""" def validate(self, value): # 校验 WKT/WKB 格式 pass class User(BaseModel): location = GeometryField() # 自动支持 GIS 查询
结语:描述符,是 Python 的“属性守门人”
在电科金仓支撑的核心系统里,描述符不是“高级特性”,而是构建健壮数据模型的基石。
记住三条铁律:
- 描述符必须定义在类级别
__get__必须处理obj is None- 校验逻辑集中管理,不在模型中重复
下次写 @property 前,问自己:
“这个校验逻辑能不能做成描述符?”
如果答案是肯定的——
用描述符,让属性自己守护自己的边界。
作者:一个坚信“数据质量始于字段校验”的技术架构师
环境:Python 3.10 + ksycopg2 + 电科金仓 KES V9R1(支撑多个省级金融核心系统)
注:所有代码均来自生产实践,拒绝“玩具示例”!✅
更多推荐



所有评论(0)