为什么Python将元组、列表、集合和字典视为根本不同的东西?
我喜欢Python的一个原因就是它的表达能力强,编程的工作量少,这得益于元组、列表、集合和字典这些数据结构。一旦你理解了列表推导式和一些基本的使用in
和for
的模式,生活就会变得轻松多了!Python真棒。
不过我确实在想,为什么这些数据结构的处理方式差别这么大,而且这种差别随着时间的推移还在变得更加奇怪。在Python 2.x的时候,我可以说它们都是基本集合类型的变种,有些不太复杂的用例却需要你把字典转换成列表再转换回来,这让我有点烦恼。(字典不就是一组有特定唯一性约束的元组吗?而列表不就是一种有不同唯一性约束的集合吗?)
现在在3.x的世界里,事情变得更加复杂了。现在有了命名元组——这开始感觉更像是一个特殊的字典。还有有序字典——这开始感觉更像是一个列表。最近我还看到了一种有序集合的用法。我能想象这种情况会不断发展下去……那独特的列表呢,等等。
Python的哲学中说“应该有一种——最好只有一种——明显的方式来做这件事”。在我看来,这种各种各样的专用集合类型和这个Python的原则是有冲突的。
那么,资深的Python爱好者们怎么看呢?
8 个回答
首先,Ordered Dictionaries(有序字典)和 Named Tuples(命名元组)是在 Python 2 中引入的,但这不是重点。
我就不指向文档了,因为如果你真的感兴趣的话,早就该看过了。
集合类型之间的第一个区别是可变性。tuple
(元组)和 frozenset
(冻结集合)是不可变类型。这意味着它们在某些情况下比 list
(列表)或 set
(集合)更高效。
如果你想要一个可以随机访问或按顺序访问的东西,但主要是在最后进行更改,你可以选择 list
。如果你想要一个可以在开头也能更改的东西,你可以选择 deque
(双端队列)。
你不能既想要蛋糕又想吃掉它——每增加一个功能,都会让你失去一些速度。
dict
(字典)和 set
(集合)与 list
(列表)和 tuple
(元组)是根本不同的。它们存储的是键的哈希值,这样你可以非常快速地检查某个项目是否在里面,但这要求键必须是可哈希的。用链表或数组进行成员测试时,速度就没有这么快了。
当你提到 OrderedDict
(有序字典)和 NamedTuple
(命名元组)时,你是在说 Python 内置类型的子类,而不是用 C 实现的。它们是为特殊情况准备的,就像你需要导入的标准库中的其他代码一样。它们不会让命名空间变得杂乱,但在需要的时候非常好用。
总有一天,你在编码时会说:“哇,我现在明白了‘应该有一种——最好只有一种——明显的方法来做这件事’的意思,set
(集合)正是我需要的,我真高兴它是 Python 语言的一部分!如果我必须用列表,那可真是要花费永远。”那时你就会明白为什么会有这些不同的类型。
简而言之(鸭子类型)
你说得对,这些数据结构之间确实有一些相似之处。记住,Python使用的是鸭子类型(如果它看起来像鸭子,叫声也像鸭子,那它就是鸭子)。如果你能在同样的情况下使用两个对象,那么在你当前的需求下,它们可以视为相同的数据类型。但你要始终记住,如果你在其他情况下使用它们,它们可能就不会表现得一样了。
考虑到这一点,我们应该看看你提到的四种数据类型之间到底有什么不同和相同之处,以便大致了解它们可以互换使用的情况。
可变性(能否改变它?)
你可以对字典、列表和集合进行修改。元组则不能被“改变”,除非你复制一份。
可变:
dict
、list
、set
不可变:
tuple
Python中的string
也是一种不可变类型。为什么我们需要一些不可变的对象呢?我可以引用这个回答:
不可变对象可以进行很多优化
在Python中,只有不可变对象是可哈希的(只有可哈希的对象才能成为集合的成员或字典的键)。
从这个特性来看,列表和元组似乎是“最接近”的两种数据类型。从高层次来看,元组是列表的不可变“快照”。这使得列表适合用于那些会随着时间变化的数据集(因为你不需要复制列表就可以修改它),而元组则适合用作字典的键(因为字典的键必须是不可变类型)。
顺序(以及关于抽象数据类型的说明)
字典和集合没有固有的顺序,这与列表和元组是有顺序的不同。字典或集合中的元素顺序是抽象化的,这意味着如果在for k in mydata
循环中,元素A在B之前,你不应该(而且通常也不能)依赖于在你开始修改mydata
后,A仍然在B之前。
保持顺序:
list
、tuple
不保持顺序:
dict
、set
从技术上讲,如果你连续两次遍历mydata
,它会保持相同的顺序,但这更多是Python机制的一个便利特性,而不是真正属于set
的抽象数据类型(数据类型的数学定义)。不过,列表和元组确实保证顺序,尤其是元组是不可变的。
迭代时你看到的内容(如果它走起来像鸭子……)
每个“元素”对应一个“项”:
set
、list
、tuple
每个“元素”对应两个“项”:
dict
在这里,你可以把命名元组看作是字典的不可变类比,因为它每个元素都有一个名称和一个值。但这只是一个微弱的比较——请记住,如果你试图在命名元组上使用仅限字典的方法,或者反之,则会出现问题。
对你问题的直接回应
字典不就是一组具有特定唯一性约束的元组列表吗?
不是的,它们有几个不同之处。字典没有固有的顺序,而列表是有顺序的。
此外,字典的每个“元素”都有一个键和一个值。而元组则可以有任意数量的元素,但每个元素只有一个值。
由于字典的机制,键像集合一样,你可以在常量时间内通过键查找值。而在元组列表(这里是对的)中,你需要遍历列表直到找到键,这意味着查找的时间复杂度是线性的。
最重要的是,字典的项可以被改变,而元组则不能。
列表不就是一种具有不同唯一性约束的集合吗?
我再次强调,集合没有固有的顺序,而列表是有的。这使得列表在表示像栈和队列这样的结构时更有用,因为你想记住添加项的顺序。而集合则没有这样的保证。不过,集合在进行成员查找时可以在常量时间内完成,而列表则需要线性时间。
现在有了命名元组——开始感觉更像是一个特殊情况的字典。现在有了有序字典——开始感觉更像是一个列表。我刚看到一个有序集合的例子。我可以想象这种情况会不断出现……那独特的列表呢?
在某种程度上,我同意你的看法。然而,数据结构库可以支持已经成熟的数据结构的常见用例。这可以避免程序员浪费时间去想出自定义扩展的标准结构。只要不失控,并且我们仍然能看到每种解决方案的独特价值,拥有一个备用的工具是好的,这样我们就不需要重新发明轮子。
一个很好的例子是Counter()类。这个专门的字典对我帮助很大,次数多到我数不清(哈哈!),它让我省去了编写自定义解决方案的麻烦。我更愿意使用一个社区帮助我开发并遵循Python最佳实践的解决方案,而不是一个只在我自定义数据结构文件夹中偶尔用到的东西。
这些数据类型各自有不同的用途。在理想情况下,你可能希望把它们统一起来,但在现实中,我们需要高效地实现基本的集合类型,比如说,排序会带来运行时的额外开销。
命名元组主要是为了让像stat()这样的接口更好用,同时在处理SQL行集时也会很方便。
你想要的大统一其实已经存在了,表现为不同的访问方式(比如getitem、getattr、iter等),这些类型会根据它们的用途灵活组合使用。