楔子
我们都知道实例在调用方法时,会自动将自身传给 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, NULL, 0); } 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 + 1, NULL); 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。
所以为什么实例调用方法的时候会自动传递第一个参数,此刻算是真相大白了。当然啦,以上只能说从概念上理解了,但是源码还没有看,下面就来看看具体的实现细节。
接下篇深度解密为什么实例在调用方法时会将自身传给 self 参数(二):https://developer.aliyun.com/article/new?spm=a2c6h.13046898.J_eBhO-wcawiLJRkGqHmozR.90.110f6ffaddNUt4#/