在Python编程中,列表(List)因其广泛的用途而广受欢迎。然而,在某些情况下,可能需要考虑避免使用列表。本文将探讨Python列表的内部机制,并提供一些替代方案,以帮助数据科学家和程序员在编写更高效的代码时做出更好的决策。
Python列表可以包含不同类型的元素,这使得它们非常灵活。例如,可以创建包含整数的列表:
L = [1, 2, 3, 4, 5]
同样,也可以创建包含字符串的列表:
L2 = [str(c) for c in L]
这展示了Python的动态类型特性,可以创建包含不同类型元素的混合列表:
L3 = [True, 3, "6", 9.0]
这种灵活性并非没有代价。为了理解为什么应该避免使用列表,需要深入了解Python的内部工作原理。
为了在数据驱动的编程和计算中变得高效,需要深入了解数据是如何存储和操作的。对于数据科学家来说,这一点尤其重要。Python因其易用性而受到越来越多程序员的青睐,其中之一就是动态类型。与C++或Java等静态类型语言不同,Python不需要显式声明所有变量的类型。
例如,在C++中,可能会这样写代码:
int sum = 0;
for (int i = 0; i <= 100; i++)
sum += i;
cout << sum;
而在Python中,同样的程序可以写成:
sum = 0
for i in range(101):
sum += i
print(sum)
这里可以看到的主要区别是,在C++中,所有变量类型都需要显式声明,而在Python中,类型是动态推断的。这意味着可以将任何类型的数据分配给任何变量。这种灵活性意味着Python变量不仅仅是一个值,它还包含了关于其类型的额外信息。
Python解释器是用C语言编写的,因此所有的Python对象都是C结构的伪装版本,它们不仅包含其值,还包含其他信息。例如,如果在Python中声明一个变量:
x = 10
x不仅仅是一个原始整数,而是一个指向包含多个值的复合C结构的指针。如果深入挖掘,可以发现这个C结构的样子。
struct _longobject {
long ob_refcnt;
PyTypeObject *ob_type;
size_t ob_size;
long ob_digit[1];
};
因此,它包含四部分:
所有这些额外信息意味着在内存和计算能力方面有更多的开销。因此,Python int对象本质上是一个指向包含有关该变量所有信息的内存位置的指针,包括包含实际整数值的内存字节。所有这些额外信息是让能够在Python中如此自由编码的原因。不仅仅是整数,Python中的所有数据类型都带有这种开销成本,然而,这种成本在结合了许多这些对象的结构中变得显著,即列表!
现在来考虑一下当使用一个由多个元素组成的标准Python容器时会发生什么。标准Python容器是列表,类似于C中的数组,两者都是可变的,但正如之前讨论的,列表可以是异构的。
但这种灵活性是非常昂贵的。为了成为异构的,列表中的每个元素都必须包含自己的类型信息、引用计数和所有其他信息。换句话说,每个项目都是一个完整的Python对象。
因此,如果进一步分解,Python列表包含一个指针,该指针指向另一个指针块,而在该块中,所有这些指针反过来指向一个单独的完整的Python对象,就像之前看到的那样。这就像一个大型的俄罗斯套娃!
当所有变量都是同一类型时,这些信息中的大部分变得多余。难道没有更有效的容器来存储这样的数据吗?这样的容器没有这样的冗余或不太有用的信息,但仍然保留了列表的有用功能?好吧,在下一节为准备了一些替代方案。
Python有一个内置模块名为‘array’,它类似于C或C++中的数组。在这个容器中,数据存储在一块连续的内存中。就像C或C++中的数组一样,这些数组一次只支持一种数据类型,因此它不像Python列表那样异构。索引与列表相似。数组的类型必须使用官方文档中提供的typecode指定。
from array import array
a = array("l", range(10))
print(a)
使用的“l”是指定想要的数组的typecode,即signed long。一些有用的方法可以与数组一起使用: