Python 属性描述符:从原理到 ORM 实践详解

Python 属性描述符:从原理到 ORM 实践详解

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__中查找该属性:
    • 若为非数据描述符,调用其__get__方法返回结果;
    • 若不是描述符,直接返回类属性的值;
  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__的核心区别与使用场景。

Python 属性描述符:从原理到 ORM 实践详解

Read more

Android WebView 版本升级方案详解

Android WebView 版本升级方案详解 目录 1. 问题背景 2. WebViewUpgrade 项目介绍 3. 升级方法详解 4. 替代方案对比 5. 接入与使用步骤 6. 注意事项与限制 7. 总结与建议 问题背景 WebView 版本差异带来的问题 Android 5.0 以后,WebView 升级需要去 Google Play 安装 APK,但即使安装了也不一定能正常工作。像华为、Amazon 等特殊机型的 WebView 的 Chromium 版本一般比较低,只能使用它自己的 WebView,无法使用 Google 的 WebView。 典型问题场景 H.265 视频播放问题:

By Ne0inhk
前端打工人必看:Axios搞定Excel导出上传,拒绝加班还能准时干饭

前端打工人必看:Axios搞定Excel导出上传,拒绝加班还能准时干饭

前端打工人必看:Axios搞定Excel导出上传,拒绝加班还能准时干饭 * 前端打工人必看:Axios搞定Excel导出上传,拒绝加班还能准时干饭 * 这玩意儿到底是个啥 * 上传文件那点破事 * 基础版:单文件上传 * 进阶版:多文件上传 * 高阶版:带进度条的上传 * 防手贱:防抖处理 * 下载文件才是真·深水区 * 最简版:基础下载 * 文件名怎么搞? * 封装一个通用的下载函数 * 带下载进度的大文件下载 * 咱得客观聊聊这方案 * 优点 * 缺点 * 真实项目里怎么落地 * 场景一:报表导出(异步生成) * 场景二:批量导入+实时预览 * 场景三:图片压缩上传 * 遇到报错别只会重启 * 下载下来是乱码或打不开 * 跨域问题 * 超时问题 * 几个让同事喊666的骚操作 * 1. 全局上传下载管理器 * 2. 利用拦截器统一处理 * 3.

By Ne0inhk
Vue入门到精通:从零开始学Vue

Vue入门到精通:从零开始学Vue

目录 一、第一个Vue程序 第一步 Vue构造函数的参数:options template配置项 第二步 模板语句的数据来源 Template配置项 Vue实例和容器 二、Vue模板语法 Vue 插值 Vue 指令 v-bind指令 v-model指令 三、MVVM分层思想 四、VM defineProperty 五、数据代理机制 Vue数据代理机制对属性名的要求 手写Vue框架数据代理的实现 六、解读Vue框架源代码 data(函数) 七、Vue事件处理 事件绑定 Vue事件绑定 事件回调函数中的this methods实现原理 八、事件修饰符 按键修饰符 九、计算属性 反转字符串methods实现 反转字符串计算属性实现 计算属性用法 十、侦听属性 比较大小的案例watch实现 computed实现

By Ne0inhk
【小沐杂货铺】基于Three.js渲染三维无人机Drone(WebGL / vue / react )

【小沐杂货铺】基于Three.js渲染三维无人机Drone(WebGL / vue / react )

🍺三维数字地球GIS系列相关文章(C++)🍺:1【小沐学GIS】基于C++绘制三维数字地球Earth(OpenGL、glfw、glut)第一期2【小沐学GIS】基于C++绘制三维数字地球Earth(OpenGL、glfw、glut)第二期3【小沐学GIS】基于C++绘制三维数字地球Earth(OpenGL、glfw、glut)第三期4【小沐学GIS】基于C++绘制三维数字地球Earth(QT、OpenGL)第四期5【小沐学GIS】基于C++绘制三维数字地球Earth(QT、OpenGL、Satellite、卫星轨道模拟)第五期6【小沐学GIS】基于C++绘制三维数字地球Earth(OpenG、SolarSystem、太阳系模拟)第六期7【小沐学GIS】基于C++绘制三维数字地球Earth(OpenGL、OpenSceneGraph

By Ne0inhk