从原理到ORM深入详解Python属性描述符使用方法

 更新时间:2026年03月05日 08:34:40   作者:郝学胜-神的一滴  
在Python的面向对象编程中,属性的控制与查找是核心知识点之一,而属性描述符作为实现属性精细化控制的重要工具,更是ORM框架的底层实现基础,本文将从实际开发痛点出发,深入讲解属性描述符的定义、分类、使用方法,需要的朋友可以参考下

在Python的面向对象编程中,属性的控制与查找是核心知识点之一,而属性描述符作为实现属性精细化控制的重要工具,更是ORM框架(如Django Model、SQLAlchemy)的底层实现基础。本文将从实际开发痛点出发,深入讲解属性描述符的定义、分类、使用方法,以及它在Python属性查找过程中的核心作用,帮你彻底理解这一重要特性。

一、为什么需要属性描述符?从property的局限性说起

在Python中,我们常用property装饰器来控制属性的获取、设置和删除过程,实现对属性的简单校验和逻辑封装。比如我们要限制用户类中age为整数类型、name为字符串类型,使用property可以轻松实现:

class User:
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise ValueError("int value need")
        self._age = value

但在实际开发中,尤其是在ORM场景下,一个数据模型类往往对应数据库的一张表,包含十几个甚至几十个字段,其中很多字段会有相同的类型校验规则(如nameemailmobile都是字符串类型,ageid都是整数类型)。

如果继续使用property,我们需要为每个字段编写重复的gettersetter方法,这会导致代码冗余度极高,维护成本大幅增加。为了解决代码复用的问题,Python为我们提供了更优雅的解决方案——属性描述符

二、属性描述符的定义与基础使用

2.1 什么是属性描述符?

属性描述符的定义非常简单:一个自定义类,只要实现了 __get__ __set__ __delete__ 三个魔法函数中的任意一个,这个类就是属性描述符。通过这个类的实例,我们可以将属性的控制逻辑封装起来,实现多字段的逻辑复用。

2.2 基础实现:整数类型校验描述符

我们以实现整数类型校验为例,编写第一个属性描述符,解决多个整数类型字段的校验复用问题:

import numbers

class IntField:
    # 实现__set__方法,完成类型校验
    def __set__(self, instance, value):
        # 校验是否为整数类型
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")
        # 校验是否为正数
        if value < 0:
            raise ValueError("positive value need")
        # 将值保存到描述符自身实例,避免死循环
        self.value = value
    
    # 实现__get__方法,获取属性值
    def __get__(self, instance, owner):
        return self.value

2.3 在模型类中使用描述符

定义好描述符后,我们可以在模型类中直接将其作为类属性使用,实现对字段的统一控制:

class User:
    # 将IntField实例作为类属性,实现age的整数校验
    age = IntField()

# 测试正常赋值
user = User()
user.age = 30
print(user.age)  # 输出:30

# 测试赋值非整数,抛出异常
user.age = "abc"  # 抛出ValueError: int value need

# 测试赋值负数,抛出异常
user.age = -5  # 抛出ValueError: positive value need

2.4 关键注意点:避免赋值死循环

在实现__set__方法时,切忌将值保存到传入的 instance (模型类实例)中,比如写成instance.age = value

因为当我们对instance.age赋值时,Python会再次调用描述符的__set__方法,从而陷入无限递归,最终导致栈溢出。正确的做法是将值保存到描述符自身的实例self)中,在__get__方法中再从self中取出。

三、属性描述符的分类:数据描述符与非数据描述符

根据实现的魔法函数不同,属性描述符分为两类,二者的核心区别在于在Python属性查找过程中的优先级不同,这也是理解属性查找的关键。

3.1 数据描述符(Data Descriptor)

实现了 __get__ __set__ 方法的描述符称为数据描述符,是我们开发中最常用的类型,比如上文的IntField就是典型的数据描述符。

数据描述符拥有最高的属性查找优先级,会覆盖实例自身的属性值。

3.2 非数据描述符(Non-data Descriptor)

只实现了 __get__ 方法(未实现__set__)的描述符称为非数据描述符,常见的例子是Python中的函数(函数实现了__get__方法,成为绑定方法)。

非数据描述符的优先级低于实例自身的属性值,仅在实例中未找到该属性时才会生效。

四、Python完整的属性查找过程:描述符的核心作用

在学习属性描述符之前,我们对Python属性查找的认知通常是:先查找实例的 __dict__ ,再查找类的 __dict__ ,最后查找基类的 __dict__

但当引入属性描述符后,Python的属性查找过程会变得更加精细,而这一过程也是getattr__getattribute__两个魔法函数的底层逻辑。当我们使用 实例.属性 (如 user.age )的方式访问属性时,等价于调用全局函数 getattr(实例, 属性名),其完整的查找顺序如下:

4.1 核心查找顺序

  1. 调用 __getattribute__:无论属性是否存在,都会先调用类中的__getattribute__方法,这是属性查找的入口;
  2. 检查数据描述符:在实例的类或基类的__dict__中查找该属性,若该属性是数据描述符,则直接调用其__get__方法,返回结果;
  3. 检查实例自身属性:在实例的__dict__中查找该属性,若找到则直接返回值;
  4. 检查非数据描述符/类属性:在实例的类或基类的__dict__中查找该属性:
    1. 若为非数据描述符,调用其__get__方法返回结果;
    2. 若不是描述符,直接返回类属性的值;
  5. 调用 __getattr__:若以上步骤均未找到属性,会触发AttributeError,此时若类中定义了__getattr__方法,会调用该方法;
  6. 抛出异常:若未定义__getattr__,则直接抛出AttributeError

4.2 关键验证:数据描述符覆盖实例属性

数据描述符的优先级高于实例自身属性,即使我们手动给实例的__dict__添加该属性,访问时仍会优先调用数据描述符的__get__方法:

# 延续上文的IntField和User
user = User()
user.age = 30
# 实例的__dict__为空,值保存在描述符实例中
print(user.__dict__)  # 输出:{}

# 手动给实例__dict__添加age属性
user.__dict__['age'] = "abc"
# 访问时仍调用数据描述符,因未给描述符赋值,会报错
print(user.age)  # 报错:IntField has no attribute 'value'

4.3 关键验证:非数据描述符被实例属性覆盖

若将IntField改为非数据描述符(仅实现__get__),则实例自身的属性会覆盖描述符:

class NonDataIntField:
    # 仅实现__get__,非数据描述符
    def __get__(self, instance, owner):
        return 10

class User:
    age = NonDataIntField()

user = User()
# 实例赋值age,覆盖非数据描述符
user.age = 30
print(user.age)  # 输出:30,而非描述符的10

五、属性描述符的实际应用:ORM框架的底层基础

属性描述符的核心价值在于属性逻辑的封装与复用,这也是所有Python ORM框架的底层实现原理。

在Django Model、SQLAlchemy等框架中,我们定义的CharFieldIntegerFieldEmailField等字段,本质上都是封装了不同校验规则和数据库映射逻辑的属性描述符

  • 字段的类型校验(如CharField限制字符串)由描述符的__set__方法实现;
  • 字段的数据库字段映射(如字段长度、是否为主键)由描述符的初始化参数实现;
  • 字段的取值逻辑由描述符的__get__方法实现。

通过属性描述符,ORM框架将数据库表的字段与Python类的属性进行了完美映射,让我们可以用面向对象的方式操作数据库,而无需编写重复的校验和映射代码。

六、总结

  1. 属性描述符的诞生:为解决property在多字段场景下的代码冗余问题,实现属性控制逻辑的复用;
  2. 定义规则:实现__get____set____delete__任一方法的自定义类,即为属性描述符;
  3. 两大分类:实现__get__+__set__数据描述符(高优先级)、仅实现__get__非数据描述符(低优先级);
  4. 属性查找核心实例.属性的查找顺序为「数据描述符 → 实例属性 → 非数据描述符/类属性 → getattr → 异常」;
  5. 实际价值:Python ORM框架的底层核心,是实现属性精细化控制和数据库映射的关键工具。

掌握属性描述符,不仅能让我们写出更优雅、更易维护的Python代码,更能帮助我们理解主流框架的底层实现逻辑,提升Python面向对象编程的核心能力。下一篇文章,我们将继续深入Python的魔法函数,讲解元类编程中__new____init__的核心区别与使用场景。

以上就是从原理到ORM详解Python属性描述符使用方法的详细内容,更多关于Python属性描述符的资料请关注脚本之家其它相关文章!

相关文章

  • 爬虫代理池Python3WebSpider源代码测试过程解析

    爬虫代理池Python3WebSpider源代码测试过程解析

    这篇文章主要介绍了爬虫代理池Python3WebSpider源代码测试过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • python循环某一特定列的所有行数据(方法示例)

    python循环某一特定列的所有行数据(方法示例)

    在Python中,处理表格数据(比如CSV文件、Excel文件等)时,我们通常会使用pandas库,因为它提供了丰富的数据结构和数据分析工具,下面,我将以处理CSV文件中的某一特定列的所有行数据为例,给出详细、完整的代码示例,感兴趣的朋友跟随小编一起看看吧
    2024-08-08
  • python中Pycharm 输出中文或打印中文乱码现象的解决办法

    python中Pycharm 输出中文或打印中文乱码现象的解决办法

    本篇文章主要介绍了python中Pycharm 输出中文或打印中文乱码现象的解决办法 ,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • python常用数据重复项处理方法

    python常用数据重复项处理方法

    在本篇文章里小编给大家整理的是关于python常用数据重复项处理方法,需要的朋友们参考下。
    2019-11-11
  • Python实战之疫苗研发情况可视化

    Python实战之疫苗研发情况可视化

    2020年底以来,欧美,印度,中国,俄罗斯等多国得制药公司纷纷推出了针对新冠/肺炎的疫苗,这部分主要分析了2020年以来全球疫情形势,各类疫苗在全球的地理分布,疫苗在各国的接种进度进行可视化展示,需要的朋友可以参考下
    2021-05-05
  • python操作yaml的方法详解

    python操作yaml的方法详解

    这篇文章主要为大家介绍了python操作yaml的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • Python遍历zip文件输出名称时出现乱码问题的解决方法

    Python遍历zip文件输出名称时出现乱码问题的解决方法

    这篇文章主要介绍了Python遍历zip文件输出名称时出现乱码问题的解决方法,实例分析了Python乱码的出现的原因与相应的解决方法,需要的朋友可以参考下
    2015-04-04
  • python ddt实现数据驱动

    python ddt实现数据驱动

    这篇文章主要为大家详细介绍了python ddt实现数据驱动,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03
  • 使用Python和Pygame轻松实现播放音频播放器

    使用Python和Pygame轻松实现播放音频播放器

    在这个数字化时代,音频和音乐已成为我们日常生活的一部分,不管是为了放松、学习还是工作,一个好的音乐播放器总是必不可少的,所以本文给大家介绍了用Python和Pygame制作自己的音频播放器,感兴趣的朋友可以参考下
    2024-01-01
  • Python 如何反方向迭代一个序列

    Python 如何反方向迭代一个序列

    这篇文章主要介绍了Python 如何反方向迭代一个序列,文中讲解非常细致,代码帮助大家更好理解和学习,感兴趣的朋友可以了解下
    2020-07-07

最新评论