当array.array比lists更高效?

6
我正在阅读'流畅的Python'这本书时,遇到了一句话,作者在其中陈述道:
如果你需要存储1000万个浮点数,那么数组更加高效,因为数组实际上不会保存完整的对象,而只是保存代表它们机器值的打包字节 - 就像C语言中的数组一样。
我无法理解作者想要传达的意思。他关于“打包字节”的说法是什么意思?“打包字节存储”是什么意思?Python列表如何存储它?如果这使它更有效,为什么它不以这种方式存储?

1
可能是为什么numpy数组如此快速的重复问题。 - Lucas
一个数组(通常)是内存中包含同种类型的连续部分。这使得与链表(Python 的list)相比,可以进行多个优化。 - Reut Sharabani
Python的列表不保证是连续的。而从中构建Array.array的C数组在内存中是连续的。它们被存储为标量(通常在堆栈上),而对象则使用在堆上分配的内存。 - Charles D Pantoga
2
@ReutSharabani。Python的list根本不是链表,它是一块连续的内存,保存着对象本身的指针。额外的间接层就是使其效率低下的原因所在。 - Mad Physicist
@CharlesAddis同意。我只是想说,我相当确定Python数组对象的数据缓冲区通常是malloced的。我会查一下以确保,但我无法想象它以其他方式工作,因为堆栈缓冲区只能存在于构造函数或初始化器的范围内。 - Mad Physicist
显示剩余4条评论
2个回答

9
假设您正在处理8字节浮点数。在这种情况下,“打包字节”意味着有一个专用的分配内存块,其中第一个8字节表示第一个浮点数,紧接着下一个8字节表示下一个浮点数,以此类推,没有浪费。这是存储数据最节省空间的方式(至少没有压缩),对于某些操作来说也可能是最时间有效的(例如数组算术运算)。
但Python的列表并不以这种方式存储东西。一方面,列表元素可以是浮点数,但“下一个”可能是其他类型的对象。另一方面,您可以删除、插入或替换列表中的项目。其中一些操作涉及动态地增加或减少列表长度,如果按打包字节存储项,则所有这些操作都非常耗时和占用内存。Python的列表类旨在尽可能地通用,权衡各种类型操作的效率。
最重要的区别可能是Python列表,在其基础实现中,是一个包含指向对象的指针的容器,而不是包含原始对象内容的容器。这意味着同一Python对象的多个引用可能出现在列表中。另一个影响是可以非常高效地更改特定项。例如,假设列表中的第一项a[0]是整数,但您想将其替换为占用更多内存的字符串,例如a[0] =“货架五通道有一匹马。”。打包数组需要(a)创建额外空间,移动内存中其余数组内容并(b)分别更新某种索引以记录项的大小和类型。与大多数语言一样,Python的打包数组实现(array.array)甚至不允许这样做:相反,对于数组来说保证和强制执行统一的元素大小和类型更加合理。相比之下,在此情况下Python列表只需用另一个指针值覆盖一个指针值,并且没有这种限制。
事实上,现在应该可以清楚地看出这些指针本身甚至不直接指向对象内容。例如,它们不会直接指向包含浮点值的8字节,而是指向PyObj数据结构,其中包含所有必要的元信息(例如声明“我的内容必须解释为浮点数”)以及内容本身。
在CPython实现中,指针本身仍然可以(或多或少)紧凑地存储在内存中。这意味着将新项目插入到列表中通常仍然是低效的(相对于如果Python列表实现使用链接列表结构作为基础的方式)。

通常情况下,不存在绝对的“高效”或“低效”——这完全取决于您要提高哪个资源的效率、容器中有哪些内容类型(以及对内容类型的限制)以及您打算如何转换容器或其内容。


3
列表与数组的存储方式相同,主要区别在于列表保存指针。重新调整列表大小几乎就像调整数组一样低效,并且往往涉及重新分配整个列表(例如使用append函数时)。 - Mad Physicist
@MadPhysicist 那是基本原则,虽然我认为列表并不总是保证在内存中是连续的。但我明白你的观点,也许我不应该给人们留下 list 是为了特定的操作而优化设计的印象。 - jez
@jez 我想强调的是数组通常具有两个保证 - 类型和大小。这些假设使它们更加优化,据我所知。 - Reut Sharabani

4
在Python底层,大部分情况下使用指针来操作。Python列表实际上是指针的数组(至少在参考实现中是这样)。这就是效率低下的一部分原因所在。另外一部分原因是Python通常使用鸭子类型,因此在尝试操作一个列表元素之前,你无法确定它是否可行。
当你访问一个列表元素并对其进行操作时,比如调用`__add__`方法,你需要A)解标记指向实际对象,B)查找它是否有`__add__`属性,检查是否可调用,然后实际调用。
在数组中,你知道要存储的数据类型,因此你知道所有的属性和操作。此外,所有数字都打包到一个单独的内存块中。但这在列表中并不适用。由于在Python中,数字只是对象的一种类型,因此数字列表实际上是一堆通用对象指针的列表。
总之,数组存储裸数字而不是指向包装数字的大型对象的指针,使得它们更小。数组在一开始就知道其内容的类型,因此将一个操作应用于整个数组仅需要进行一次检查,而不像列表那样需要对每个元素进行检查。

@ReutSharabani。知道大小是无关紧要的。你也知道列表的大小。 - Mad Physicist
不是这样的。列表可以在不重新分配内存的情况下改变大小,而数组通常没有任何修改大小的操作。它们的长度通常是一个属性而不是函数。一般来说。 - Reut Sharabani
@ReutSharabani 这个问题特别涉及到由内置模块array表示的数组。 - pvg
不过,当列表被重新分配时,您不会对其应用操作,因此,为了实际使用列表,它的大小是固定的。此外,在底层,列表大小只是一个属性。 - Mad Physicist
@MadPhysicist,“append”会重新调整列表的大小(有时需要重新分配内存),我不确定您的意思。数组通常没有这个选项。如果引用说“如果你想要10-20个浮点数”,那么选择数组就不太合适。如果引用说“如果你想要浮点数和字符串”,也是如此。 - Reut Sharabani
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接