打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

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

楔子


我们都知道实例在调用方法时,会自动将自身传给 self 参数,那么你有没有想过这背后的原理是怎么样的呢?

本篇文章就来探讨一下这背后的原理。


属性引用


在调用方法之前肯定要先获取,而实例在获取属性(或方法)的时候,需要通过属性操作符 . 的方式,我们看一下这个过程是怎样的?

class Girl:
    
    def __init__(self):
        self.name = "satori"
        self.age = 16
        
    def get_info(self):
        return f"name: {self.name}, age: {self.age}"
    
g = Girl()
# 获取 name 属性
name = g.name
# 获取 get_info 方法并调用
g.get_info()

想要了解背后都发生了什么,最直接的途径就是查看字节码,这里只看模块对应的字节码。

 # 偏移量为 0 ~ 12 的字节码是用来创建类的
 # 这部分以后再聊,总之这几条字节码执行完毕之后
 # Girl 这个类就已经创建好了
 0 LOAD_BUILD_CLASS
 2 LOAD_CONST               0 (<code object Girl at 0x00...>)
 4 LOAD_CONST               1 ('Girl')
 6 MAKE_FUNCTION            0
 8 LOAD_CONST               1 ('Girl')
10 CALL_FUNCTION            2
12 STORE_NAME               0 (Girl)
 
 # g = Girl() 对应的字节码
 # 将类型对象 Girl 压入运行时栈
14 LOAD_NAME                0 (Girl)
 # 将 Girl 从运行时栈弹出,调用生成实例对象
16 CALL_FUNCTION            0
 # 将实例对象用变量 g 保存起来
18 STORE_NAME               1 (g)
 
 # name = g.name 对应的字节码
 # 加载变量 g
20 LOAD_NAME                1 (g)
 # 获取 g.name,加载属性用的是 LOAD_ATTR
22 LOAD_ATTR                2 (name)
 # 将结果交给变量 name 保存
24 STORE_NAME               2 (name)
 
 # g.get_info() 对应的字节码
 # 加载变量 g
26 LOAD_NAME                1 (g)
 # 获取方法 g.get_info,加载方法用的是 LOAD_METHOD
28 LOAD_METHOD              3 (get_info)
 # 调用方法,注意:调用方法对应的指令是 CALL_METHOD
 # 而调用函数对应的指令是 CALL_FUNCTION
30 CALL_METHOD              0
 # 从栈顶弹出返回值
32 POP_TOP
 # return None
34 LOAD_CONST               2 (None)
36 RETURN_VALUE

除了 LOAD_METHOD 和 LOAD_ATTR,其它的指令基本都见过了,也很简单,因此下面重点分析这两条指令。

case TARGET(LOAD_METHOD){
    //从符号表中获取符号,因为是 g.get_info
    //那么这个 name 就指向字符串对象 "get_info"
    PyObject *name = GETITEM(names, oparg);
    //从栈顶获取元素obj,显然这个 obj 就是代码中的实例对象 g
    PyObject *obj = TOP();
    //meth 是一个 PyObject * 指针
    //显然它要指向一个方法
    PyObject *meth = NULL;
    
    //这里是获取和 "get_info" 绑定的方法,然后让meth指向它
    //具体做法就是调用 _PyObject_GetMethod,传入二级指针&meth
    //然后让 meth 存储的地址变成指向具体方法的地址
    int meth_found = _PyObject_GetMethod(obj, name, &meth);
    
    //如果 meth == NULL,raise AttributeError
    if (meth == NULL) {
        /* Most likely attribute wasn't found. */
        goto error;
    }
    
    //注意:无论是 Girl.get_info、还是 g.get_info
    //对应的指令都是 LOAD_METHOD
    //类去调用的话,说明得到的是一个未绑定的方法,说白了就等价于函数
    //实例去调用的话,会得到一个绑定的方法,相当于对函数进行了封装
    //关于绑定和未绑定我们后面会详细介绍
    if (meth_found) {
        //如果 meth_found 为 1
        //说明 meth 是一个绑定的方法,obj 就是 self
        //将 meth 设置为栈顶元素,然后再将 obj 压入栈中
        SET_TOP(meth);
        PUSH(obj);  // self
    }
    else {
        //否则说明 meth 是一个未绑定的方法
        //那么将栈顶元素设置为 NULL,然后将 meth 压入栈中
        SET_TOP(NULL);
        Py_DECREF(obj);
        PUSH(meth);
    }
    DISPATCH();
}

获取方法是 LOAD_METHOD 指令 ,获取属性则是 LOAD_ATTR 指令,来看一下。

case TARGET(LOAD_ATTR){
    //可以看到和 LOAD_METHOD 本质上是类似的,但更简单一些
    //name 依旧是符号,这里指向字符串对象 "name"
    PyObject *name = GETITEM(names, oparg);
    //从栈顶获取变量 g
    PyObject *owner = TOP();
    //res 显然就是获取属性返回的结果了,即 g.name
    //通过 PyObject_GetAttr 进行获取
    PyObject *res = PyObject_GetAttr(owner, name);
    Py_DECREF(owner);
    //设置到栈顶
    SET_TOP(res);
    if (res == NULL)
        goto error;
    DISPATCH();
}

所以这两个指令本身是很简单的,而核心在 PyObject_GetAttr 和  _PyObject_GetMethod 上面,前者用于获取属性、后者用于获取方法。

来看一下 PyObject_GetAttr 具体都做了什么事情。

//Objects/object.c
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{   
    // v: 对象
    // name: 属性名
    
    // 获取实例对象 v 对应的类型对象
    PyTypeObject *tp = Py_TYPE(v);
    
    // name 必须是一个字符串
    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    // 通过类型对象的 tp_getattro 成员获取实例对应的属性
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
    
    //tp_getattro 和 tp_getattr 功能一样,但前者可以支持中文
    if (tp->tp_getattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL)
            return NULL;
        return (*tp->tp_getattr)(v, (char *)name_str);
    }
    
    //属性不存在,抛出异常
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return NULL;
}

PyTypeObject 里面定义了两个与属性访问相关的操作:tp_getattro 和 tp_getattr。其中前者是优先选择的属性访问动作,而后者已不推荐使用。

这两者的区别在 PyObject_GetAttr 中已经显示很清楚了,主要是在属性名的使用上。

  • tp_getattro 所使用的属性名是一个 PyUnicodeObject *;

  • 而 tp_getattr 所使用的属性名是一个 char *。

如果这两个成员同时被定义,那么优先使用 tp_getattro。

问题来了,自定义类对象的 tp_getattro 对应哪一个 C 函数呢?显然我们要去找 object。

object 在底层对应 PyBaseObject_Type,它的 tp_getattro 为 PyObject_GenericGetAttr,因此虚拟机在创建 Girl 这个类时,也会将此操作继承下来。

//Objects/object.c
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(
                 obj, name, NULL0);
}

PyObject *
_PyObject_GenericGetAttrWithDict(
                PyObject *obj, PyObject *name,
                PyObject *dict, int suppress)
{
    //拿到 obj 的类型对象
    //对于我们当前的例子来说,显然是 class Girl
    PyTypeObject *tp = Py_TYPE(obj);
    //描述符
    PyObject *descr = NULL;
    //返回值
    PyObject *res = NULL;
    //描述符的 __get__ 
    descrgetfunc f;
    Py_ssize_t dictoffset;
    PyObject **dictptr;
    
    //name 必须是字符串
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    Py_INCREF(name);
    
    //...
    
    //从 mro 列表中获取属性对应的值,并检测是否为描述符
    //如果属性不存在、或者存在但对应的值不是描述符,则返回 NULL
    descr = _PyType_Lookup(tp, name);

    f = NULL;
    if (descr != NULL) {
        Py_INCREF(descr);
        //如果 descr 不为 NULL,说明该属性被代理了
        //descr 是描述符,f 就是它的 __get__
        //f = descr.__class__.__get__ 
        f = descr->ob_type->tp_descr_get;
        //补充:类型对象在底层都是 PyTypeObject
        //它有两个成员:tp_descr_get 和 tp_descr_set
        //tp_descr_get 对应 Python 中的 __get__
        //tp_descr_set 对应 Python 中的 __set__ 

        //如果 f 不为 NULL,并且 descr 是数据描述符
        if (f != NULL && PyDescr_IsData(descr)) {
            //那么直接调用描述符的 __get__ 方法,返回结果
            res = f(descr, obj, (PyObject *)obj->ob_type);
            //...
        }
    }
    
    //走到这说明要获取的属性没有被代理,或者说代理它的是非数据描述符
    //那么实例优先从自身的属性字典中获取,当然还有一种情况
    //就是属性被数据描述符代理,但是该数据描述符没有 __get__
    //那么仍会优先从实例对象自身的 __dict__ 中寻找属性
    
    if (dict == NULL) {
        // ...
    }
    //dict 不为 NULL,从字典中获取
    if (dict != NULL) {
        // ...
    }
    
    //如果程序走到这里,说明什么呢?
    //显然意味着实例的属性字典里面没有要获取的属性
    //但如果下面的 f != NULL 成立,说明属性被代理了
    //并且代理属性的描述符是非数据描述符,它的优先级低于实例
    //所以实例会先到自身的属性字典中查找,找不到再去执行描述符的 __get__
    if (f != NULL) {
        //第一个参数是描述符本身,也就是 __get__ 里面的 self
        //第二个参数是实例对象,也就是 __get__ 里面的 instance
        //第三个参数是类对象,也就是 __get__ 里面的 owner
        res = f(descr, obj, (PyObject *)Py_TYPE(obj));
        // ...
        goto done;
    }
    
    //程序能走到这里,说明属性字典里面没有要找的属性
    //并且也没有执行描述符的 __get__
    //但如果 describe 还不为 NULL,这说明什么呢?
    //显然该属性仍被描述符代理了,只是这个描述符没有 __get__
    //如果是这种情况,那么会返回描述符本身
    if (descr != NULL) {
        res = descr;
        descr = NULL;
        goto done;
    }
    
    //找不到,就报错
    if (!suppress) {
        PyErr_Format(PyExc_AttributeError,
                     "'%.50s' object has no attribute '%U'",
                     tp->tp_name, name);
    }
  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

代码有点长,但是逻辑不难理解,用一张流程图总结一下。

其实难点在于里面涉及了很多描述符相关的知识,这里为了方便理解,我们对描述符的内容做一个补充。

如果一个类定义了 __get__ 或 __set__,那么它的实例对象就被称为描述符。并且定义了 __set__ 被称为数据描述符,只有 __get__ 而没有 __set__ 则被称为非数据描述符。

1)如果实例的某个属性被数据描述符代理,那么实例在设置属性和访问属性时,会执行描述符的 __set__ 和 __get__,举个例子。

class Descr:

    def __get__(self, instance, owner):
        print("__get__")
        print(instance)
        print(owner)

    def __set__(self, instance, value):
        print("__set__")
        print(instance)
        print(value)

class Girl:
    # name 属性被数据描述符代理了
    name = Descr()


g = Girl()
# 设置属性时,会执行描述符的 __set__
# 参数 instance 就是实例本身,value 则是赋的值
g.name = "古明地觉"
"""
__set__
<__main__.Girl object at 0x7fcf180d9d60>
古明地觉
"""


# 获取属性时,会执行描述符的 __get__
# 参数 instance 也是实例本身,owner 则是实例对应的类
g.name
"""
__get__
<__main__.Girl object at 0x7f9ca8269d60>
<class '__main__.Girl'>
"""

# 在获取属性时,不管实例有没有 name 这个属性
# 只要被数据描述符代理,获取的时候都无条件地执行描述符的 __get__

2)如果实例的某个属性被非数据描述符代理,那么实例在设置属性时,会设置到自己的属性字典里面。但在获取属性时,如果自身存在该属性,那么直接获取,不存在则执行描述符的 __get__;

class Descr:

    def __get__(self, instance, owner):
        print("__get__")
        print(instance)
        print(owner)

class Girl:
    # name 属性被非数据描述符代理
    name = Descr()


g = Girl()

# 由于不存在 name 属性,所以依旧执行描述符的 __get__
g.name
"""
__get__
<__main__.Girl object at 0x7fe9d80e1d60>
<class '__main__.Girl'>
"""


# 此时会将 name 属性设置到属性字典中
# 因为代理它的是非数据描述符
g.name = "古明地觉"
# 由于是非数据描述符,那么当属性字典中存在时
# 就不会再走描述符的 __get__ 了
print(g.name)  # 古明地觉

3)如果代理属性的描述符只有 __set__,没有 __get__,那么设置属性时依旧执行描述符的 __set__。但获取属性时,如果实例自身存在该属性,那么就从实例自身获取,如果不存在,则返回描述符本身。

class Descr:

    def __set__(self, instance, value):
        print("__set__")
        print(instance)
        print(value)

class Girl:
    # name 被数据描述符代理
    name = Descr()


g = Girl()

# 设置 name 属性,依旧执行描述符的 __set__
g.name = "古明地恋"
"""
__set__
<__main__.Girl object at 0x7fd8181e9d60>
古明地恋
"""


# 注意:我们在 __set__ 中只是做了几行打印
# 没有修改实例本身,所以实例对象 g 中不存在属性 name
# 而代理 name 属性的是数据描述符,因此访问的时候本应执行 __get__
# 但描述符没有 __get__,而且实例自身也不存在 name 属性
# 因此,这种情况下会返回描述符本身,因为代理 name 的是一个描述符
print(g.name)
"""
<__main__.Descr object at 0x7fd808077940>
"""


# 但如果实例有 name 属性,那么会从实例自身查找
# 虽然数据描述符的优先级大于实例,访问属性时应该执行 __get__
# 但问题是描述符没有 __get__,此时只能从实例自身查找了
# 如果实例也没有,则直接返回描述符本身,就像上面那样
g.__dict__["name"] = "古明地恋"
# 需要通过属性字典的方式来设置,否则会执行 __set__
print(g.name)
"""
古明地恋
"""

以上是属性被描述符代理的情况,如果没有被代理,那么访问和设置属性就会直接作用在实例上面。设置属性等价于往实例的属性字典中添加一个键值对,获取属性则等价于从字典中拿到 key 对应的 value。

所以 PyObject_GenericGetAttr 函数的代码看似很长,但它的逻辑是很简单的,说白了它就是Python 里面访问属性操作所对应的 C 实现,因为 Python 解释器就是 C 写出来的。

获取方法则通过 _PyObject_GetMethod,过程与之类似,这里就不再看了。


函数变身


了解完属性引用之后,终于到我们的主题了,那就是 self 参数。

class Girl:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_info(self):
        return f"name = {self.name}, age = {self.age}"

g = Girl("satori"16)
res = g.get_info()
print(res)  # name = satori, age = 16

我们在调用 g.get_info 的时候,并没有给 self 传递参数,那么 self 到底是不是一个真正有效的参数呢?还是说它仅仅只是一个语法意义上的占位符而已?

不用想,self 肯定是货真价实的参数,只不过自动帮你传递了。根据使用 Python 的经验,我们知道第一个参数就是实例本身。那么这是怎么实现的呢?想要弄清这一点,还是要从字节码入手。而调用方法的字节码是 CALL_METHOD,那么玄机就隐藏在这里面。

调用时的指令参数是 0,表示不需要传递参数。注意:这里说的不需要传递参数,指的是不需要我们手动传递。

case TARGET(CALL_METHOD){
    PyObject **sp, *res, *meth;
    //栈指针,指向运行时栈的栈顶
    sp = stack_pointer;

    meth = PEEK(oparg + 2);
    //meth 为 NULL,说明是函数
    //我们传递的参数从 orarg 开始
    if (meth == NULL) {
        res = call_function(tstate, &sp, oparg, NULL);
        stack_pointer = sp;
        (void)POP(); /* POP the NULL. */
    }
    //否则是方法,我们传递的参数从 oparg + 1开始
    //而第一个参数显然要留给 self
    else {
        res = call_function(tstate, &sp, oparg + 1NULL);
        stack_pointer = sp;
    }

    PUSH(res);
    if (res == NULL)
        goto error;
    DISPATCH();
}

为了对比,我们再把 CALL_FUNCTION 指令的源码贴出来。

case TARGET(CALL_FUNCTION){
    PREDICTED(CALL_FUNCTION);
    PyObject **sp, *res;
    sp = stack_pointer;
    res = call_function(tstate, &sp, oparg, NULL);
    stack_pointer = sp;
    PUSH(res);
    if (res == NULL) {
        goto error;
    }
    DISPATCH();
}

通过对比发现了端倪,这两者都调用了call_function,但是传递的参数不一样。

如果是类调用,那么这两个指令是等价的;但如果是实例调用,CALL_METHOD 的第三个参数是 oparg + 1,CALL_FUNCTION 则是 oparg。

但是这还不足以支持我们找出问题所在,如果你仔细看一下函数的类型对象 PyFunction_Type,会发现里面隐藏着一个秘密。

PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "function",
    sizeof(PyFunctionObject),
    //...
    //...
    
    //注意注意注意,看下面这行
    func_descr_get,                          /* tp_descr_get */
    0,                                       /* tp_descr_set */
    offsetof(PyFunctionObject, func_dict),   /* tp_dictoffset */
    0,                                       /* tp_init */
    0,                                       /* tp_alloc */
    func_new,                                /* tp_new */
};

我们说 tp_descr_get 对应 __get__,而它被设置成了 func_descr_get,这意味着函数是一个描述符,因为它的类型对象实现了 __get__。

def func():
    pass

print(func.__get__)
"""
<method-wrapper '__get__' of function object at 0x...>
"""

同理,实例对象 g 在调用 get_info 之前,肯定要先获取 get_info。而在获取的时候,显然会执行 get_info 的 __get__。也就是说,g.get_info 会得到什么,取决于 get_info 的 __get__ 会返回什么。

那么函数的 __get__ 会返回什么呢?显然这要去 func_descr_get 函数中一探究竟。

// funcobject.c
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{  
    //如果是类获取函数:那么obj为NULL,type为类对象本身
    //如果是实例获取函数:那么obj为实例,type仍是类对象本身
    
    //如果obj为空,说明是类获取
    //那么直接返回func本身, 也就是原来的函数
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    //如果是实例对象,那么调用 PyMethod_New 
    //将函数和实例绑定在一起,得到一个 PyMethodObject 对象 
    return PyMethod_New(func, obj);
}

函数对应的结构体是 PyFunctionObject,那么 PyMethodObject 是啥应该不需要我说了,显然就是方法对应的结构体。所以类里面的定义的就是单纯的函数,通过类去调用的话,和调用一个普通函数并无区别。

但是实例调用就不一样了,实例在拿到类的成员函数时,会先调用 PyMethod_New 将函数包装成方法,然后再对方法进行调用。

class Girl:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_info(self):
        return f"name = {self.name}, age = {self.age}"

g = Girl("satori"16)
print(Girl.get_info.__class__)
print(g.get_info.__class__)
"""
<class 'function'>
<class 'method'>
"""

在获取 get_info 时,会发现它被描述符代理了,而描述符就是成员函数本身。因为类型对象 PyFunction_Type 实现了 tp_descr_get,即 __get__,所以它的实例对象(函数)本质上就是个描述符。

因此无论是类还是实例,在调用时都会执行 func_descr_get。如果是类调用,那么实例 obj 为空,于是会将成员函数直接返回,因此类调用的就是函数本身。

如果是实例调用,则执行 PyMethod_New,将 PyFunctionObject 包装成 PyMethodObject,然后调用。因此,实例调用的是方法。

那么问题来了,方法在底层长什么样呢?可以肯定的是,方法也是一个对象,一个 PyObject。

//classobject.h
typedef struct {
    PyObject_HEAD
    //可调用的PyFunctionObject对象
    PyObject *im_func;  
    //self参数,instance对象
    PyObject *im_self;   
    //弱引用列表,不做深入讨论
    PyObject *im_weakreflist;
    //速度更快的矢量调用
    //因为方法和函数一样,肯定是要被调用的
    //所以它们都自己实现了一套调用方式:vectorcallfunc
    //而没有走类型对象的 tp_call
    vectorcallfunc vectorcall;
} PyMethodObject;

所以方法就是对函数的一个封装,我们用 Python 举例说明:

class Girl:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_info(self):
        return f"name = {self.name}, age = {self.age}"

g = Girl("satori"16)

# 方法是对函数的封装
# 只不过里面不仅仅有函数,还有实例
method = g.get_info
# 拿到的是实例本身
print(method.__self__ is g)  # True
# 拿到是成员函数,也就是 Girl.get_info
print(method.__func__ is Girl.get_info)  # True

print(
    method()
    == 
    Girl.get_info(g)
    ==
    method.__func__(method.__self__)
)  # True

而方法是在 PyMethod_New 中创建的,再来看看这个函数。

//classobjet.c
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{   
    PyMethodObject *im; 
    if (self == NULL) {
        PyErr_BadInternalCall();
        return NULL;
    }
    im = free_list;
    //缓存池
    if (im != NULL) {
        free_list = (PyMethodObject *)(im->im_self);
        (void)PyObject_INIT(im, &PyMethod_Type);
        numfree--;
    }
    //缓冲池如果空了,直接创建PyMethodObject对象
    else {
        //可以看到方法的类型在底层是 &PyMethod_Type
        im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
        if (im == NULL)
            return NULL;
    }
    im->im_weakreflist = NULL;
    Py_INCREF(func);
    //im_func指向PyFunctionObject对象
    im->im_func = func; 
    Py_XINCREF(self);
    //im_self指向实例对象
    im->im_self = self;
    //会通过method_vectorcall来对方法进行调用
    im->vectorcall = method_vectorcall;
    //被 GC 跟踪
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;
}

在PyMethod_New中,分别将im_func,im_self 设置为函数、实例。因此通过 PyMethod_New 将函数、实例结合在一起,得到的 PyMethodObject 就是我们说的方法。并且我们还看到了 free_list,说明方法也使用了缓存池。

所以不管是类还是实例,获取成员函数时都会走描述符的 func_descr_get,然后在里面会判断是类获取还是实例获取。如果是类获取,会直接返回函数本身;如果是实例获取,则通过 PyMethod_New 将函数和实例绑定起来得到方法,这个过程称为成员函数的绑定

当然啦,调用方法本质上还是调用方法里面的 im_func,也就是函数。只不过会处理自动传参的逻辑,将内部的 im_self(实例)和我们传递的参数组合起来(如果没有传参,那么只有一个 im_self),然后整体传递给 im_func。

所以为什么实例调用方法的时候会自动传递第一个参数,此刻算是真相大白了。当然啦,以上只能说从概念上理解了,但是源码还没有看,下面就来看看具体的实现细节。


方法调用


通过字节码,我们知道 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会执行<class 'classmethod'>的 __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 中访问属性对应的逻辑在底层是如何实现的。当然啦,也解析了方法和函数的区别,以及它们在底层的调用方式。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Python yield与实现
译|Python幕后(1):CPython VM 原理
C++开发实战(三):通过python调用C++接口
Python进阶系列(十四)
C++调用Python(3)
使用C/C++ 扩展Python
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服