函数为什么也是描述符?从obj.method()彻底理解 Python 方法绑定机制
很多 Python 初学者第一次看到这句话时都会惊讶:
Python 中的函数也是描述符。
函数不就是一段可以被调用的代码吗?为什么会和“描述符”这种听起来很底层的机制扯上关系?
更有意思的是,当我们写:
classUser:defsay_hello(self):print("hello")u=User()u.say_hello()我们明明只传了零个参数,但say_hello(self)却需要一个self。这个self到底是谁帮我们传进去的?
答案就是:函数对象实现了描述符协议,Python 在访问实例方法时,会自动把函数绑定到实例上,生成一个绑定方法。
这篇文章就围绕一个核心问题展开:
为什么 Python 中函数也是描述符?它到底解决了什么问题?
一、先理解描述符:它是 Python 属性访问的“拦截器”
在 Python 中,只要一个对象实现了下面三个方法中的任意一个,它就可以被称为描述符:
__get__(self,instance,owner)__set__(self,instance,value)__delete__(self,instance)最常见的是__get__。
我们先写一个最小描述符:
classMyDescriptor:def__get__(self,instance,owner):print("触发 __get__")print("instance:",instance)print("owner:",owner)return"这是描述符返回的值"classDemo:value=MyDescriptor()d=Demo()print(d.value)输出类似:
触发 __get__ instance:<__main__.Demoobjectat 0x...>owner:<class'__main__.Demo'>这是描述符返回的值当你访问:
d.valuePython 并不是简单地从对象字典里拿值,而是发现Demo.value是一个描述符,于是自动调用:
Demo.__dict__["value"].__get__(d,Demo)这就是描述符的本质:它可以接管属性访问过程。
二、函数为什么也是描述符?
现在我们看一个普通类方法:
classUser:defgreet(self):return"hello"很多人以为greet在类里就是一个“方法”。其实在类对象里,它首先是一个函数对象。
我们可以验证:
classUser:defgreet(self):return"hello"print(User.__dict__["greet"])print(type(User.__dict__["greet"]))输出类似:
<function User.greet at 0x...><class'function'>也就是说,类体中的def greet(self)创建的是一个函数对象,并把它放进了类的命名空间中。
关键来了:函数对象本身实现了__get__方法。
func=User.__dict__["greet"]print(hasattr(func,"__get__"))print(func.__get__)输出:
True<method-wrapper'__get__'of functionobjectat 0x...>所以,函数是描述符。
准确地说,普通函数实现了非数据描述符协议:
function.__get__(instance,owner)当我们访问:
u=User()u.greetPython 实际上会调用:
User.__dict__["greet"].__get__(u,User)它返回的不是原始函数,而是一个绑定方法 bound method。
三、obj.method()背后发生了什么?
看这段代码:
classUser:defgreet(self,name):returnf"hello,{name}"u=User()print(User.greet)print(u.greet)输出类似:
<function User.greet at 0x...><bound method User.greet of<__main__.Userobjectat 0x...>>两者不同:
User.greet拿到的是函数对象。
u.greet拿到的是绑定方法。
绑定方法内部保存了两样东西:
method=u.greetprint(method.__func__)# 原始函数print(method.__self__)# 被绑定的实例输出类似:
<function User.greet at 0x...><__main__.Userobjectat 0x...>所以:
u.greet("Tom")本质上等价于:
User.greet(u,"Tom")也就是说,self并不神秘,它只是 Python 在函数描述符的__get__阶段帮我们绑定进去的实例对象。
可以用下面的流程理解:
u.greet ↓ 在 User 类中找到 greet 函数对象 ↓ 发现 greet 实现了 __get__ ↓ 调用 greet.__get__(u, User) ↓ 返回 bound method ↓ 调用 bound method("Tom") ↓ 实际执行 User.greet(u, "Tom")四、手写一个“函数描述符”,模拟方法绑定
为了彻底理解函数为什么是描述符,我们可以自己模拟一个简化版方法绑定机制。
classBoundMethod:def__init__(self,func,instance):self.func=func self.instance=instancedef__call__(self,*args,**kwargs):returnself.func(self.instance,*args,**kwargs)classFunctionLikeDescriptor:def__init__(self,func):self.func=funcdef__get__(self,instance,owner):ifinstanceisNone:returnself.funcreturnBoundMethod(self.func,instance)defsay(self,message):returnf"{self.name}:{message}"classUser:def__init__(self,name):self.name=name speak=FunctionLikeDescriptor(say)u=User("Alice")print(u.speak("hello"))输出:
Alice:hello这里的FunctionLikeDescriptor做的事情,本质上就是普通函数在类中作为方法时做的事情:
- 如果通过类访问,返回原始函数;
- 如果通过实例访问,返回绑定了实例的方法对象;
- 调用绑定方法时,自动把实例作为第一个参数传入。
这就是self自动注入的底层逻辑。
五、为什么 Python 要这样设计?
因为 Python 的类模型非常统一。
在 Python 中,类也是对象,函数也是对象,方法绑定也不是语法魔法,而是建立在对象协议之上的行为。
这种设计带来了几个好处。
1. 类中的函数可以被直接访问
classCalculator:defadd(self,x,y):returnx+yprint(Calculator.add)这允许我们显式传入实例:
c=Calculator()print(Calculator.add(c,1,2))输出:
3这说明方法并不是特殊语法,它本质上仍然是函数。
2. 实例方法绑定是动态完成的
每次访问实例方法时,Python 都会动态创建一个绑定方法对象。
classDemo:defrun(self):passd=Demo()print(d.runisd.run)通常输出:
False为什么?
因为每次执行d.run,都会触发一次函数描述符的__get__,返回一个新的绑定方法对象。
但它们背后的函数和实例是相同的:
m1=d.run m2=d.runprint(m1.__func__ism2.__func__)print(m1.__self__ism2.__self__)输出:
TrueTrue这对于调试非常有帮助。
3.staticmethod和classmethod本质也是描述符
我们平时写的这些装饰器,其实也依赖描述符机制。
classDemo:definstance_method(self):print("实例方法",self)@staticmethoddefstatic_method():print("静态方法")@classmethoddefclass_method(cls):print("类方法",cls)访问它们:
d=Demo()d.instance_method()d.static_method()d.class_method()三者行为不同:
实例方法:自动绑定实例 self 静态方法:不绑定任何对象 类方法:自动绑定类 cls背后原因是:
普通函数 -> __get__ 返回绑定实例的方法 staticmethod -> __get__ 返回原始函数 classmethod -> __get__ 返回绑定类的方法可以简单模拟一下staticmethod:
classMyStaticMethod:def__init__(self,func):self.func=funcdef__get__(self,instance,owner):returnself.func再模拟classmethod:
classMyClassMethod:def__init__(self,func):self.func=funcdef__get__(self,instance,owner):defwrapper(*args,**kwargs):returnself.func(owner,*args,**kwargs)returnwrapper这说明 Python 的方法体系不是零散设计,而是统一建立在描述符协议之上。
六、函数是“非数据描述符”
描述符分两类:
数据描述符:实现了 __set__ 或 __delete__ 非数据描述符:只实现了 __get__普通函数只实现了__get__,所以它是非数据描述符。
这会影响属性查找优先级。
Python 查找属性时,大致顺序是:
1. 类中的数据描述符 2. 实例自身的 __dict__ 3. 类中的非数据描述符 4. 类中的普通属性 5. __getattr__因为函数是非数据描述符,所以实例属性可以覆盖同名方法。
例如:
classUser:defgreet(self):return"hello"u=User()print(u.greet())输出:
hello现在给实例添加同名属性:
u.greet="我覆盖了 greet 方法"print(u.greet)输出:
我覆盖了 greet 方法此时再调用:
u.greet()会报错:
TypeError:'str'objectisnotcallable这就是为什么在项目中不建议把实例属性命名成和方法一样的名字。
例如下面的写法很危险:
classTask:defstatus(self):return"running"def__init__(self):self.status="pending"这里self.status会覆盖status()方法。
更好的写法是:
classTask:defget_status(self):return"running"def__init__(self):self.status="pending"或者使用属性描述符:
classTask:def__init__(self):self._status="pending"@propertydefstatus(self):returnself._status七、实践案例:用描述符构建一个字段校验器
理解函数是描述符之后,我们就能更自然地理解property、ORM 字段、表单校验器等机制。
比如我们写一个简单的字段校验描述符:
classPositiveNumber:def__init__(self,name):self.name=namedef__get__(self,instance,owner):ifinstanceisNone:returnselfreturninstance.__dict__.get(self.name)def__set__(self,instance,value):ifvalue<=0:raiseValueError(f"{self.name}必须是正数")instance.__dict__[self.name]=valueclassProduct:price=PositiveNumber("price")stock=PositiveNumber("stock")def__init__(self,price,stock):self.price=price self.stock=stock p=Product(99,10)print(p.price)print(p.stock)输出:
9910如果赋值非法:
p.price=-1会抛出:
ValueError:price 必须是正数这和函数描述符的思想是一致的:把属性访问行为封装起来,让对象在读取、写入、删除属性时具备可控逻辑。
只不过普通函数使用描述符是为了完成“方法绑定”,而我们自定义描述符通常用于:
数据校验 类型约束 延迟加载 缓存计算 权限控制 ORM 字段映射八、property也是描述符
很多人用过@property,但不知道它也是描述符。
classUser:def__init__(self,birth_year):self.birth_year=birth_year@propertydefage(self):return2026-self.birth_year u=User(2000)print(u.age)输出:
26看起来像访问属性:
u.age实际上会触发property.__get__。
如果添加 setter:
classUser:def__init__(self):self._name=""@propertydefname(self):returnself._name@name.setterdefname(self,value):ifnotvalue:raiseValueError("name 不能为空")self._name=value u=User()u.name="Alice"print(u.name)这里的property就是典型的数据描述符,因为它不只控制读取,还能控制写入。
九、常见误区:函数、方法、绑定方法不是一回事
在 Python 中,这三个概念要分清楚。
1. 函数
类中原始定义的是函数:
classA:deffoo(self):passprint(A.__dict__["foo"])这是:
function2. 未绑定访问
通过类访问:
print(A.foo)得到的仍然主要是函数对象。
调用时需要手动传实例:
a=A()A.foo(a)3. 绑定方法
通过实例访问:
a.foo得到的是绑定方法:
<bound method A.foo of<__main__.Aobjectat...>>此时实例已经被绑定到__self__上。
调用时不需要再传self:
a.foo()十、调试技巧:如何观察方法绑定?
在排查复杂面向对象问题时,可以用下面几个属性:
classService:defprocess(self,data):returndata.upper()s=Service()method=s.processprint(method.__self__)print(method.__func__)print(method.__name__)print(method.__qualname__)输出类似:
<__main__.Serviceobjectat 0x...><function Service.process at 0x...>process Service.process如果你怀疑某个实例方法被覆盖,可以检查:
print(s.__dict__)print(Service.__dict__)例如:
s.process="broken"print(s.__dict__)print(Service.__dict__["process"])你会发现实例字典中出现了同名属性,导致方法访问被覆盖。
十一、最佳实践:如何避免描述符相关问题?
1. 不要让实例属性和方法同名
坏例子:
classJob:defresult(self):return"success"def__init__(self):self.result=None好例子:
classJob:defget_result(self):return"success"def__init__(self):self.result=None或者:
classJob:def__init__(self):self._result=None@propertydefresult(self):returnself._result2. 装饰器中保留函数元信息
写装饰器时建议使用functools.wraps。
importtimefromfunctoolsimportwrapsdeftimer(func):@wraps(func)defwrapper(*args,**kwargs):start=time.perf_counter()result=func(*args,**kwargs)end=time.perf_counter()print(f"{func.__name__}耗时:{end-start:.6f}秒")returnresultreturnwrapperclassCalculator:@timerdefcompute_sum(self,n):returnsum(range(n))c=Calculator()print(c.compute_sum(1_000_000))print(c.compute_sum.__name__)没有wraps时,__name__可能会变成wrapper,这会影响调试、日志、文档生成和某些框架行为。
3. 写自定义描述符时处理类访问
描述符的__get__中一定要考虑:
ifinstanceisNone:returnself例如:
classField:def__get__(self,instance,owner):ifinstanceisNone:returnselfreturninstance.__dict__.get("value")否则通过类访问描述符时,可能出现意料之外的结果。
十二、总结:函数是描述符,是 Python 优雅对象模型的关键拼图
现在我们可以回答标题中的问题了。
Python 中函数为什么也是描述符?
因为 Python 需要一种统一、灵活、可扩展的机制,把类中的普通函数在实例访问时自动转换成绑定方法。
换句话说:
classUser:defgreet(self):pass这里的greet首先是函数对象。
当你访问:
u.greet函数对象的__get__被触发,返回一个绑定了u的方法对象。
于是:
u.greet()等价于:
User.greet(u)这就是self自动传入的真正原因。
描述符不仅解释了实例方法,还解释了:
property staticmethod classmethod ORM 字段 校验器 缓存属性 框架中的依赖注入当你理解“函数也是描述符”之后,你会发现 Python 的很多高级特性不再神秘。它们不是魔法,而是一组优雅协议的组合。
Python 的美,正在于此:表面简单,底层深邃。初学者可以用它快速写出清晰代码,资深开发者也可以沿着这些协议深入语言内核,构建强大、灵活、可维护的系统。
最后留一个问题给你:
你在项目中有没有遇到过“方法突然不能调用”或者“属性覆盖方法”的问题?
下次遇到它,也许就该从描述符和属性查找顺序开始排查了。