看起来很简单直观,对吧?所有学过Python的人都在某个时候使用过列表。几乎所有的教程、视频、训练营和博客都会讲解如何使用列表以及它们提供的各种方法。但你是否曾经想过列表究竟是如何工作的?它们是如何自动调整大小的,这与C语言中的静态数组有什么不同?Python到底隐藏了什么?它实际上是如何添加或删除元素的?
这就是本文要讨论的内容。我们将通过查看唯一能够揭示真相的地方——源代码,来回答所有这些问题。我们的任务是揭开帷幕,窥探支撑Python的C代码,理解列表的真实面貌。这里没有魔法,只有纯粹的逻辑。
那么,为什么我们突然谈论起C语言?当你在终端中输入python3时,你实际上是在运行一个程序。这个程序,也就是Python语言最流行的实现,被称为CPython,它几乎完全是用C语言编写的。
可以把它想象成实际运行你的Python脚本的软件。每当你创建一个列表或调用.append()方法时,你实际上是在告诉CPython运行一个特定的C函数。要真正理解列表是如何工作的,我们必须查看定义列表的C代码。
第一个令人惊讶的事实是,Python这个著名的面向对象语言是建立在C语言之上的,而C语言并不具备我们通常理解的类和对象。这怎么可能呢?
面向对象编程的基本构建块实际上只是结构体(用于组织数据)、指针(用于创建引用)、函数指针(用于表示方法)和间接引用(一种在运行时决定执行哪个函数的方式)。
这四个构建块让CPython能够支持面向对象编程的关键特性:将数据和方法组合在一起(封装)、让一个类继承另一个类(继承)以及让对象根据其类型表现出不同的行为(多态)。
这是所有Python对象的根。每个其他对象结构体都包含一个PyObject,使其成为一切的绝对基础。下面是使用结构体和指针定义的PyObject:
某些对象,比如数字7,具有固定的大小。而其他对象,如列表和字符串,是可以容纳可变数量事物的容器。PyVarObject是所有这些可变大小容器的共同基础。
这里的PyObject_HEAD只是一个宏,这是C语言中一种巧妙的方式,用于将PyObject中的两个字段直接复制到这个结构体的顶部。这基本上就是继承!PyVarObject"继承"了ob_refcnt和ob_type,然后添加了一个新字段ob_size。
好了,准备好接受这个重大秘密吧,这个让我陷入这一切的秘密。在内部,一个动态的、灵活的Python列表基本上就是一个静态的C数组😱。是的,我当时也是一脸震惊。
当你写my_list = []时,Python会:
这是Python列表最有趣的部分。它不会每次都只增加一个槽位,而是使用一个聪明的过度分配策略:
当你调用my_list.append(x)时,Python会:
这就是为什么append通常是O(1)操作,但偶尔会需要O(n)时间来增长数组。
这就是为什么insert是O(n)操作 - 它可能需要移动很多元素。
这也是一个O(n)操作,因为它需要移动元素来填补空缺。
那么,Python到底隐藏了什么?在剥开层层包装,深入研究C源代码之后,我们找到了答案,这并不是魔法。我们每天使用的动态的、不断增长的列表是建立在一个简单的静态C数组之上的。这就是秘密。
了解这一点会改变你看待自己代码的方式。使用my_list[99999]进行快速访问?这是C数组的原始能力,通过简单的数学计算找到内存地址。但这种能力是有代价的。my_list.insert(0, 'x')感觉很慢的原因是,你正在见证同一个C数组的蛮力现实,它必须一个接一个地物理移动每个元素,只是为了在前面腾出空间。
最终,列表只是一个建立在巧妙权衡之上的优秀工程作品。它打赌你会比在开头插入更频繁地使用append,所以它通过过度分配策略为这种情况进行了优化。现在,你也知道了这个秘密。