深度解密为什么实例在调用方法时会将自身传给 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 中访问属性对应的逻辑在底层是如何实现的。当然啦,也解析了方法和函数的区别,以及它们在底层的调用方式。

相关文章
|
存储 数据采集 人工智能
AI时代:云存储加速多模态数据存储与管理创新
阿里云存储产品高级解决方案架构师欧阳雁(乐忱)分享了中国企业在全闪存高端存储市场的快速增长,指出AI大模型的发展推动了企业级存储市场。去年,高端企业级存储闪存占比约为25%,相较于欧美50%的比例,显示出中国在AI领域的巨大增长潜力。演讲涵盖AI业务流程,包括数据预处理、训练和推理的痛点,以及针对这些环节的存储解决方案,强调了稳定、高性能和生命周期管理的重要性。此外,还介绍了数据预处理的全球加速和弹性临时盘技术,训练阶段的高性能存储架构,推理场景的加速器和AI Agent的应用,以及应对大数据业务的存储考量,如对象存储、闪电立方和冷归档存储产品。
40041 20
|
关系型数据库 MySQL 数据库
Linux C/C++ 开发(学习笔记七):Mysql数据库C/C++编程实现 插入/读取/删除
Linux C/C++ 开发(学习笔记七):Mysql数据库C/C++编程实现 插入/读取/删除
523 0
Python中文件操作的详细使用:open()、os.open()和with open()
Python中文件操作的详细使用:open()、os.open()和with open()
|
SQL 关系型数据库 MySQL
一次性导入千万级数据到Mysql(附源码)
MySql数据迁移、导入,在我们日常开发中,可以说是经常碰到。如果数据量比较小,一般都没什么问题,但是如果是涉及到千万级、亿级的数据量大数据量迁移,这里面就涉及到一个问题:如何快速导入千万数据到MySQL。 下面我们通过对比3种方法,来谈谈MySQL怎么高性能插入千万级的数据。
2635 0
|
JSON 中间件 C#
氚云的第三方接口调用| 学习笔记
快速学习氚云的第三方接口调用。
|
缓存 Python
深度解密为什么实例在调用方法时会将自身传给 self 参数(一)
深度解密为什么实例在调用方法时会将自身传给 self 参数
173 0
|
存储 安全 数据安全/隐私保护
ERP系统的灾备与数据恢复:保障企业业务连续性
【7月更文挑战第29天】 ERP系统的灾备与数据恢复:保障企业业务连续性
634 2
|
Kubernetes 应用服务中间件 nginx
Kubernetes上安装Metallb和Ingress并部署应用程序
Kubernetes上安装Metallb和Ingress并部署nginx应用程序,使用LoadBalancer类型的KubernetesService
915 107
|
Shell Python
如何将PyCharm中的终端运行前面的PS如何修改成当前环境
这篇文章介绍了如何在PyCharm的终端中修改命令提示符(PS)以反映当前激活的环境,通过更改PyCharm设置中的Shell Path实现。
|
前端开发 Java 数据库连接
35个项目,开源,开源!
35个项目,开源,开源!
1424 0
35个项目,开源,开源!