全文共5388字,预计学习时长14分钟
图源:unsplash
Python最流行的容器数据类型非列表(list)莫属。它非常灵活,可以在项目中的任何地方使用,还可保存各种数据:整数、字符串和自定义类实例。此外,它是可变的,允许根据需要添加或删除项。因此,一些程序员也开始变得过度依赖使用列表而不考虑替代方案。
其实,还有比列表更好的选择,不应该被忽略掉。
1.数据不可变性与散列——元组
数据容器不需要一直改变。有时候,我们实际上根本不想改变数据容器。本例需要一个不可变的数据容器来保存数据。
假设用户会在构建的应用程序里选四位数字作为其安全码。理论上,可以使用列表对象来存储这些数字。但可能会发生一些意外的事情—代码可能会被不小心更改。
参考下列代码:
>>> code_list_saved = [5, 9, 0, 3]
>>>code_list_entered = [5, 9, 0, 3]
>>> code_list_saved==code_list_entered
True
>>>code_list_saved[2] =7
>>> code_list_saved==code_list_entered
False
最初,这两个列表被评估为相等,以便用户可以解锁受保护的信息。但是,如果只更改已保存代码中的一个数字,两个列表就会变得不相等,用户将无法访问安全信息。所以,我们希望保存的代码具有不可变性。
在这种情况下应该考虑使用元组。众所周知,元组在Python中是不可变的对象,这意味着它们的值在创建之后不能被更改。下面显示了使用元组替换实现的过程:
code_tuple_saved = (5, 9, 0, 3)
>>>code_tuple_entered = (5, 9, 0, 3)
>>>code_tuple_saved ==code_tuple_entered
True
>>>code_tuple_saved[2] =7
Traceback (most recentcall last):
File"<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support itemassignment
元祖的不变性
如上所示,已被保存的代码现在储存在元组当中。如果试图更改其中一个数字会出现TypeError(输入错误),阻止了代码的任何意外更改。此外,作为数据安全性的一个直接实现,元组对象可以在Python中被直接散列。
通过将代码存储在元组对象中,可以使用一个散列值来表示代码,这会使黑客更难破解应用程序。请看一下简化版的执行代码:
>>> code_picked = (5, 9, 0, 3)
>>> stored_code =hash(code_picked)
>>>-8016007988208603090
>>>
>>> code_attempt1 = (9, 5, 0, 3)
>>> hashed_attempt1=hash(code_attempted)
>>> code_attempt2 = (5, 9, 0, 3)
>>> hashed_attempt2=hash(code_attempt2)
>>> stored_code == hashed_attempt1
False
>>> stored_code == hashed_attempt2
True
>>>code_picked_list = [5, 9, 0, 3]
>>>hash(code_picked_list)
Traceback (most recentcall last):
File"<stdin>", line 1, in <module>
TypeError: unhashabletype: 'list'
2.先进先出——双端队列
有时,我们需要频繁地从序列的末尾添加或删除项。它要求项目按照先进先出顺序(先进先出)进行操作,即首先处理添加的第一项。使用列表,我们可以通过pop(0)函数来实现这一操作。但是,执行此操作的时间会很长,列表中的项必须进行移位,这是一个O(n)操作。
图源:unsplash
相比之下,“deque”数据类型是一种双端队列的形式,是为了能从两端快速插入和删除数据而设计的。为了执行先进先出(FIFO)操作,Python将能够直接删除队列开头的项,而不需要移动其他的所有项。这意味着这种先进先出操作速度很快。让我们看看下面的对比:
>>># Import needed modules
>>>from collections import deque
>>>from timeit import timeit
>>>
>>># Declare afunction to measure the time for FIFO
>>>deftime_FIFO_testing(n):
... integer_list =list(range(n))
... integer_deque =deque(range(n))
... t_list =timeit(lambda : integer_list.pop(0), number=n)
... t_deque =timeit(lambda : integer_deque.popleft(), number=n)
... returnf"{n: <9} list: {t_list:.4} | deque: {t_deque:.4}"
...
>>> numbers = (100, 1000, 10000, 100000)
>>>for number in numbers:
... print(time_FIFO_testing(number))
...
100 list: 3.41e-05| deque: 1.645e-05
1000 list: 0.0004852| deque: 0.0003466
10000 list: 0.01762| deque: 0.002618
100000 list: 2.059| deque: 0.02067
3.成员检查——集合
根据特定应用程序中的业务需求,你通常需要检查数据容器是否包含所要查找的特定项。当然,列表具有所需的内置方法,可以进行评估。如下所示,我们只需使用in关键字进行检查,就会返回一个布尔值:
>>> integers = [1, 2, 3, 4, 5]
>>>5in integers
True
>>>9in integers
False
但是,如果应用程序需要频繁检查成员,就应考虑使用集合而不是列表。集合是Python中另一个重要的内置容器数据类型。集合的独特之处在于集合中的所有元素都必须是唯一的且可散列(哈希)的。
必须具备哈希性,因为Python在内部将集合视为哈希列表执行操作。使用哈希表作为实现机制的一个最主要的好处是特定项的查找时间是按期或持续的。来个简单的比较看看容器有数百万条记录时,集合在哪里胜过列表:
>>># Import needed modules
>>>from random import randint
>>>from timeit import timeit
>>>
>>># Declare afunction to measure the time for membership testing
>>>deftime_membership_testing(n):
... integers_list =list(range(n))
... integers_set =set(range(n))
... t_list =timeit(lambda : randint(0, 2*n) in integers_list, number=10000)
... t_set =timeit(lambda : randint(0, 2*n) in integers_set, number=10000)
... returnf"{n: <9} list: {t_list:.4} | set: {t_set:.4}"
...
>>> numbers = (100, 1000, 10000, 100000)
>>>for number in numbers:
... print(time_membership_testing(number))
...
100 list: 0.02304| set: 0.01333
1000 list: 0.1042| set: 0.01309
10000 list: 0.9028| set: 0.01713
100000 list: 8.867| set: 0.01932
成员资格检查:列表与集合
如你所见,列表中元素增加,成员检查的时间会呈线性增加。相比之下,使用一个集合进行成员检查所花时间几乎保持不变,更重要的是,这个时间比使用一个列表所需的时间短得多。
4.值检索——词典
图源:unsplash
与集合类似,另一个导入内置的数据类型“dict”,要求其键是散列的。
键的散列性意味着dict中存储的数据会参与散列表。与其他主流语言(如Java、Swift、Kotlin)一样,Python在底层使用散列表来运行字典。但是,与集合不同,字典存储键值对,哈希密钥的需求是构建哈希表的基础。
根据哈希机制,检索特定键值对所需的时间是恒定的,时间复杂度为O(1)——使用Big-O表示法。这种O(1)时间复杂性意味着无论dict有多少个元素,检索特定项的时间总是保持在相同的量级上。我们可以看到下面一个大致的对比:
>>># Import needed modules
>>>from random import randint
>>>from timeit import timeit
>>>
>>># Declare afunction to measure the time for value retrieval
>>>deftime_value_retrieval_testing(n):
... id_list =list(range(n))
... score_list =list(range(n))
... scores_dict = {x: x for x inrange(n)}
... t_list =timeit(lambda : score_list[id_list.index(randint(0, n-1))], number=10000)
... t_dict =timeit(lambda : scores_dict[randint(0, n-1)], number=10000)
... returnf"{n: <9} list: {t_list:.4} | dict: {t_dict:.4}"
...
>>> numbers = (100, 1000, 10000, 100000)
>>>for number in numbers:
... print(time_value_retrieval_testing(number))
...
100 list: 0.02423| dict: 0.01309
1000 list: 0.07968| dict: 0.01322
10000 list: 0.625| dict: 0.01565
100000 list: 6.223| dict: 0.01583
假设我们需要存储一组学生的分数。使用列表,我们将有一个列表存储学生ID号,另一个列表用于储存相关分数。要想知道某个学生的分数,我首先要找出这个学生的指数,然后用这个指数进一步得到分数。
相比之下,使用字典,我们将只存储所有这些学生id-score键值对,并将学生id号作为键。在这两种方法中,当记录数较多时,字典的性能要优于列表。
但是,有一点需要注意,因为字典存储键值对,所以数据模型中应该要有有意义的信息来标识这些值。此外,字典中的键必须是唯一的,不过值可以相同。
5.大量表格型数据——数组
Python已经被逐渐广泛地应用于数据科学领域中的数据处理、分析和建模。其日益流行的一个主要原因是各种开源软件包的发展,开发了适用于大规模数据集的自定义类。因此,我们不应该使用列表,而应该考虑专门为这些与计算相关的工作设计的替代方案。
例如,如果需要处理大量的数字型数据,应该考虑使用NumPy数组,这是NumPy包中实现的核心数据类型;如果需要处理具有混合数据类型(例如字符串、日期、数字)的结构化数据,则应该考虑使用Pandas DataFrame,这是Pandas包中实现的核心数据类型之一;如果进行机器学习,肯定需要研究张量,这是主要机器学习框架(如TensorFlow和PyTorch)中最重要的数据类型。
图源:unsplash
网络上有许多教程,这些教程有很多针对数据结构的详细讨论。笔者的观点是,如果要处理大量的数据记录,一定要研究这些替代方案——它们在较低级别有特殊的实现方式,可用于优化大量的操作。
我们不应该局限于使用列表功能,因为Python及其相关包提供了其他的数据结构,且这些数据结构可为特定业务需求提供最佳解决方案。保持开放的思想,完全理解自己的选择更重要。
一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”
(添加小编微信:dxsxbb,加入读者圈,一起讨论最新鲜的人工智能科技哦~)