函数的描述符特性与绑定方法的生成机制
一、为什么把两件事放在同一篇讲
在 Python 中,「函数」(function)本身是一种非数据描述符(non-data descriptor)。
解释器把函数放进类属性槽里时,正是靠描述符协议把它**魔术般地」变成「绑定方法」(bound method)。
理解描述符是理解「方法绑定」的唯一入口;理解绑定过程又能反向验证描述符的工作方式。二者不可分割。
二、描述符协议(descriptor protocol)速览
| 协议成员 | 是否必须 | 调用时机 | 作用 |
|---|---|---|---|
__get__(self, obj, objtype=None) |
必须 | 读取属性时 | 返回「计算后的值」 |
__set__(self, obj, value) |
可选 | 赋值时 | 拦截写操作 |
__delete__(self, obj) |
可选 | del 时 | 拦截删除 |
- 数据描述符:至少实现
__set__或__delete__;优先级高于实例字典。 - 非数据描述符:只实现
__get__;优先级低于实例字典,高于类字典的普通值。
三、函数对象:一个典型的非数据描述符
CPython 源码:Objects/funcobject.c
PyFunction 结构体里自带:
PyDescrObject f_descr; /* 嵌入的 descriptor 头部 */
因此所有函数天生带 __get__,签名:
function.__get__(self, obj, objtype=None) -> method
obj is None→ 返回未绑定函数本身(Python 3 里就是原函数)。obj is not None→ 返回绑定方法,把obj作为第一个参数(self)固化。
四、绑定方法(bound method)的生成过程
-
类属性检索
MyClass.spam触发type.__getattribute__→PyType_Lookup找到类字典里的函数对象spam。 -
描述符触发
因为函数实现了__get__,解释器转而执行:
method = spam.__get__(None, MyClass)# 未绑定
或
method = spam.__get__(instance, MyClass)# 绑定 -
方法对象诞生
CPython 内部新建一个PyMethodObject,保存:im_func→ 原函数指针im_self→ 绑定的实例(或 NULL)im_class→ 所属类
这一步对用户完全透明。
-
调用阶段
绑定方法被执行时,它的__call__把im_self插到参数列表最前面,再转发给im_func。
五、代码级演示:从函数到绑定方法
class Foo:def bar(self, x):return x * 2f = Foo()
print(Foo.bar) # <function Foo.bar at ...> (未绑定)
print(f.bar) # <bound method Foo.bar of <__main__.Foo object ...>>
验证描述符身份:
>>> Foo.bar.__get__(None, Foo) is Foo.bar
True
>>> f.bar.__func__ is Foo.bar
True
>>> f.bar.__self__ is f
True
六、静态方法与类方法:描述符的「二次包装」
staticmethod / classmethod 同样是描述符,只是它们在 __get__ 里不返回原函数,而是返回:
staticmethod:原函数(无绑定)classmethod:绑定到类对象的新方法
源码级等价:
class staticmethod:def __init__(self, func):self.func = funcdef __get__(self, obj, objtype=None):return self.funcclass classmethod:def __init__(self, func):self.func = funcdef __get__(self, obj, objtype=None):if objtype is None:objtype = type(obj)return self.func.__get__(objtype, objtype)
因此:
函数 → 描述符 →(被 staticmethod/classmethod 再次包装)→ 新的描述符
形成一条「描述符链」。
七、优先级现场实验
class A:def f(self): pass # 非数据描述符a = A()
a.f = 123 # 实例字典覆盖
print(a.f) # 123
del a.f # 删除后恢复描述符
print(a.f) # <bound method A.f ...>
把函数升级为数据描述符:
class DataDescriptor:def __get__(self, obj, objtype=None):return 42def __set__(self, obj, value):passclass B:f = DataDescriptor()b = B()
b.f = 99
print(b.f) # 42,优先级:数据描述符 > 实例字典
八、CPython 源码级鸟瞰(快速索引)
| 文件 | 关键函数 | 说明 |
|---|---|---|
Objects/funcobject.c |
func_descr_get |
函数描述符入口 |
Objects/classobject.c |
method_call |
绑定方法执行 |
Objects/typeobject.c |
type_getattro |
属性检索总控 |
Python/ceval.c |
_PyMethodDef_RawFastCall |
方法调用加速 |
九、常见面试速答模板
Q: “Python 的函数写在类里就能自动变成方法,底层是怎么做到的?”
A:
函数本身是非数据描述符,实现了 __get__(self, obj, cls)。
类属性检索时,解释器发现它带描述符协议,于是把 obj 传进去;
__get__ 返回一个新对象——绑定方法,内部保存原函数与实例。
调用阶段,绑定方法把实例插到参数最前面,再转发给原函数,于是看似“自动传 self”。
十、结论一句话
函数 →(描述符协议)→ 绑定方法;
静态/类方法 →(再包装成描述符)→ 改变绑定规则。
整个“方法”概念在 Python 里完全是描述符协议的副作用,无魔法,唯协议。