深度解密为什么实例在调用方法时会将自身传给 self 参数(二)

简介: 深度解密为什么实例在调用方法时会将自身传给 self 参数(二)

接上篇深度解密为什么实例在调用方法时会将自身传给 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 三个参数,那么此时的运行时栈如下:


f2398f0f4a6611ea588a76d23eac11b5.png


_PyObject_Vectorcall 的第一个参数就是要调用的函数 func;第二个参数是 args,指向给函数 func 传递的首个参数;至于到底给 func 传了多少个,则由第三个参数 nargs 指定。

但如果调用的不是函数,而是方法呢?我们仍以传入 name、age、gender 三个参数为例,解释一下源码的具体细节。

首先是 PyObject **newargs = (PyObject**)args - 1; ,这意味着什么呢?

5965a70cefc186ca711ded5f91686091.png

然后 nargs += 1; 表示参数个数加 1,这很好理解,因为多了一个 self。

PyObject *tmp = newargs[0]; 做的事情也很简单,相当于将 name 的前一个元素保存了起来,赋值为 tmp。

关键来了,newargs[0] = self; 会将 name 的前一个元素设置为实例 self,此时运行时栈如下:

6e93b7aac4132c7f7e0d20ab45f670b7.png

然后调用 _PyObject_Vectorcall,显然第二个参数就变成了 newargs,因为 name 前面多了一个 self,所以现在是 newargs 指向函数 func 的首个参数。而从 Python 的角度来说,就是将实例我们给 func 传入的参数组装了起来。

调用完之后拿到返回值,非常 Happy。但需要注意的是,从内存布局上来讲,参数 name 的前面是没有 self 的容身之处的。而 self 之所以能挤进去,是因为它把参数 name 的前一个元素给顶掉了,至于被顶掉的元素到底是啥我们不得而知,也无需关注,它有可能是 free 区域里面的某个元素。总之关键的是,函数 func 调用完之后,还要再换回来,否则在逻辑上就相当于越界了。

所以通过 newargs[0] = tmp; 将 name 的前一个元素再替换回来。

但相比上面这种做法, 其实还有一个更通用的办法。

b8d5aa4713bcefac1bc0f1fa077793d9.png

将我们传递的参数都向后移动一个位置,然后空出来的第一个位置留给 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 methodunbound 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,所以它的实例对象是一个描述符。

8c2147b4572dede0ee49376c75ab4f09.png

此时调用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 中访问属性对应的逻辑在底层是如何实现的。当然啦,也解析了方法和函数的区别,以及它们在底层的调用方式。

相关文章
|
5月前
|
存储 C语言
C 语言函数完全指南:创建、调用、参数传递、返回值解析
函数是一段代码块,只有在被调用时才会运行。 您可以将数据(称为参数)传递给函数。 函数用于执行某些操作,它们对于重用代码很重要:定义一次代码,并多次使用。
156 3
|
Serverless 开发工具
ABAP 方法调用的参数传递里,通过引用传递的方式,能修改原始参数值吗?
ABAP 方法调用的参数传递里,通过引用传递的方式,能修改原始参数值吗?
ABAP 方法调用的参数传递里,通过引用传递的方式,能修改原始参数值吗?
解决传入的请求具有过多的参数,该服务器支持最多 2100 个参数
解决传入的请求具有过多的参数,该服务器支持最多 2100 个参数
|
9天前
|
JSON 前端开发 数据格式
@RequestMapping运用举例(有源码) 前后端如何传递参数?后端如何接收前端传过来的参数,传递单个参数,多个参数,对象,数组/集合(有源码)
文章详细讲解了在SpringMVC中如何使用`@RequestMapping`进行路由映射,并介绍了前后端参数传递的多种方式,包括传递单个参数、多个参数、对象、数组、集合以及JSON数据,并且涵盖了参数重命名和从URL中获取参数的方法。
10 0
@RequestMapping运用举例(有源码) 前后端如何传递参数?后端如何接收前端传过来的参数,传递单个参数,多个参数,对象,数组/集合(有源码)
|
9天前
|
缓存 Python
深度解密为什么实例在调用方法时会将自身传给 self 参数(一)
深度解密为什么实例在调用方法时会将自身传给 self 参数
21 0
|
2月前
|
存储 前端开发 rax
函数过程的调用
函数过程的调用
数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
|
11月前
在调用一个函数时传递了一个参数,但该函数定义中并未接受任何参数
在调用一个函数时传递了一个参数,但该函数定义中并未接受任何参数
108 2
|
5月前
|
小程序
小程序封装组件简单案例,所有小程序适用(传入参数、外抛事件、传入样式)
小程序封装组件简单案例,所有小程序适用(传入参数、外抛事件、传入样式)
88 0
【学习笔记之我要C】函数的参数与调用
【学习笔记之我要C】函数的参数与调用
131 0