接上篇深度解密为什么实例在调用方法时会将自身传给 self 参数(一):https://developer.aliyun.com/article/1617398?spm=a2c6h.13148508.setting.19.72964f0erDh2MC
方法调用
通过字节码,我们知道 LOAD_METHOD 指令结束之后,便开始执行CALL_METHOD。它和 CALL_FUNCTION 之间最大的区别就是:
- CALL_METHOD 针对的是 PyMethodObject 对象;
- CALL_FUNCTION 针对的是 PyFunctionObject 对象。
但是这两个指令调用的都是 call_function 函数,然后内部执行的也都是 Girl.get_info。因为执行方法,本质上还是执行方法里面的 im_func,只不过会自动将 im_self 和我们传递的参数组合起来,一起传给 im_func。
假设 obj 是 cls 的实例对象,那么 obj.xxx() 在底层会被翻译成 cls.xxx(obj),前者只是后者的语法糖。
然后在 PyMethod_New 中,我们看到虚拟机给 im->vectorcall 赋值为 method_vectorcall,而方法调用的秘密就隐藏在里面。
// classobject.c static PyObject * method_vectorcall(PyObject *method, PyObject *const *args, size_t nargsf, PyObject *kwnames) { assert(Py_TYPE(method) == &PyMethod_Type); PyObject *self, *func, *result; //实例对象 self self = PyMethod_GET_SELF(method); //方法里面的成员函数 func = PyMethod_GET_FUNCTION(method); //参数个数 Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); //... //这里的代码比较有趣,一会单独说 //总之它的逻辑就是将 self 和我们传递的参数组合起来 //通过 _PyObject_Vectorcall 对 func 进行调用 //所以 method_vectorcall 只是负责组装参数 //真正执行的依旧是 PyFunctionObjec 的 _PyObject_Vectorcall PyObject **newargs = (PyObject**)args - 1; nargs += 1; PyObject *tmp = newargs[0]; newargs[0] = self; result = _PyObject_Vectorcall(func, newargs, nargs, kwnames); newargs[0] = tmp; //... return result; }
再来说说里面的具体细节,假设我们调用的不是方法,而是一个普通的函数,并且依次传入了 name、age、gender 三个参数,那么此时的运行时栈如下:
_PyObject_Vectorcall 的第一个参数就是要调用的函数 func;第二个参数是 args,指向给函数 func 传递的首个参数;至于到底给 func 传了多少个,则由第三个参数 nargs 指定。
但如果调用的不是函数,而是方法呢?我们仍以传入 name、age、gender 三个参数为例,解释一下源码的具体细节。
首先是 PyObject **newargs = (PyObject**)args - 1; ,这意味着什么呢?
然后 nargs += 1; 表示参数个数加 1,这很好理解,因为多了一个 self。
PyObject *tmp = newargs[0]; 做的事情也很简单,相当于将 name 的前一个元素保存了起来,赋值为 tmp。
关键来了,newargs[0] = self; 会将 name 的前一个元素设置为实例 self,此时运行时栈如下:
然后调用 _PyObject_Vectorcall,显然第二个参数就变成了 newargs,因为 name 前面多了一个 self,所以现在是 newargs 指向函数 func 的首个参数。而从 Python 的角度来说,就是将实例和我们给 func 传入的参数组装了起来。
调用完之后拿到返回值,非常 Happy。但需要注意的是,从内存布局上来讲,参数 name 的前面是没有 self 的容身之处的。而 self 之所以能挤进去,是因为它把参数 name 的前一个元素给顶掉了,至于被顶掉的元素到底是啥我们不得而知,也无需关注,它有可能是 free 区域里面的某个元素。总之关键的是,函数 func 调用完之后,还要再换回来,否则在逻辑上就相当于越界了。
所以通过 newargs[0] = tmp; 将 name 的前一个元素再替换回来。
但相比上面这种做法, 其实还有一个更通用的办法。
将我们传递的参数都向后移动一个位置,然后空出来的第一个位置留给 self,这样也是可以的。但很明显,此做法的效率不高,因为这是一个 O(N) 操作,而源码中的做法是 O(1)。
所以底层实现一定要讲究效率,采用各种手段极限优化。因为 Python 语言的设计模式就决定了它的运行效率注定不高,如果虚拟机源码再写的不好的话,那么运行速度就真的不能忍了。
总结一下上面内容,函数调用和方法调用本质上是一样的。方法里面的成员 im_func 指向一个函数,调用方法的时候底层还是会调用函数,只不过在调用的时候会自动把方法里面的 im_self 作为第一个参数传到函数里面去。而类在调用的时候,所有的参数都需要手动传递。
还是那句话:obj.xxx() 本质上就是 cls.xxx(obj);而 cls.xxx() 仍是 cls.xxx()。
因此到了这里,我们可以在更高的层次俯视一下Python的运行模型了,最核心的模型非常简单,可以简化为两条规则:
- 1)在某个名字空间中寻找符号对应的对象
- 2)对得到的对象进行某些操作
抛开面向对象这些花里胡哨的外表,其实我们发现自定义类对象就是一个名字空间,实例对象也是一个名字空间。只不过这些名字空间通过一些特殊的规则连接在一起,使得符号的搜索过程变得复杂,从而实现了面向对象这种编程模式。
bound method 和 unbound method
当对成员函数进行引用时,会有两种形式:bound method 和 unbound method。
- bound method:被绑定的方法,说白了就是方法,PyMethodObject。比如实例获取成员函数,拿到的就是方法。
- unbound method:未被绑定的方法,说白了就是成员函数本身。比如类获取成员函数,拿到的还是成员函数本身,只不过对应的指令也是 LOAD_METHOD,所以叫未被绑定的方法。
因此 bound method 和 unbound method 的本质区别就在于成员函数有没有和实例绑定在一起,成为方法。前者完成了绑定动作,而后者没有完成绑定动作。
//funcobject.c static PyObject * func_descr_get(PyObject *func, PyObject *obj, PyObject *type) { //obj:相当于 __get__ 里面的 instance //type:相当于 __get__ 里面的 owner //类获取成员函数,obj 为空,直接返回成员函数 //所以它也被称为是 "未被绑定的方法" if (obj == Py_None || obj == NULL) { Py_INCREF(func); return func; } //实例获取,则会先通过 PyMethod_New //将成员函数 func 和实例 obj 绑定在一起 //返回的结果被称为 "被绑定的方法",简称方法 //而 func 会交给方法的 im_func 成员保存 //obj 则会交给方法的 im_self 成员保存 //im_func和im_self对应Python里面的 __func__和__self__ return PyMethod_New(func, obj); }
我们用 Python 演示一下:
class Girl(object): def get_info(self): print(self) g = Girl() Girl.get_info(123) # 123 #我们看到即便传入一个 123 也是可以的 #这是我们自己传递的,传递什么就是什么 g.get_info() # <__main__.A object at 0x00...> #但是 g.get_info() 就不一样了 #它是 Girl.get_info(g) 的语法糖 #被绑定的方法,说白了就是方法 #方法的类型为 <class 'method'>,在底层对应 &PyMethod_Type print(g.get_info) # <bound method Girl.get_info of ...> print(g.get_info.__class__) # <class 'method'> #未被绑定的方法,这个叫法只是为了和"被绑定的方法"形成呼应 #但说白了它就是个成员函数,类型为 <class 'function'> print(Girl.get_info) # <function Girl.get_info at 0x00...> print(Girl.get_info.__class__) # <class 'function'>
我们说成员函数和实例绑定,会得到方法,这是没错的。但是成员函数不仅仅可以和实例绑定,和类绑定也是可以的。
class Girl(object): @classmethod def get_info(cls): print(cls) print(Girl.get_info) print(Girl().get_info) """ <bound method Girl.get_info of <class '__main__.Girl'>> <bound method Girl.get_info of <class '__main__.Girl'>> """ # 无论实例调用还是类调用 # 第一个参数传进去的都是类 Girl.get_info() Girl().get_info() """ <class '__main__.Girl'> <class '__main__.Girl'> """
此时通过类去调用得到的不再是一个函数,而是一个方法,这是因为我们加上了classmethod装饰器。加上装饰器之后,get_info 就不再是原来的函数了,而是 classmethod(get_info),也就是 classmethod 的实例对象。
首先 classmethod 在 Python 里面是一个类,它在底层对应的是 &PyClassMethod_Type;而 classmethod 的实例对象在底层对应的结构体也叫 classmethod。
typedef struct { PyObject_HEAD PyObject *cm_callable; PyObject *cm_dict; } classmethod;
由于 &PyClassMethod_Type 内部实现了 tp_descr_get,所以它的实例对象是一个描述符。
此时调用get_info会执行的 __get__。
然后看一下 cm_descr_get 的具体实现:
//funcobject.c static PyObject * cm_descr_get(PyObject *self, PyObject *obj, PyObject *type) { //这里的self就是 Python 里面的类 classmethod 的实例 //只不过在虚拟机中,它的实例也叫 classmethod classmethod *cm = (classmethod *)self; if (cm->cm_callable == NULL) { PyErr_SetString(PyExc_RuntimeError, "uninitialized classmethod object"); return NULL; } //如果 type 为空,让 type = Py_TYPE(obj) //所以不管是类调用还是实例调用,第一个参数都是类 if (type == NULL) type = (PyObject *)(Py_TYPE(obj)); return PyMethod_New(cm->cm_callable, type); }
所以当类在调用的时候,类也和函数绑定起来了,因此也会得到一个方法。不过被 classmethod 装饰之后,即使是实例调用,第一个参数传递的还是类本身,因为和函数绑定的是类、而不是实例。
但不管和函数绑定的是类还是实例,绑定之后的结果都叫方法。所以得到的究竟是函数还是方法,就看这个函数有没有和某个对象进行绑定,只要绑定了,那么它就会变成方法。
至于调用我们就不赘述了,上面已经说过了。不管和函数绑定的是实例还是类,调用方式不变,唯一的区别就是第一个参数不同。
小结
以上我们就探讨了为什么实例调用方法时,会自动将自身传给 self,说白了就是因为描述符机制。像 property、staticmethod、classmethod 等等都是通过描述符来实现的,描述符在 Python 里面是一个很强大的机制,但使用的频率却不高,更多的是在一些框架的源码中出现。
然后我们还探讨了属性引用,查看了 Python 中访问属性对应的逻辑在底层是如何实现的。当然啦,也解析了方法和函数的区别,以及它们在底层的调用方式。