函数的描述符特性与绑定方法的生成机制

函数的描述符特性与绑定方法的生成机制

一、为什么把两件事放在同一篇讲

在 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)的生成过程

  1. 类属性检索
    MyClass.spam 触发 type.__getattribute__PyType_Lookup 找到类字典里的函数对象 spam

  2. 描述符触发
    因为函数实现了 __get__,解释器转而执行:
    method = spam.__get__(None, MyClass) # 未绑定

    method = spam.__get__(instance, MyClass) # 绑定

  3. 方法对象诞生
    CPython 内部新建一个 PyMethodObject,保存:

    • im_func → 原函数指针
    • im_self → 绑定的实例(或 NULL)
    • im_class → 所属类
      这一步对用户完全透明。
  4. 调用阶段
    绑定方法被执行时,它的 __call__im_self 插到参数列表最前面,再转发给 im_func


五、代码级演示:从函数到绑定方法

class Foo:
    def bar(self, x):
        return x * 2

f = 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 = func
    def __get__(self, obj, objtype=None):
        return self.func

class classmethod:
    def __init__(self, func):
        self.func = func
    def __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 42
    def __set__(self, obj, value):
        pass

class 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 里完全是描述符协议的副作用,无魔法,唯协议。

posted @ 2025-11-05 18:43  wangya216  阅读(9)  评论(0)    收藏  举报