描述符:__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__


七、特别提醒:电科金仓场景下的最佳实践

  1. KES 数据类型精准映射
    金融核心系统 中,金额、时间、大整数 等字段必须精确映射到 KES 的对应类型。
    用描述符确保 Python 类型与 KES 类型严格匹配。

  2. 生产环境建议

    • 必填字段校验(避免 NULL 插入)
    • 长度限制(匹配 KES 表结构)
    • 敏感字段加密(如身份证号)
  3. 扩展性设计

    # 支持 KES 特有类型
    class GeometryField(BaseField):
        """GIS 字段(适配电科金仓 PostGIS 扩展)"""
        def validate(self, value):
            # 校验 WKT/WKB 格式
            pass
    
    class User(BaseModel):
        location = GeometryField()  # 自动支持 GIS 查询
    

结语:描述符,是 Python 的“属性守门人”

在电科金仓支撑的核心系统里,描述符不是“高级特性”,而是构建健壮数据模型的基石

记住三条铁律:

  1. 描述符必须定义在类级别
  2. __get__ 必须处理 obj is None
  3. 校验逻辑集中管理,不在模型中重复

下次写 @property 前,问自己:

“这个校验逻辑能不能做成描述符?”

如果答案是肯定的——
用描述符,让属性自己守护自己的边界


作者:一个坚信“数据质量始于字段校验”的技术架构师
环境:Python 3.10 + ksycopg2 + 电科金仓 KES V9R1(支撑多个省级金融核心系统)
注:所有代码均来自生产实践,拒绝“玩具示例”!✅

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐