对象模型
 2024-01-01
 10264字
 Python

Everything is object in Python.

概括地说,对象是对数据的抽象。这些数据可以是保存在内存中的,也可以是保存在磁盘等外部存储器中的。当然在讨论编程语言时,一般是指保存在内存中的数据,这个时候就可以说,一个具体的对象对应于一个内存实体。

内存布局

Python中的数据(对象)是靠内存块指针组织起来的。

内存块分为两种,一种是结构体变量,另一种是动态数组。这个动态是C语言中的概念,是指在程序运行过程中分配的内存,而不是在运行前就可以确定。这些内存块靠指针相连,形成一个有向图。

例如一个保存了1, 2, 3三个整数的列表对象,其内存布局如下图。

这个例子很好理解。结构体变量用于保存列表对象的基本信息,然后开辟一个动态数组,动态数组内的元素都是指针变量,用于指向其它的对象。这样,列表就可以“保存”任意多的任意对象了。

显然,列表对象就是对黄色框线内的两个内存块的抽象。对于动态数组的指针指向的内存块,逻辑上不应该属于列表对象。利用sys模块的getsizeof()函数查看对象占用的内存大小时,也只计量结构体变量和动态数组占用的空间。由于列表是变长的,为了避免每次长度改变都要重新分配内存,在为动态数组分配内存时通常会多分配一些余量,删除元素时也不是立马缩容,因此动态数组尾部通常都有一些空指针,它们对列表对象的行为没有影响,但是会被getsizeof()函数计量。

Python中对象可以分为内建对象自定义对象两类。

内建对象的类型信息已经事先定义好,解释器对这类对象是全知的,可以事先定义对应的结构体类型,以结构体变量的方式分配内存。自定义对象就是我们以class关键字定义的对象,解释器不可能对这些对象未卜先知,只能以动态数组的方式分配内存。详见自定义对象一节。

Python中的对象可以分为可变对象不可变对象,从内存的角度理解就是内存块(指用于存储数据的内存块)的内容可不可变的问题。元组的内存布局与列表基本一致,但是动态数组中的内容不可变,也就是那些指针不能指向别的对象,所以元组被称作不可变对象,列表被称作可变对象。但是元组只能保证指向的对象不变,对被指向对象没有任何约束,因此不能保证Python层面的数据不变,因为被指向对象本身的值是可以改变的。

对象使用

名字绑定

经过序章的铺垫我们知道,想要使用一个Python对象很简单,只要给出它的内存首地址和类型信息,我们就可以顺藤摸瓜找到它的各个字段,从而正确的解读和使用这个对象。

Python中使用指针给出对象地址的方式有两种。一种是直接使用指针,例如前面列表的例子,ls[0]的含义就是使用列表对象ls的动态数组中的第一个指针变量指向的对象。另外一种则是名字绑定,例如a = 1,其原理示意如下图。

真正绑定在一起的其实两个指针,这两个指针其中一个指向字符串对象'a'(名字都是字符串对象),另一个指向整数对象1,这样就把名字'a'和对象1绑定在了一起。

注意,不管是直接使用指针还是利用名字,解释器都会把它们解读为被指向或者被绑定的对象,而不是这个指针或者这个名字本身。

另外我们可能已经意识到,这种绑定在一起的指针对,可以用字典的键值对来实现。所以在大多数时候,通过名字来使用一个对象、或者进行名字绑定的时候,都会涉及一个或多个字典。

常用的名字绑定操作有以下几个。

# 赋值
a = 1

# 模块导入
import xxx
from xx import xxx

# as关键字
with xxx as xx
import xxx as xx
except xxx as xx

# 类定义和函数定义
class xx
def xx

# for循环、调用传参
for i in ls
func(x)  # 将被传入的实参和名字'x'绑定

这也解释了为什么Python已经在后台使用的模块,我们使用时还需要显式导入的原因。因为模块也是一种对象,我们并不知道它的地址是什么,只有使用import语句让解释器帮我们把名字和模块对象绑定在一起之后我们才能显式地使用它。

引用

Python这种使用对象的方式也被称为引用,名字绑定中的名字也会被称作变量。但是要注意,不管是名字还是对象中的指针,都可能绑定/指向到别的对象上去。我们对一个变量重新赋值的时候,其实是让这个名字绑定到了另一个对象上。这种特性让我们不能把Python中的变量简单的理解为可以盛放内容的盒子。

我们以一个例子说明这种特性潜在的问题。Python没有递加运算符(即x++),如果我们尝试实现它,初学者可能会编写以下代码:

def inc(x):
    x += 1

如果我们调用它会发现它和期望结果大相径庭:

a = 0
inc(a)
print(a)

# 运行结果:
# 0

这是因为整数对象是不可变对象,执行x += 1时,不会改变x绑定的对象的值,而是让x绑定到了一个新的整数对象上,这个新的整数对象的值是原来的整数对象的值加1。

本手册中的蓝色方框均侧重表示原理而非细节,所以省去了背后的指针对和用于标识字符串的''

C语言中的递增和递减运算符是时代的产物(方便编译器编译为INC和DEC指令),在Python中没有方便的实现方法,也没有必要实现。举这个例子只是为了说明名字绑定特性可能会带来的问题。下面这两个例子则是可能会遇到的。

  1. 某算法题要求使用深度优先搜索遍历二叉树,在到达叶子节点是判断是否符合某个条件,并记录符合条件的叶子节点的总数。如果使用以下代码(用随机数代替判断条件)则会发生和上面一样的情况:

    from random import random
    
    def dfs(cnt, deepth, limit):
        if deepth == limit:  # 模拟到达叶子结点
            if random() > 0.5:
                cnt += 1
        else:
            dfs(cnt, deepth + 1, limit)  # 模拟访问左子树
            dfs(cnt, deepth + 1, limit)  # 模拟访问右子树
    
    def solution(n):
        res = 0
        dfs(res, 0, n)
        return res
    
    print(solution(5))
    
    # 运行结果:
    # 0

    在C语言中常见的解决方式是使用指针传递,但是Python中使用指针比较麻烦,只能利用Python的特性曲线救国:

    from random import random
    
    def dfs(cnt, deepth, limit):
        if deepth == limit:  # 模拟到达叶子结点
            if random() > 0.5:
                cnt[0] += 1
        else:
            dfs(cnt, deepth + 1, limit)  # 模拟访问左子树
            dfs(cnt, deepth + 1, limit)  # 模拟访问右子树
    
    def solution(n):
        res = [0]
        dfs(res, 0, n)
        return res[0]
    
    print(solution(5))
    
    # 运行结果(可能会变化):
    # 15

    结合列表对象的内存布局可以轻松理解。

    solution()放到类中或者使用闭包变量也可以解决,这里为了使正确代码和谬误代码有一致的形式所以使用这种方法。

  2. 在Python中模拟矩阵。一个3 x 3的矩阵很可能会被这么初始化:

    matrix = [[0] ** 3] ** 3
    print(matrix)
    
    # 运行结果:
    # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

    初看没什么问题。但是如果修改某个元素,结果可能和想象地不太一致:

    matrix[0][1] = 1
    print(matrix)
    
    # 运行结果:
    # [[0, 1, 0], [0, 1, 0], [0, 1, 0]]

    如果画出matrix的内存布局(修改之前)则一目了然:

如果想正确的初始化一个m * n的矩阵(二维数组),可以使用下面的形式:

# 思考一下为什么内层可以用列表乘法
matrix = [[0] * n for _ in range(m)]

类型对象

下面这段代码想必都很熟悉,但是站在编译器的角度它会告诉虚拟机什么呢?

class Dog:
    def __init__(self, name):
        self.name = name

    def hello(self):
        print(f"Hello, I'm {self.name}")

里面的细节比较复杂,我们留在一章详细分析,这里只关注大概流程。这个流程大白话翻译一下就是:我要新定义一种数据类型,类型信息长这样,你先创建一个类型对象用来保存一下这些信息(也就是说类型对象保存了这种类型的对象的类型信息),然后把这个类型对象和名字Dog绑定在一起,方便我后面使用。注意,创建类型对象的过程会执行class Dog:里面的语句,只不过def语句的作用就是创建一个函数对象,不会给我们反馈。如果把代码修改成下面这样,则会执行print函数输出对应信息。

class Dog:
    print('building Dog type')
    def __init__(self, name):
        self.name = name

    def hello(self):
        print(f"Hello, I'm {self.name}")

我们可以使用id(Dog)输出这个对象的内存地址,证明Dog确实是个客观存在于内存当中的对象。

Python标准中规定,每一个Python对象都有一个标识符(identifier),用来唯一的标识一个对象。内置函数id()就是返回一个对象的唯一标识符。语言标准没有规定这个标识符到底是什么,但是在具体实现中,一般使用对象的内存地址作为标识符,因为这是天然唯一的。

Dog这种类型的对象的内存布局信息没有保存在Dog类型对象中,因为Python为所有的自定义类型规定了一个统一的内存布局,见自定义对象一节。其支持的操作自然就是类中定义的那些方法。注意,这些方法保存在类型对象中。

由于每个对象的属性可能都是不同的,所以属性保存在对象本身而不是其类型对象中,详见自定义对象一节。

在明白Dog其实是一个对象之后,我们对peter = Dog('peter')这样的操作可能有些疑惑:对象还能像函数一样调用的?是的。这种对象叫做可调用对象。这个概念可能比较新奇,但其实我们一直在使用——因为函数本身就是一种对象。

我们通常把peter = Dog('peter')这种从类创建对象的操作叫做实例化,与Dog这个类型对象相应的,我们称peter对象为实例对象。注意,类型对象中保存的方法是实例对象的方法,而不是类型对象本身的方法。类型对象的方法在后面讲解。

明白类型对象-实例对象的关系之后,定义方法时的self的作用也就呼之欲出。类型对象保存方法,实例对象来调用。但是有的方法依赖于实例对象本身的属性,例如输出的狗的名字,这个时候就要明确是哪个实例对象调用的这个方法。参数self就是用来传入发起调用的实例对象的。也就是说,在执行peter.hello()的时候,近似于在执行Dog.hello(peter)

泛型编程

这一节主要解释对象使用一节中的遗留问题:解释器如何获得对象的类型信息。

我们都知道,C语言的指针大部分都携带类型信息,例如int *pfloat *p等。但是类型信息必须由指针携带吗?好像也不一定。比如我们可以规定某个固定的字段作为指针指向其类型对象。

Python中也是这么做的,首先为所有的对象定义个公共头部(位于Include/object.h):

//源码中定义了大量的宏,为了使观感更清晰,列出时展开了部分宏。

typedef struct _object {
    Py_ssize_t ob_refcnt; //引用计数,用于垃圾回收。int64。
    struct _typeobject *ob_type; //指向对象的类型对象
} PyObject;

在64位架构下,结构体大小为16字节,包含两个8字节的字段。第一个字段用于引用计数,就是记录有多少个指针指向了它。第二个字段指向其类型对象。所以我们只需要以void *指针(不携带类型信息的指针)的形式给出对象的首地址,解释器就可以顺着ob_type指针找到它的类型对象,从而获得其类型信息。这种技巧就是C语言中的泛型编程。

为了增强可读性,Python开发者将void *又包装了一下叫做PyObject *,用于指向任何Python对象。

这也是Python运行速度慢的一个原因。C语言中的加法可能一条机器指令就能解决,但是对于Python,需要先找到它的类型对象,在从类型对象中查看它支持什么样的加法,背后可能需要十几条甚至几十条指令。但是没有办法,方便编写程序和执行速度快通常不能兼得。

哪有什么岁月静好,解释器替你负重前行罢了。

Python中有很多类型,对应的类型对象也有很多个,而只有struct _typeobject *ob_type这一种指针,所以这里也使用了泛型编程技巧。前面也说了,这是所有对象的公共头部,类型对象自然也包含了这个头部。那么类型对象的ob_type字段指向什么呢?或者说,Dog类型对象的类型对象是什么?这个疑问我们将在元类一节探明。

自定义对象

自定义对象以动态数组的方式分配内存,并且这个动态数组是固定的48字节大小,被分为6个8字节大小的字段。每个字段的含义如下:

  1. 引用计数。
  2. 类型对象指针。前两个字段和公共头部是相洽的。
  3. 属性字典指针。
  4. 弱引用链表头结点指针,弱引用机制参考弱引用一节。
  5. 我还没搞清楚。
  6. 哨兵,类似C字符串中的\0

属性字典

我们访问对象的属性的时候是使用名字访问的,背后的字典就是这个属性字典。对于自定义对象来说就是第3个字段指向的字典。用__dict__可以获取这个字典:

peter.__dict__

# 运行结果:
# {'name': 'peter'}

既然属性保存在这个字典中,字典又是可变对象,那是不是可以动态的添加对象的属性呢?是的,可以。

peter.test1 = 1
peter.__dict__['test2'] = 2

print(peter.test1, peter.test2)

# 运行结果:
# 1 2

但是在实际使用过程中,一般不要用这种方式添加属性(修改属性是可以的)。

这里有一个问题,__dict__没有在属性字典中,为什么peter还可以访问到呢?这就涉及到Python的属性访问(也就是.)机制,是一个比较复杂的过程,在本章继承一节和类机制>属性描述符中均有涉及。

特殊方法

我们知道,想要对对象进行某种操作,就要调用对应的方法,比如peter.hello()。而特殊方法就是当我们想进行某种操作时,解释器默默的在后台帮我们调用,不需要我们显式调用的方法。这些操作一般都以Python语法或内置函数的形式表露出来。举例如下表:

语法 解释器调用的方法
a + b a.__add__(b)
func() func.__call__()
x in s s.__contain__(x)
ls[i] ls.__getitem__(i)
abs(x) x.__abs__()

在形式上,特殊方法都以双下划线开始并以双下划线结尾。

特殊方法是Python层面比较底层的东西,因为它直接与Python的语法相关。从某种程度上说,深入理解各个特殊方法就等于深入理解Python。

常见的特殊方法可以分为构建、初始化与析构数值操作比较操作序列操作类型转换对象展示属性访问描述符可调用对象上下文管理等。

我们可以在类型对象中定义这些方法,以使我们的对象支持对应的操作。

常见的特殊方法列举如下。

序列操作

使用示例 特殊方法
len(s) __len__
s[i] __getitem__
s[i] = x __setitem__
del s[i] __delitem__
reversed(s) __reversed__
x in s __contains__

__iter__详见Features > 迭代器与可迭代对象

__missing__主要用于实现字典子类时处理key不存在的情况。

对象展示

__str____repr__两个方法。在使用print函数输出时,会优先调用__str__方法,如果__str__不存在,会调用__repr__方法;在交互模式下直接展示对象时,会直接调用__repr__方法。

>>> class A:
...     def __repr__(self):
...         return 'call __repr__'
...     def __str__(self):
...         return 'call __str__'
...
>>> a = A()
>>> print(a)
call __str__
>>> a
call __repr__

可调用对象

在类中定义__call__方法时,其实例对象就可以进行调用操作。切记,__call__方法的第一个参数也是发起调用的实例对象。

>>> class A:
...     def __call__(self, text):
...         return text * 2
...
>>> a = A()
>>> a('啊!')
'啊!啊!'

PyTypeObject

类型对象记录了对象的所有的操作,因此了解类型对象对了解对象至关重要。这一节主要讨论类型对象的内存布局。

在Python中,一个类型对象就是一个比较大的结构体,结构体的元素大多数是函数指针,指向操作对应的函数。并且这些结构体都是同一个类型——PyTypeObject。创建一个类型对象,在C语言层面就是生成一个新的PyTypeObject类型的结构体,并且填充其中的字段。

这些函数指针指向的函数对应的都是上一节提到的特殊方法。原因也很好理解,解释器不知道会有哪些自定义方法,自然也无法把自定义方法和结构体字段对应起来,但是对特殊方法是知道的。

自定义方法则保存在类型对象的属性字典中。这个字典同样可以用__dict__获取,在C语言层面,指向这个字典的则是结构体中的PyObject *tp_dict指针。

从中可以看到,在Python中,属性和方法并没有本质的区别,只不过属性保存在实例对象的属性字典中,方法保存在类型对象的属性字典。

下面我们关注一下PyTypeObject的具体结构。

开始之前要先看一下与PyObject相对的PyVarObject(同样位于Include/object.h):

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;  //在64位架构下,结构体大小为24B

看名字和结构就知道,PyVarObject是变长对象的头部。这两个头部经常出现,所以为了省事,开发者又定义了两个宏(也在Include/object.h中):

#define PyObject_HEAD          PyObject ob_base;
#define PyObject_VAR_HEAD      PyVarObject ob_base;

好了下面该找PyTypeObject了。同样在Include/object.h中有这样一条语句:

typedef struct _typeobject PyTypeObject;

而真正定义类型的struct _typeobjectInclude/cpython/object.h中(这就是顶级开发者组织代码的方式吗,爱了爱了)。因为要记录对象所有的操作,这个结构体的字段非常之多,但是根据名字和注释也能推断一二,我们会在后面慢慢学习大多数字段的作用。完整定义如下:

struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    // Strong reference on a heap type, borrowed reference on a static type
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;
};

继承

继承可以让一个类拥有另一个类的全部方法和属性。前者叫后者的子类,后者叫前者的父类。

继承关系是怎么实现的呢?需要从.的机制说起。我们已经知道,a.p背后是a.__getattr__(p)。这个函数的大概逻辑是按照实例对象->类型对象->父类的过程查找,找到就返回,找不到则继续查找,直到确定没有该属性,抛出异常。

因为有属性描述符的关系,“找到就返回”这句话是不对的,这里只是描述大概逻辑。具体原因见类机制>属性描述符一节。

从这个流程来看,子类的属性和方法会覆盖父类的。

继承关系通过类型对象的struct _typeobject *tp_base指针记录,在Python层面可以用__base__属性获取。

object

有许多特殊方法对Python程序的正常运行十分重要。例如,如果没有__init__方法,对象就无法初始化。所以Python设计者定义了一个object类,其中定义了许多常用或十分重要的特殊方法,如果定义类时没有指定继承关系,就让它隐式的继承object。这样,所有的类都会间接或直接的继承object

object作为一个类型对象,底层是一个PyTypeObject类型的结构体变量。并且作为内建对象,这个结构体变量在C语言层面是有名字的,叫做PyBaseObject,定义在Object/typeobject.c中。它的tp_base指针是空指针,不然上面访问属性的流程就会陷入死循环。

多继承

有时候一个类会继承多个类,我们把这种情形叫多继承。多继承关系通过类型对象的PyObject *tp_bases记录,这个指针指向一个元组,元组中的元素就是各个父类,在Python层面可以用__bases__属性获取。tp_base指针则指向元组的第一个元素。

Python的类之间靠继承关系形成一个巨大的有向无环图。

假如某个继承关系如上图所示,箭头上的数字表示书写顺序(即class A(B, C)),这时候属性访问的查找顺序是什么样的呢?

这个问题就是基础数据结构中的拓扑排序,Python采用C3算法确定拓扑排序序列。对于上图而言,查找顺序是A -> B -> D -> C -> E -> object。对于不太复杂的继承关系,该方法近似于深度优先搜索,并且把公共继承的父类放在后面(DE公共继承object)。具体细节可搜索C3算法。用a.__mro__或者a.mro()可以获取依据该算法得到的拓扑序列。

元类

上一节我们讨论了Python中的继承关系,并且知道继承关系的顶层就是object类。这一节讨论Python中的实例化关系或者说类型关系。

Python中有一个特殊的对象叫type,它是一个可调用对象,以type(a)形式调用,会沿着对象aob_type指针找到其类型对象并返回。

延续之前的例子,执行print(type(peter)),得到的结果是<class '__main__.Dog'>;或者执行print(type(peter) is Dog),结果是True。这里的'__main__.Dog'就是Dog类型对象的tp_name指针指向的内容。

const char *tp_name; /* For printing, in format "<module>.<name>" */

我们可以继续使用type寻找Dog的类型:print(type(Dog))。结果是<class 'type'>。根据tp_name的作用,这个字符串'type'是**Dog的类型对象**的tp_name指针指向的字符串,但是这个类型对象是不是我们一开始说的这个type并不确定,因为任何对象的tp_nmae指向的字符串都可以'type'。我们可以这么验证一下:print(type(Dog) is type)。结果是True。也就是说Dog的类型对象确实就是type。那type的类型对象呢?

print(type(type))
print(type(type) is type)

# 运行结果:
# <class 'type'>
# True

is严格比较两个变量是否引用到了同一个对象,逻辑上等价于id(a) == id(b)

除了type(a)之外,a.__class__也可以查看对象a的类型,两者效果完全相同。

看来type就是实例化关系的顶层。

事实上type是一个特殊的类型对象,绝大多数的类型对象的类型对象都是type,包括type自己。实际运行过程中,其它的类型对象的完整创建过程确实都有type参与,说type是其它所有类型对象的类型可以接受。但是它怎么实例化自己呢?答案是它不需要真的实例化自己。type是内建对象,只需要把它的ob_type指针指向自己,那么在逻辑上,它的类型就是它自己。

在逻辑上,所有的对象都是由其类型对象实例化而来的,都应该叫做实例对象,但是为了避免混乱,我们一般在称呼那些“普通”对象、或者刻意强调实例化关系的时候使用实例对象这个词。

我们把type这种可以实例化出其它类型对象的类型对象,叫做元类(meta class)。元类在日常使用中并不多见,但理解这个概念可以帮助我们理清Python的类型系统。

object一样,type对应的结构体变量在C语言层面也是有名字的,叫作PyTye_Type,同样定义在Object/typeobject.c中。

在上一节我们说object是所有类的父类,这一节又说type是所有类型对象的类型。那么typeobject会是对方的特例吗?

print(type(object) is type)
print(type.__base__ is object)

# 运行结果:
# True
# True

看来并不是。解决这种鸡生蛋悖论的方法也很简单——底层指针赋个值罢了。

用一张图总结一下:

除了type之外,Python也允许创建自定义元类,详见 > 自定义元类

生命周期

对象从创建(实例化)到销毁所经历的过程称作对象的生命周期。

我们先罗列一下马上要接触到的PyTypeObject中的字段,然后以peter = Dog('peter')这条语句为例介绍对象的实例化过程。

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
ternaryfunc tp_call; // 对应__call__
initproc tp_init; // 对应__init__
allocfunc tp_alloc;
newfunc tp_new; // 对应__new__

执行这条语句的时候,会先调用Dog这个类型对象,并传入参数'peter',调用的结果是返回一个Dog类型的实例对象;然后将名字peter绑定到这个实例对象上。

调用某个对象背后执行的是其__call__方法,也就是Dog('peter')等价于Dog.__call__('peter')。由于Dogtype的实例对象,所以Dog.__call__('peter')等价于type.__call__(Dog, 'peter'),也就是会执行typetp_call指针指向的函数,并把Dog作为第一个参数传入,该函数关键代码如下(注意代码中的type是C函数形参,不是Python中的type对象):

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *obj;

    /* Special case: type(x) should return Py_TYPE(x) */

    obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(_PyErr_Occurred(tstate));
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!_PyErr_Occurred(tstate));
        }
    }
    return obj;
}
  1. 调用该函数时,会自动将Dog传入函数的第一个参数,相当于在Python层面定义方法时的self

  2. 注释部分的大概意思是,如果调用的对象是type本身(因为type的类型也是type,所以type的操作也记录在type中,这很自洽),那么就返回相应的类型对象(具体代码没有贴出来);如果不是,就执行正常的实例化过程(因为只有类型对象的类型是type,调用这些类型对象的目的只能是实例化一个实例对象)。

  3. 第8行执行Dogtp_new指针指向的函数,也就是__new__方法,由于这里没有定义,所以会继承object__new__方法,也就是Dogtp_newobjecttp_new指向同一个函数。

    这个函数的第一个参数也是调用者,这里例子中就是Dog。函数的主要作用是根据Dogtp_basicsizetp_itemsize确定需要分配的内存大小,然后调用Dogtp_alloc指针指向的函数分配内存,并把内存块第一个8字节的元素(引用计数)置为1,第二个8字节元素(对应ob_type指针)指向Dog

  4. 内存分配好之后,这个实例对象就算创建出来了,但还不是完成品,需要再调用Dogtp_init指针指向的函数,往属性字典填充内容。如果没有定义__init__方法,这个指针就会指向objecttp_init指针指向的函数,也就是继承object__init__方法。object__init__方法约等于啥也不干,也确实不需要干什么。

以上就是对象实例化的过程,执行完peter = Dog('peter')之后内存中我们关心的部分如下图:

除了内建对象之外,在Python中创建的对象在C语言层面是没有名字的,解释器只需要知道动态数组地址就可以了。自定义的实例对象的每个字段也没有显式的名字,但是前两个字段的实际意义和PyObject结构体一致:第一个记录引用个数,第二个指向类型对象。示意图里面的ob_type是根据实际意义标注的。

引用计数会随着指向到对象的指针的数量而变化,当引用计数减至0时,该对象就隐藏在茫茫内存中不可能再显式找到,之后这个对象就会被垃圾回收进程清除以释放占用的内存,从而结束它的整个生命周期,消逝在代码长河中。