深入理解 python 实例方法、静态方法、类方法

最近在看python源码解析,这里整理一下python关于类机制中关于实例方法、静态方法、类方法的内容,这篇文章不会涉及python源代码,而是从源码的角度进行简要的解析,以求对不同方法的定义和调用过程有清晰的了解。


首先看看我们的研究对象,简单的包含了类的三种方法,调用的话分别输出一句话,大家先记下来这个类:


下面观察python是如何创建一个类的。python虚拟机执行字节码,是以名字空间(作用域)为划分的,在外层名字空间一般是"看不见"内层空间的内容的,也就是说在顺序上,python执行完 class A(object) 这句话之后就转到 next_step = 0 这句话了,dis一下就知道了:


我们关注序号6 MAKE_FUNCTION 、序号 12 CALL_FUNCTION两条字节码,我们不深究这两句话执行了什么内容,从字面上我们就可以理解,python处理 def class A(object)这句话是以类似于函数调用的方式进行的,目的很简单,就是要从当前的名字空间进入到 class 内部的名字空间去。


接下来我们看看转入到class内部的时候要做些什么(这里修改了一下dis的内容),我们关注序号14、序号26、序号36三条MAKE FUNCTION, 序号18、序号30、序号38三条STORE_FAST,我们也从字面上去理解,对于 def 的三条类方法 f、g、h,python统统是建立一个函数,具体来说是建立了一个funcobject,然后把它存起来,最后通过另外一条字节码把这三个funcobject 弹出去,回到 def class A(object)这句话来打个包,这就创建了一个类对象 classobject 出来了。再画一幅图帮助大家理解:

上面是虚拟机创建类的过程,我们来看看创建出来的类到底有些什么,也就是 class A 有些什么属性,所以我们看看A.__dict__:

果然,从A.__dict__可以看到我们刚刚创建出来的三个对应于 f、g、h 的funcobject,也可以看到__init__这个funcobject等等,这里再解释一下,funcobject是对应于 c 一级的 struct 对象,在上面的 function、classmethod、staticmethod 都是由它变出来的。


接下来我们观察实例创建的过程,dis 一下 a = A(1) 这句话,也就是我们平时创建实例的语句:


很简单,重点只有一条,序号20 CALL_FUNCTION,那你可能会问了,这不是个类吗,为什么能调用它,事实上python中只要定义了__call__方法就可以调用,更进一步,如果调用metaclass就会创建一个类,调用一个类就会创建一个实例,也可以理解成 python 对 ()的重载:

讲了这么多,那和类中的方法有什么关系呢,好,接下来我们就看看非常容易迷惑的三种方法的调用:

这里列举了所有的调用的可能,但其实只是一种表象,只要抓住本质,就知道什么情况要怎么调用,我们来观察调用一个实例方法会发生什么事情,我们看下面这个简化版的例子:

dis 一下 a.h() 这条语句:


关注序号26 LOAD_ATTR 和序号 28 CALL_FUNCTION,说了要调用方法嘛,那CALL_FUNCTION没有问题,关键就在于python对于类的属性的搜索过程,也就是要从 A.__dict__ 中找到 f 对应的 funcobject,对了,就是刚刚 class A 创建过程中放进去的三个 funcobject 中的一个。可是这里有一个问题,在类创建的时候还不知道实例是什么,如果直接返回这个funcobject的话,我们平常调用的self参数在哪里呢?所以在这一步 python 还会做一些工作。


这个工作叫做成员函数绑定。这里还要知道一个概念 descriptor,一个类如果实现了__get__和__set__,那么它的实例是 descriptor,类实现了__get__没有实现__set__叫 non data descriptor,两个都实现了就叫 data descriptor,(descriptor的概念初见的话会比较难理解,建议多看几次):


要提及这个是因为当类在搜索属性的时候如果返回一个descriptor,descriptor会返回 __get__() 的结果,而普通的对象则会直接返回:


如果查看在创建类的时候创建的三个 funcobject (函数对象)的类(函数对象的类对象)会发现,这个类实现了__get__,也就是说这三个funcobject是 non data descriptor,看到这里就知道了,哦原来执行 GET_ATTR 的时候返回的是 funcobject 的__get__的结果而不是直接返回的,所谓的成员函数绑定就是在这个__get__里面完成的。


* 这段可以跳过,这里给出类搜索属性的顺序,优先返回 data descriptor 的__get__的结果,如果不是data descriptor,则尝试搜索实例的__dict__,也就是实例的属性,如果也没有,就返回 non data descriptor 的__get__的结果,如果也不是non data descriptor, 就往基类的__dict__中找。我们现在研究的,要返回一个类中的方法,首先它不是一个data descriptor;然后实例的__dict__中没有它,这很自然因为方法是属于类的,不是属于实例的;而它是一个non data descriptor,所以返回__get__的内容,和上面所说的是一致的。


  我们直接来看到底成员函数绑定,到底绑定了什么:


那当然是绑定实例呀!是的,虽然这个__get__的过程非常的曲折,我们直接从结果来看,如果是从实例去搜索,搜索的结果是bound method,这个东西就是从 funcobject 变过来的,只不过有一个专门的域记录了要绑定的实例 a 而已;如果是从类去搜索,搜索的结果就是 function,很不巧这个东西什么都没有绑定。

现在我们可以解释这个错误的原因了,由于从实例 a 搜索出来 h 是绑定了实例 a 的,所以在传递参数的时候第一个参数被自动的传进 a ,也就是self <- a,而从类 A 搜索出来 h 是没有绑定东西的,所以就会报错说缺少了一个参数,如果要正确的调用的话就要给它加一个参数:

OK,最后来解决一下静态方法和类方法,其实@是python的语法糖,我们先把他还原:

staticmethod也是python的内置类,这个类的工作想想就知道,就是把一个东西重新包装的,把什么重新包装?当然是我们从类中搜索出来的结果了,直接上结果:

顾名思义,静态方法,就是把从类中搜索出来的结果统统变成什么都不绑定的function,至于类方法,那就是统统变成绑定了类的bound method:

OK,本质我们就掌握了,现在再看三种方法的调用,应该就很清晰了。对于实例方法,实例调用就绑定了实例,类调用就什么都不绑定。静态方法和类方法本质上是个装饰器,实例或者类调用静态方法都不绑定,实例或者类调用类方法都绑定类。


这篇文章到这里就算结束了,博主是根据《python源码解析》的内容整理出来的,有兴趣的小伙伴也可以阅读,对源码有兴趣的小伙帮也可以查看相关的代码。文中 dis 的部分经过一些修改,只是为了说明内容,而代码部分则经过验证。另外如果有错漏的地方也请大家指出。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值