在 Python 中,字符串、整数、列表,它们 100% 都是对象(Object)。
既然它们都是对象,为什么访问它们不触发描述符协议,而访问方法、函数、你的 Decorator 实例就会触发?
第一类:数据对象(字符串、整数、列表等)
class MyClass:
name = "Alice" # 字符串对象
age = 18 # 整数对象
items = [1, 2, 3] # 列表对象
● 它们是什么? 它们是纯粹的数据。
● 底层特征: 在 CPython 的 C 语言源码中,字符串( PyUnicode_Type )、整数( PyLong_Type )的底层 C 结构体里,没有实现 tp_descr_get 这个槽位(即指针为 NULL)。
● 设计思想: Python 解释器认为,数据就是用来“存取”的。当你问我要 c.name 时,我就老老实实把 "Alice" 这个数据扔给你,不需要任何花里胡哨的拦截和计算。
第二类:行为对象(函数、方法、你的 Decorator 实例等)
class MyClass:
def method(self): pass # 函数对象
attr = Decorator() # 自定义类的实例对象
● 它们是什么? 它们是代码、逻辑、或者带有控制意图的代理。
● 底层特征: 函数( PyFunction_Type )和你手写的 Decorator 类,在底层 C 结构体中,都明确实现了 tp_descr_get 这个槽位。
● 设计思想: Python 解释器认为,行为对象不仅仅是被存储的值,它们需要感知上下文。比如函数需要知道自己是被哪个实例调用的(为了绑定 self ),你的 Decorator 需要知道是谁在访问它(为了返回 partial )。
你之所以觉得混乱,是因为 Python 在语法层面告诉你“一切皆对象”,但在底层执行层面,解释器其实是“看人下菜碟”的。
绑定方法(Bound Method)。
class MyClass: def my_func(self): pass obj = MyClass()
当你调用 obj.my_func() 时,Python 必须自动把 obj 传给 self 。
如果没有描述符协议
my_func 在类字典里只是一个普通的函数对象。当你通过 obj 访问它时,如果没有特殊机制,Python 只能把这个函数原封不动地扔给你。
那你每次调用都得写成: MyClass.my_func(obj) 。这太反人类了!
有了描述符协议
函数对象(Function Object)在 Python 内部其实就是一个实现了 get 的描述符!
1. 你访问 obj.my_func 。
2. Python 发现 my_func 是个对象,且它有 get 。
3. Python 调用 my_func.get(obj, MyClass) 。
4. 函数对象的 get 内部逻辑是:“既然你是通过实例 obj 来访问我的,那我就把自己包装成一个‘绑定方法’,把 obj 塞进我的口袋里,然后把这个包装好的东西给你。”
在这段代码中, c.attr 确实就是一个普通的“数据对象”,它和字符串、整数在底层行为上是一模一样的。
你现在的理解已经完全到位了:
● 函数/方法:天生自带 get ,是行为对象。
● 你的 deco() 实例:因为没有写 get ,所以它就是普通数据,和 "hello" 或 123 没有任何本质区别。
Python 的设计者认为:如果一个对象主动声明了“我知道怎么被访问”(通过实现 get ),那么解释权就应该交给它,而不是由 Python 解释器强行直接返回它。
这种设计的精妙之处
它允许对象感知上下文。
● 当一个对象躺在类的字典里时,它是“冷”的,不知道谁会用他。
● 当通过 c.method 访问时,描述符协议被触发,对象瞬间变“热”了,它知道了:“哦,原来是 c 这个实例在找我!”
这就是为什么 get(self, instance, owner) 需要这三个参数:
● self : 描述符自己(那个代理对象)。
● instance : 谁在访问我(实例 c )。
● owner : 我在哪个类里(类 cls )。
有了这三个信息,描述符就可以动态地返回不同的结果。
描述符协议的设计思想是为了实现 Python 的动态性与灵活性。
它把 “数据”(存在实例字典里的值)和 “行为”(如何获取、设置、删除这个值)分离开了。
● 函数变成方法:靠它。
● @property :靠它。
● ORM 框架(如 SQLAlchemy/Django):靠它。
● 你的装饰器自动绑定 self :也靠它。