
Python通过描述符协议(descriptor protocol)实现方法绑定:当从实例访问类属性中的普通函数时,该函数的__get__方法被触发,自动将实例作为第一个参数(self)注入;而从类直接访问或赋值给实例属性时则无此行为。
python通过描述符协议(descriptor protocol)实现方法绑定:当从实例访问类属性中的普通函数时,该函数的`__get__`方法被触发,自动将实例作为第一个参数(`self`)注入;而从类直接访问或赋值给实例属性时则无此行为。
在Python面向对象编程中,一个看似反直觉却高度一致的行为常引发困惑:为什么把一个普通函数赋值给类属性后,通过实例调用它,居然会自动传入实例本身(即 self)? 例如:
def plain_func(*args, **kwargs):
print("Args:", args)
class B:
fn = plain_func # 将普通函数赋为类属性
b = B()
b.fn("hello") # 输出: Args: (<__main__.B object at 0x...>, 'hello')这并非Bug,而是Python底层描述符协议(Descriptor Protocol) 的明确设计——函数对象天然实现了__get__方法,使其成为“非数据描述符”(non-data descriptor)。这一机制正是Python统一处理“方法绑定”的核心。
? 关键原理:函数是描述符
所有用户定义的函数(function类型)在Python中都内置了__get__方法。当通过点号(.)访问属性时:
- 若该属性对象实现了
__get__,且当前访问发生在实例上(如b.fn),解释器会调用fn.__get__(b, B); -
function.__get__的默认行为是:返回一个“绑定方法”(bound method)对象,该对象已预置第一个参数为被访问的实例(b); - 因此
b.fn()实际等价于plain_func(b, "hello")。
反之:
立即学习“Python免费学习笔记(深入)”;
- 若通过类名访问(如
B.fn()),__get__被调用为fn.__get__(None, B),此时返回未绑定的原始函数,不注入任何参数; - 若将函数赋值给实例属性(如
b.fn = plain_func),则绕过类字典查找,不触发描述符协议,b.fn()直接调用原函数,无自动传参。
? 静态方法为何“失效”?——装饰器的本质
静态方法(@staticmethod)的包装器(staticmethod对象)本身也实现了__get__,但其逻辑是:无论从类还是实例访问,均返回原始函数,不注入任何参数。
然而,一旦你执行:
BStatic.fn = BStatic.static_fn # ❌ 错误用法
你实际是将 BStatic.static_fn 这个已包装的 staticmethod 对象(类型为 <class></class>)赋给了类属性。但注意:staticmethod 对象自身不是函数,它只有在通过 __get__ 被访问时才“释放”出内部函数。
而关键在于:staticmethod 对象本身不实现 __call__(直到 Python 3.10+ 才支持),且当它被赋值为类属性后,再次被实例访问(bs.fn)时,触发的是 staticmethod.__get__ —— 它返回的是内部函数,但该函数本身仍是普通函数! 因此后续调用 bs.fn() 时,又触发了该函数自身的 __get__,导致 self 被注入。
✅ 正确获取静态方法原始函数的方式(绕过描述符):
# 从类的 __dict__ 中直接取(跳过 __get__)
BStatic.fn = BStatic.__dict__["static_fn"] # type: staticmethod
# 或者显式解包(推荐)
BStatic.fn = BStatic.static_fn.__func__ # 直接拿到原始函数对象
bs = BStatic()
bs.fn("test") # ✅ 输出: () {'test'} —— 无 self 注入⚠️ 实践建议与避坑指南
-
不要混用装饰器与动态赋值:
@staticmethod/@classmethod的语义依赖于它们在类体中被定义的位置。动态赋值会破坏绑定语义。 -
区分“函数对象”与“方法对象”:
-
B.fn→function类型(未绑定) -
b.fn→method类型(绑定方法,含self) -
B.__dict__['fn']→ 原始函数或装饰器包装器
-
-
调试技巧:使用
type(obj)和obj.__func__检查真实类型与底层函数:print(type(BStatic.static_fn)) # <class 'staticmethod'> print(type(BStatic.static_fn.__func__)) # <class 'function'> print(BStatic.static_fn is BStatic.__dict__['static_fn']) # True
✅ 总结
Python的方法调用机制本质是描述符协议驱动的自动化参数注入,而非语法糖。理解 function.__get__ 的行为,就掌握了实例方法、类方法、静态方法三者差异的底层钥匙。所谓“魔法”,不过是精心设计的协议:函数因描述符而成为方法,而装饰器则是对这一协议的定制化重写。掌握它,你便真正读懂了Python面向对象的骨架。


















