news 2026/6/21 3:19:22

函数为什么也是描述符?从 `obj.method()` 彻底理解 Python 方法绑定机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
函数为什么也是描述符?从 `obj.method()` 彻底理解 Python 方法绑定机制

函数为什么也是描述符?从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.value

Python 并不是简单地从对象字典里拿值,而是发现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.greet

Python 实际上会调用:

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做的事情,本质上就是普通函数在类中作为方法时做的事情:

  1. 如果通过类访问,返回原始函数;
  2. 如果通过实例访问,返回绑定了实例的方法对象;
  3. 调用绑定方法时,自动把实例作为第一个参数传入。

这就是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.staticmethodclassmethod本质也是描述符

我们平时写的这些装饰器,其实也依赖描述符机制。

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"])

这是:

function

2. 未绑定访问

通过类访问:

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._result

2. 装饰器中保留函数元信息

写装饰器时建议使用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 的美,正在于此:表面简单,底层深邃。初学者可以用它快速写出清晰代码,资深开发者也可以沿着这些协议深入语言内核,构建强大、灵活、可维护的系统。

最后留一个问题给你:

你在项目中有没有遇到过“方法突然不能调用”或者“属性覆盖方法”的问题?
下次遇到它,也许就该从描述符和属性查找顺序开始排查了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/16 11:14:56

SVM数学直觉解析:从最大间隔到核技巧的工程本质

1. 这不是公式堆砌&#xff0c;而是你真正能“看见”的SVM数学 我带过不少刚接触机器学习的工程师和研究生&#xff0c;他们第一次看SVM推导时&#xff0c;常被一堆拉格朗日乘子、对偶问题、核函数绕得头晕。有人抄下公式就跑&#xff0c;结果调参像抓瞎&#xff1b;有人死磕凸…

作者头像 李华