Python 描述符进阶:数据描述符和非数据描述符到底有什么区别?

Python 描述符进阶:数据描述符和非数据描述符到底有什么区别?

在 Python 编程中,很多高级特性看似神秘,其实都藏在对象模型的细节里。比如你每天都会写的:

python 复制代码
obj.name
obj.method()
obj.age = 18

这些再普通不过的属性访问,背后并不只是"从对象里拿一个值"这么简单。Python 会沿着一套清晰的属性查找规则,判断这个属性来自实例、类、父类,还是一个特殊对象。

这个特殊对象,就是描述符。

如果说装饰器让函数拥有了可扩展能力,元类让类的创建过程可以被定制,那么描述符协议则让属性访问拥有了"可编程"的能力。它是 property、实例方法、staticmethodclassmethod、ORM 字段、表单校验、缓存属性等机制背后的重要基础。

上一篇我们理解了什么是描述符协议,这一篇继续深入一个更关键的问题:

数据描述符和非数据描述符有什么区别?

这个问题看似细节,实际却是理解 Python 属性访问优先级的核心。很多框架的"魔法",都建立在这个区别之上。


一、先回顾:什么是描述符?

在 Python 中,只要一个对象实现了下面三个方法中的任意一个,它就可以被称为描述符:

python 复制代码
__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)

描述符通常作为类属性存在,用来控制实例属性的读取、赋值或删除。

python 复制代码
class SimpleDescriptor:
    def __get__(self, instance, owner):
        return "来自描述符的值"


class User:
    name = SimpleDescriptor()


u = User()
print(u.name)

输出:

python 复制代码
来自描述符的值

这里的 name 并不是普通字符串,而是一个描述符对象。当访问 u.name 时,Python 发现 User.name 实现了 __get__,于是调用它的 __get__ 方法。

描述符真正强大的地方在于:它不是简单保存数据,而是可以控制属性访问行为。


二、数据描述符与非数据描述符的定义

描述符分为两类:

  1. 数据描述符;
  2. 非数据描述符。

它们的区别非常简单,但影响非常深远。

1. 数据描述符

如果一个描述符实现了 __set____delete__ 方法,它就是数据描述符。通常它也会实现 __get__

python 复制代码
class DataDescriptor:
    def __get__(self, instance, owner):
        return "读取数据描述符"

    def __set__(self, instance, value):
        print("写入数据描述符")

这里 DataDescriptor 是数据描述符,因为它实现了 __set__

即使 __set__ 只是抛出异常,它仍然是数据描述符:

python 复制代码
class ReadOnly:
    def __get__(self, instance, owner):
        return "只读属性"

    def __set__(self, instance, value):
        raise AttributeError("该属性不可修改")

这也是 property 实现只读属性的常见方式。


2. 非数据描述符

如果一个描述符只实现了 __get__,没有实现 __set____delete__,它就是非数据描述符。

python 复制代码
class NonDataDescriptor:
    def __get__(self, instance, owner):
        return "读取非数据描述符"

非数据描述符只能控制读取行为,不能拦截赋值和删除。

Python 中非常典型的非数据描述符是普通函数。类中的方法之所以会自动绑定 self,就是因为函数对象实现了 __get__

python 复制代码
class User:
    def say_hello(self):
        print("hello")


u = User()

print(User.say_hello)
print(u.say_hello)

大致输出:

python 复制代码
<function User.say_hello at 0x...>
<bound method User.say_hello of <__main__.User object at 0x...>>

通过类访问时,它是普通函数;通过实例访问时,它变成了绑定方法。这背后就是非数据描述符在工作。


三、最核心区别:属性查找优先级不同

数据描述符和非数据描述符最大的区别,不在于方法数量,而在于它们和实例属性发生冲突时,谁优先。

当执行:

python 复制代码
obj.attr

Python 的属性查找顺序大致如下:

text 复制代码
数据描述符
    ↓
实例字典 obj.__dict__
    ↓
非数据描述符
    ↓
类属性
    ↓
__getattr__

这条规则非常重要,可以直接记下来。

它意味着:

  • 数据描述符优先级高于实例属性;
  • 非数据描述符优先级低于实例属性;
  • 实例属性可以覆盖非数据描述符;
  • 实例属性不能覆盖数据描述符。

我们通过代码来看。


四、代码实验:数据描述符不会被实例属性覆盖

python 复制代码
class DataDesc:
    def __get__(self, instance, owner):
        return "来自数据描述符"

    def __set__(self, instance, value):
        print(f"数据描述符拦截赋值:{value}")


class Demo:
    attr = DataDesc()


obj = Demo()

obj.__dict__["attr"] = "来自实例字典"

print(obj.__dict__)
print(obj.attr)

输出:

python 复制代码
{'attr': '来自实例字典'}
来自数据描述符

虽然实例字典里已经有了 attr,但访问 obj.attr 时,Python 仍然优先调用数据描述符的 __get__

这就是数据描述符的"强势"之处:只要类属性中存在同名数据描述符,它就会优先于实例属性。

再看赋值:

python 复制代码
obj.attr = 100

输出:

python 复制代码
数据描述符拦截赋值:100

这次赋值并没有直接写入 obj.__dict__,而是被 __set__ 拦截了。

这也是为什么 property 可以控制属性赋值:

python 复制代码
class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value <= 0:
            raise ValueError("价格必须大于 0")
        self._price = value

price 本质上就是一个数据描述符。


五、代码实验:非数据描述符会被实例属性覆盖

现在看非数据描述符。

python 复制代码
class NonDataDesc:
    def __get__(self, instance, owner):
        return "来自非数据描述符"


class Demo:
    attr = NonDataDesc()


obj = Demo()

print(obj.attr)

obj.__dict__["attr"] = "来自实例字典"

print(obj.attr)

输出:

python 复制代码
来自非数据描述符
来自实例字典

第一次访问时,实例字典里没有 attr,所以调用非数据描述符的 __get__

第二次访问时,实例字典里有了 attr,于是 Python 直接返回实例字典中的值,不再调用非数据描述符。

这就是非数据描述符的特点:它可以提供默认行为,但允许实例覆盖。

这个设计非常优雅,因为它让某些属性可以"第一次由类提供,后续由实例接管"。


六、对比总结:一张表看懂区别

对比维度 数据描述符 非数据描述符
必要方法 实现 __set____delete__ 只实现 __get__
是否能拦截读取 可以 可以
是否能拦截赋值 可以 不可以
是否能拦截删除 可以,如果实现 __delete__ 不可以
与实例属性冲突时 数据描述符优先 实例属性优先
常见例子 property、ORM 字段、校验字段 普通实例方法、缓存属性
适合场景 类型校验、只读属性、字段管理 延迟计算、默认绑定、可覆盖属性

最值得记住的是这一句:

数据描述符像"强规则",非数据描述符像"默认值"。


七、为什么实例方法是非数据描述符?

在 Python 中,函数对象作为类属性时,会表现为非数据描述符。

python 复制代码
class Person:
    def greet(self):
        return "hello"


p = Person()

print(p.greet())

当访问 p.greet 时,Python 实际上调用了函数对象的 __get__,返回一个绑定了实例 p 的方法对象。

可以用下面的伪代码理解:

python 复制代码
method = Person.__dict__["greet"].__get__(p, Person)
method()

由于普通函数是非数据描述符,所以它可以被实例属性覆盖。

python 复制代码
class Person:
    def greet(self):
        return "hello"


p = Person()

p.greet = lambda: "hi"

print(p.greet())

输出:

python 复制代码
hi

实例属性 p.greet 覆盖了类中的方法。这在某些测试、Mock、动态替换行为中很有用。

但也要谨慎使用,因为过度动态修改实例方法,会降低代码可读性。


八、实战案例一:用数据描述符实现字段校验

假设我们在做一个用户系统,需要保证:

  • 用户名必须是字符串;
  • 年龄必须是整数;
  • 年龄不能小于 0;
  • 分数必须在 0 到 100 之间。

如果把校验逻辑全部写在 __init__ 中,代码很快会变得臃肿。更好的方式是使用数据描述符。

python 复制代码
class IntegerField:
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.name = None

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

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} 必须是整数")

        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} 不能小于 {self.min_value}")

        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} 不能大于 {self.max_value}")

        instance.__dict__[self.name] = value

使用:

python 复制代码
class Student:
    age = IntegerField(min_value=0, max_value=150)
    score = IntegerField(min_value=0, max_value=100)

    def __init__(self, age, score):
        self.age = age
        self.score = score


s = Student(18, 95)

print(s.age)
print(s.score)

s.score = 120

最后一行会抛出异常:

python 复制代码
ValueError: score 不能大于 100

这里 IntegerField 是数据描述符,因为它实现了 __set__。任何对 agescore 的赋值都会被它拦截。

这个模式非常适合:

  • 表单字段校验;
  • 配置项校验;
  • ORM 模型字段;
  • API 入参对象;
  • 数据清洗流程。

九、实战案例二:用非数据描述符实现 cached_property

非数据描述符最经典的用途之一,是实现缓存属性。

有些属性计算成本较高,比如读取文件、统计数据、解析配置、执行复杂计算。我们希望第一次访问时计算,之后直接复用结果。

python 复制代码
class cached_property:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self

        print(f"计算 {self.name}")
        value = self.func(instance)

        instance.__dict__[self.name] = value

        return value

使用:

python 复制代码
class Report:
    def __init__(self, numbers):
        self.numbers = numbers

    @cached_property
    def total(self):
        return sum(self.numbers)


r = Report([1, 2, 3, 4, 5])

print(r.total)
print(r.total)
print(r.__dict__)

输出:

python 复制代码
计算 total
15
15
{'numbers': [1, 2, 3, 4, 5], 'total': 15}

为什么第二次没有打印"计算 total"?

因为 cached_property 只实现了 __get__,它是非数据描述符。

第一次访问时,描述符计算结果,并把结果写入:

python 复制代码
instance.__dict__["total"] = 15

第二次访问时,实例字典中的 total 优先级高于非数据描述符,所以 Python 直接返回缓存值。

这正是非数据描述符的巧妙应用:允许实例属性覆盖描述符结果。


十、如果 cached_property 是数据描述符会怎样?

我们稍微改一下,给它加上 __set__

python 复制代码
class BadCachedProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self

        print(f"计算 {self.name}")
        value = self.func(instance)
        instance.__dict__[self.name] = value
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

现在它变成了数据描述符。即使实例字典中已经存在同名属性,访问时仍然优先调用 __get__

结果可能是每次访问都重新计算,缓存效果被破坏。

这说明:描述符不是实现方法越多越好。你要根据需求选择数据描述符或非数据描述符。

如果你要强制控制赋值,选择数据描述符。

如果你希望实例可以覆盖结果,选择非数据描述符。


十一、属性查找流程图

可以用下面这个流程图理解:
#mermaid-svg-Y2S6FMLagfSCaFle{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Y2S6FMLagfSCaFle .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Y2S6FMLagfSCaFle .error-icon{fill:#552222;}#mermaid-svg-Y2S6FMLagfSCaFle .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Y2S6FMLagfSCaFle .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Y2S6FMLagfSCaFle .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Y2S6FMLagfSCaFle .marker.cross{stroke:#333333;}#mermaid-svg-Y2S6FMLagfSCaFle svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Y2S6FMLagfSCaFle p{margin:0;}#mermaid-svg-Y2S6FMLagfSCaFle .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Y2S6FMLagfSCaFle .cluster-label text{fill:#333;}#mermaid-svg-Y2S6FMLagfSCaFle .cluster-label span{color:#333;}#mermaid-svg-Y2S6FMLagfSCaFle .cluster-label span p{background-color:transparent;}#mermaid-svg-Y2S6FMLagfSCaFle .label text,#mermaid-svg-Y2S6FMLagfSCaFle span{fill:#333;color:#333;}#mermaid-svg-Y2S6FMLagfSCaFle .node rect,#mermaid-svg-Y2S6FMLagfSCaFle .node circle,#mermaid-svg-Y2S6FMLagfSCaFle .node ellipse,#mermaid-svg-Y2S6FMLagfSCaFle .node polygon,#mermaid-svg-Y2S6FMLagfSCaFle .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Y2S6FMLagfSCaFle .rough-node .label text,#mermaid-svg-Y2S6FMLagfSCaFle .node .label text,#mermaid-svg-Y2S6FMLagfSCaFle .image-shape .label,#mermaid-svg-Y2S6FMLagfSCaFle .icon-shape .label{text-anchor:middle;}#mermaid-svg-Y2S6FMLagfSCaFle .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Y2S6FMLagfSCaFle .rough-node .label,#mermaid-svg-Y2S6FMLagfSCaFle .node .label,#mermaid-svg-Y2S6FMLagfSCaFle .image-shape .label,#mermaid-svg-Y2S6FMLagfSCaFle .icon-shape .label{text-align:center;}#mermaid-svg-Y2S6FMLagfSCaFle .node.clickable{cursor:pointer;}#mermaid-svg-Y2S6FMLagfSCaFle .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Y2S6FMLagfSCaFle .arrowheadPath{fill:#333333;}#mermaid-svg-Y2S6FMLagfSCaFle .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Y2S6FMLagfSCaFle .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Y2S6FMLagfSCaFle .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y2S6FMLagfSCaFle .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Y2S6FMLagfSCaFle .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y2S6FMLagfSCaFle .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Y2S6FMLagfSCaFle .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Y2S6FMLagfSCaFle .cluster text{fill:#333;}#mermaid-svg-Y2S6FMLagfSCaFle .cluster span{color:#333;}#mermaid-svg-Y2S6FMLagfSCaFle div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Y2S6FMLagfSCaFle .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Y2S6FMLagfSCaFle rect.text{fill:none;stroke-width:0;}#mermaid-svg-Y2S6FMLagfSCaFle .icon-shape,#mermaid-svg-Y2S6FMLagfSCaFle .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y2S6FMLagfSCaFle .icon-shape p,#mermaid-svg-Y2S6FMLagfSCaFle .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Y2S6FMLagfSCaFle .icon-shape .label rect,#mermaid-svg-Y2S6FMLagfSCaFle .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y2S6FMLagfSCaFle .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Y2S6FMLagfSCaFle .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Y2S6FMLagfSCaFle :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是







访问 obj.attr
类或父类中是否存在 attr?
attr 是否是数据描述符?
调用数据描述符 get
obj.dict 中是否有 attr?
返回实例属性
attr 是否是非数据描述符?
调用非数据描述符 get
返回普通类属性
是否定义 getattr?
调用 getattr
抛出 AttributeError

在工程实践中,你不一定每天都手写描述符,但只要你理解这张图,很多 Python 行为都会变得清晰。


十二、常见坑点:数据到底应该存在哪里?

很多初学者第一次写描述符时,会把值存在描述符对象自己身上。

错误示例:

python 复制代码
class BadField:
    def __init__(self):
        self.value = None

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = value

看起来没问题,但实际很危险。

python 复制代码
class User:
    age = BadField()


a = User()
b = User()

a.age = 18
b.age = 30

print(a.age)
print(b.age)

输出:

python 复制代码
30
30

为什么?

因为 age = BadField() 是类属性,整个 User 类只有一个 BadField 描述符对象。所有实例共享它。

正确做法是把每个实例的数据存进实例自己的 __dict__

python 复制代码
class GoodField:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

这样每个实例都有自己的数据,互不影响。


十三、结合 __set_name__ 写更优雅的描述符

__set_name__ 会在类创建时自动调用,用于告诉描述符:你被绑定到了哪个属性名上。

python 复制代码
class Field:
    def __set_name__(self, owner, name):
        print(f"{owner.__name__}.{name} 被绑定")
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value


class User:
    name = Field()
    age = Field()

输出:

python 复制代码
User.name 被绑定
User.age 被绑定

以前很多描述符写法需要手动传字段名:

python 复制代码
name = Field("name")

现在更推荐用 __set_name__

python 复制代码
name = Field()

它减少了重复,也避免字段名写错。


十四、项目实践:迷你配置系统

假设我们要写一个应用配置类,要求:

  • host 必须是字符串;
  • port 必须是 1 到 65535 的整数;
  • debug 必须是布尔值。

我们可以构建一套描述符字段。

python 复制代码
class TypedField:
    expected_type = object

    def __init__(self, *, default=None):
        self.default = default
        self.name = None

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

    def validate(self, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.name} 必须是 {self.expected_type.__name__}"
            )

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name, self.default)

    def __set__(self, instance, value):
        self.validate(value)
        instance.__dict__[self.name] = value


class StringField(TypedField):
    expected_type = str


class BoolField(TypedField):
    expected_type = bool


class PortField(TypedField):
    expected_type = int

    def validate(self, value):
        super().validate(value)
        if not 1 <= value <= 65535:
            raise ValueError("port 必须在 1 到 65535 之间")

使用:

python 复制代码
class AppConfig:
    host = StringField(default="127.0.0.1")
    port = PortField(default=8000)
    debug = BoolField(default=False)


config = AppConfig()

print(config.host)
print(config.port)
print(config.debug)

config.port = 9000
config.debug = True

config.port = 70000

最后一行会抛出异常:

python 复制代码
ValueError: port 必须在 1 到 65535 之间

这个案例展示了数据描述符在项目中的真实价值:把校验、默认值、错误提示等逻辑封装起来,让业务类保持干净。


十五、描述符与框架设计思维

如果你用过 Django ORM,可能见过类似写法:

python 复制代码
class User(models.Model):
    name = models.CharField(max_length=50)
    age = models.IntegerField()

如果你用过表单库,也可能见过:

python 复制代码
class RegisterForm:
    username = StringField(required=True)
    password = StringField(required=True)

这些写法都体现了一种思想:用类属性声明字段,用描述符管理字段行为。

当然,成熟框架内部远比我们这里复杂,还会结合元类、类型注解、反射、数据库映射、验证器、序列化等机制。但理解描述符后,你再看这些框架,就不会只觉得"神奇",而能看见它背后的设计路径。

这也是学习 Python 进阶知识的意义:不是为了炫技,而是为了看懂优秀框架为什么这样设计,并在自己的项目中写出更稳、更优雅的代码。


十六、最佳实践:什么时候用数据描述符,什么时候用非数据描述符?

适合使用数据描述符的场景:

text 复制代码
需要强制控制属性赋值
需要字段类型校验
需要范围校验
需要只读属性
需要拦截删除行为
需要构建模型字段或表单字段

适合使用非数据描述符的场景:

text 复制代码
只需要控制读取行为
允许实例覆盖属性
需要实现缓存属性
需要延迟计算
需要模拟方法绑定行为

可以这样判断:

如果你的规则是"必须经过我",用数据描述符。

如果你的规则是"我提供默认能力,但实例可以接管",用非数据描述符。


十七、单元测试建议

描述符通常隐藏在属性访问背后,所以更需要测试。

下面是一个简单测试示例:

python 复制代码
def test_integer_field_valid_value():
    class User:
        age = IntegerField(min_value=0, max_value=150)

    user = User()
    user.age = 18

    assert user.age == 18


def test_integer_field_invalid_type():
    class User:
        age = IntegerField(min_value=0, max_value=150)

    user = User()

    try:
        user.age = "18"
    except TypeError as e:
        assert "age 必须是整数" in str(e)


def test_instances_are_independent():
    class User:
        age = IntegerField(min_value=0)

    a = User()
    b = User()

    a.age = 18
    b.age = 30

    assert a.age == 18
    assert b.age == 30

重点测试这些内容:

  • 正常赋值是否成功;
  • 错误类型是否抛异常;
  • 边界值是否正确;
  • 多个实例之间是否互不影响;
  • 通过类访问描述符时是否安全;
  • 实例属性是否会覆盖非数据描述符。

十八、性能与可维护性思考

描述符很强大,但不要滥用。

如果只是一个简单的计算属性,property 更清晰:

python 复制代码
@property
def full_name(self):
    return f"{self.first_name} {self.last_name}"

如果只是普通数据存储,直接用实例属性即可:

python 复制代码
self.name = name

如果多个字段共享复杂校验规则,或者你正在设计可复用框架组件,描述符才真正值得出场。

好的 Python 编程不是把所有高级语法都用上,而是知道每种工具适合解决什么问题。成熟的工程代码,往往不是最炫的,而是最容易理解、最稳定、最方便扩展的。


十九、总结:区别不只是语法,而是控制权

数据描述符和非数据描述符的区别,可以浓缩成三句话:

第一,数据描述符实现了 __set____delete__,非数据描述符通常只实现 __get__

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

第三,数据描述符适合强约束,非数据描述符适合默认行为与延迟计算。

理解这一区别后,你会发现很多 Python 机制都豁然开朗:

  • property 为什么能拦截赋值;
  • 实例方法为什么能自动绑定 self
  • cached_property 为什么能缓存结果;
  • ORM 字段为什么能像普通属性一样使用;
  • 框架为什么喜欢用类属性声明规则。

Python 的优雅,不只是语法简洁,更在于它把复杂能力藏进了一套统一而开放的对象协议中。描述符就是其中最值得深入理解的一环。

当你真正掌握数据描述符和非数据描述符后,你写的就不只是 Python 代码,而是在和 Python 对象模型协作。


互动思考

你在项目中是否遇到过属性校验、延迟加载、缓存计算或字段声明的场景?

如果让你实现一个配置系统、ORM 模型或表单校验工具,你会选择 property、数据描述符,还是非数据描述符?

欢迎在评论区分享你的实践经验。技术成长从来不是孤独的路,每一次交流,都是我们重新理解代码的一次机会。


SEO 关键词建议

Python编程、Python教程、Python实战、Python最佳实践、Python描述符、数据描述符、非数据描述符、Python属性查找、Python高级编程、Python对象模型。