数据结构

数据结构(Data Structures)基本上人如其名——它们只是一种结构,能够将一些数据聚合在一起。换句话说,它们是用来存储一系列相关数据的集合。

Python 中有四种内置的数据结构——列表(List)、元组(Tuple)、字典(Dictionary)和集合(Set)。我们将了解如何使用它们,并利用它们将我们的编程之路变得更加简单。

列表

列表 是一种用于保存一系列有序项目的集合,也就是说,你可以利用列表保存一串项目的序列。想象起来也不难,你可以想象你有一张购物清单,上面列出了需要购买的商品,除开在购物清单上你可能为每件物品都单独列一行,在 Python 中你需要在它们之间多加上一个逗号。

项目的列表应该用方括号括起来,这样 Python 才能理解到你正在指定一张列表。一旦你创建了一张列表,你可以添加、移除或搜索列表中的项目。既然我们可以添加或删除项目,我们会说列表是一种可变的(Mutable)数据类型,意即,这种类型是可以被改变的。

有关对象与类的快速介绍

虽然到目前为止我经常推迟有关对象(Object)与类(Class)的讨论,但现在对它们进行稍许解释能够有助于你更好地理解列表。我们将在后面的章节讨论有关它们的更多细节。

列表是使用对象与类的实例。当我们启用一个变量 i 并将整数 5 赋值给它时,你可以认为这是在创建一个 int (即类型)之下的对象(即实例) i。实际上,你可以阅读 help(int) 来了解更多内容。

一个类也可以带有方法(Method),也就是说对这个类定义仅对于它启用某个函数。只有当你拥有一个属于该类的对象时,你才能使用这些功能。举个例子,Python 为 list 类提供了一种 append 方法,能够允许你向列表末尾添加一个项目。例如 mylist.append('an item') 将会向列表 mylist 添加一串字符串。在这里要注意到我们通过使用点号的方法来访问对象。

一个类同样也可以具有字段(Field),它是只为该类定义且只为该类所用的变量。只有当你拥有一个属于该类的对象时,你才能够使用这些变量或名称。字段同样可以通过点号来访问,例如 mylist.field

案例(保存为 ds_using_list.py):

# This is my shopping list
shoplist = ['apple', 'mango', 'carrot', 'banana']

print('I have', len(shoplist), 'items to purchase.')

print('These items are:', end=' ')
for item in shoplist:
    print(item, end=' ')

print('\nI also have to buy rice.')
shoplist.append('rice')
print('My shopping list is now', shoplist)

print('I will sort my list now')
shoplist.sort()
print('Sorted shopping list is', shoplist)

print('The first item I will buy is', shoplist[0])
olditem = shoplist[0]
del shoplist[0]
print('I bought the', olditem)
print('My shopping list is now', shoplist)

输出:

$ python ds_using_list.py
I have 4 items to purchase.
These items are: apple mango carrot banana
I also have to buy rice.
My shopping list is now ['apple', 'mango', 'carrot', 'banana', 'rice']
I will sort my list now
Sorted shopping list is ['apple', 'banana', 'carrot', 'mango', 'rice']
The first item I will buy is apple
I bought the apple
My shopping list is now ['banana', 'carrot', 'mango', 'rice']

它是如何工作的

变量 shoplist 是一张为即将前往市场的某人准备的购物清单。在 shoplist 中,我们只存储了一些字符串,它们是我们需要购买的物品的名称,但是你可以向列表中添加任何类型的对象,包括数字,甚至是其它列表。

我们还使用 for...in 循环来遍历列表中的每一个项目。学习到现在,你必须有一种列表也是一个序列的意识。有关序列的特性将会在稍后的章节予以讨论。

在这里要注意在调用 print 函数时我们使用 end 参数,这样就能通过一个空格来结束输出工作,而不是通常的换行。

接下来,如我们讨论过的那般,我们通过列表对象中的 append 方法向列表中添加一个对象。然后,我们将列表简单地传递给 print 函数,整洁且完整地打印出列表内容,以此来检查项目是否被切实地添加进列表之中。

接着,我们列表的 sort 方法对列表进行排序。在这里要着重理解到这一方法影响到的是列表本身,而不会返回一个修改过的列表——这与修改字符串的方式并不相同。同时,这也是我们所说的,列表是可变的(Mutable)而字符串是不可变的(Immutable)

随后,当我们当我们在市场上买回某件商品时,我们需要从列表中移除它。我们通过使用 del 语句来实现这一需求。在这里,我们将给出我们希望从列表中移除的商品,del 语句则会为我们从列表中移除对应的项目。我们希望移除列表中的第一个商品,因此我们使用 del shoplist[0](要记住 Python 从 0 开始计数)。

如果你想了解列表对象定义的所有方法,可以通过 help(list) 来了解更多细节。

元组

元组(Tuple)用于将多个对象保存到一起。你可以将它们近似地看作列表,但是元组不能提供列表类能够提供给你的广泛的功能。元组的一大特征类似于字符串,它们是不可变的,也就是说,你不能编辑或更改元组。

元组是通过特别指定项目来定义的,在指定项目时,你可以给它们加上括号,并在括号内部用逗号进行分隔。

元组通常用于保证某一语句或某一用户定义的函数可以安全地采用一组数值,意即元组内的数值不会改变。

案例(保存为 ds_using_tuple.py):

# 我会推荐你总是使用括号
# 来指明元组的开始与结束
# 尽管括号是一个可选选项。
# 明了胜过晦涩,显式优于隐式。
zoo = ('python', 'elephant', 'penguin')
print('Number of animals in the zoo is', len(zoo))

new_zoo = 'monkey', 'camel', zoo
print('Number of cages in the new zoo is', len(new_zoo))
print('All animals in new zoo are', new_zoo)
print('Animals brought from old zoo are', new_zoo[2])
print('Last animal brought from old zoo is', new_zoo[2][2])
print('Number of animals in the new zoo is',
      len(new_zoo)-1+len(new_zoo[2]))

输出:

$ python ds_using_tuple.py
Number of animals in the zoo is 3
Number of cages in the new zoo is 3
All animals in new zoo are ('monkey', 'camel', ('python', 'elephant', 'penguin'))
Animals brought from old zoo are ('python', 'elephant', 'penguin')
Last animal brought from old zoo is penguin
Number of animals in the new zoo is 5

它是如何工作的

变量 zoo 指的是一个包含项目的元组。我们能够看到 len 函数在此处用来获取元组的长度。这也表明元组同时也是一个序列

现在,我们将这些动物从即将关闭的老动物园(Zoo)转移到新的动物园中。因此,new_zoo 这一元组包含了一些本已存在的动物以及从老动物园转移过去的动物。让我们回到话题中来,在这里要注意到元组中所包含的元组不会失去其所拥有的身份。

如同我们在列表里所做的那般,我们可以通过在方括号中指定项目所处的位置来访问元组中的各个项目。这种使用方括号的形式被称作索引(Indexing)运算符。我们通过指定 new_zoo[2] 来指定 new_zoo 中的第三个项目,我们也可以通过指定 new_zoo[2][2] 来指定 new_zoo 元组中的第三个项目中的第三个项目1。一旦你习惯了这种语法你就会觉得这其实非常简单。

包含 0 或 1 个项目的元组

一个空的元组由一对圆括号构成,就像 myempty = () 这样。然而,一个只拥有一个项目的元组并不像这样简单。你必须在第一个(也是唯一一个)项目的后面加上一个逗号来指定它,如此一来 Python 才可以识别出在这个表达式想表达的究竟是一个元组还是只是一个被括号所环绕的对象,也就是说,如果你想指定一个包含项目 2 的元组,你必须指定 singleton = (2, )

针对 Perl 程序员的提示

列表中的列表不会丢失其标识,即列表不会像在 Perl 里那般会被打散(Flattened)。这同样也适用于元组中的元组、列表中的元组或元组中的列表等等情况。对于 Python 而言,它们只是用一个对象来存储另一个对象,不过仅此而已。

字典

字典就像一本地址簿,如果你知道了他或她的姓名,你就可以在这里找到其地址或是能够联系上对方的更多详细信息,换言之,我们将键值(Keys)(即姓名)与值(Values)(即地址等详细信息)联立到一起。在这里要注意到键值必须是唯一的,正如在现实中面对两个完全同名的人你没办法找出有关他们的正确信息。

另外要注意的是你只能使用不可变的对象(如字符串)作为字典的键值,但是你可以使用可变或不可变的对象作为字典中的值。基本上这段话也可以翻译为你只能使用简单对象作为键值。

在字典中,你可以通过使用符号构成 d = {key : value1 , key2 : value2} 这样的形式,来成对地指定键值与值。在这里要注意到成对的键值与值之间使用冒号分隔,而每一对键值与值则使用逗号进行区分,它们全都由一对花括号括起。

另外需要记住,字典中的成对的键值—值配对不会以任何方式进行排序。如果你希望为它们安排一个特别的次序,只能在使用它们之前自行进行排序。

你将要使用的字典是属于 dict 类下的实例或对象。

案例(保存为 ds_using_dict.py):

# “ab”是地址(Address)簿(Book)的缩写

ab = {
    'Swaroop': 'swaroop@swaroopch.com',
    'Larry': 'larry@wall.org',
    'Matsumoto': 'matz@ruby-lang.org',
    'Spammer': 'spammer@hotmail.com'
}

print("Swaroop's address is", ab['Swaroop'])

# 删除一对键值—值配对
del ab['Spammer']

print('\nThere are {} contacts in the address-book\n'.format(len(ab)))

for name, address in ab.items():
    print('Contact {} at {}'.format(name, address))

# 添加一对键值—值配对
ab['Guido'] = 'guido@python.org'

if 'Guido' in ab:
    print("\nGuido's address is", ab['Guido'])

输出:

$ python ds_using_dict.py
Swaroop's address is swaroop@swaroopch.com

There are 3 contacts in the address-book

Contact Swaroop at swaroop@swaroopch.com
Contact Matsumoto at matz@ruby-lang.org
Contact Larry at larry@wall.org

Guido's address is guido@python.org

它是如何工作的

我们通过已经讨论过的符号体系来创建字典 ab。然后我们通过使用索引运算符来指定某一键值以访问相应的键值—值配对,有关索引运算符的方法我们已经在列表与元组部分讨论过了。你可以观察到这之中的语法非常简单。

我们可以通过我们的老朋友——del 语句——来删除某一键值—值配对。我们只需指定字典、包含需要删除的键值名称的索引算符,并将其传递给 del 语句。这一操作不需要你知道与该键值相对应的值。

接着,我们通过使用字典的 items 方法来访问字典中的每一对键值—值配对信息,这一操作将返回一份包含元组的列表,每一元组中则包含了每一对相应的信息——键值以及其相应的值。我们检索这一配对,并通过 for...in 循环将每一对配对的信息相应地分配给 nameaddress 变量,并将结果打印在 for 代码块中。

如果想增加一堆新的键值—值配对,我们可以简单地通过使用索引运算符访问一个键值并为其分配与之相应的值,就像我们在上面的例子中对 Guido 键值所做的那样。

我们可以使用 in 运算符来检查某对键值—值配对是否存在。

要想了解有关 dict 类的更多方法,请参阅 help(dict)

关键字参数与字典

如果你曾在你的函数中使用过关键词参数,那么你就已经使用过字典了!你只要这么想——你在定义函数时的参数列表时,就指定了相关的键值—值配对。当你在你的函数中访问某一变量时,它其实就是在访问字典中的某个键值。(在编译器设计的术语中,这叫作符号表(Symbol Table)

序列

列表、元组和字符串可以看作序列(Sequence)的某种表现形式,可是究竟什么是序列,它又有什么特别之处?

序列的主要功能是资格测试(Membership Test)(也就是 innot in 表达式)和索引操作(Indexing Operations),它们能够允许我们直接获取序列中的特定项目。

上面所提到的序列的三种形态——列表、元组与字符串,同样拥有一种切片(Slicing)运算符,它能够允许我们序列中的某段切片——也就是序列之中的一部分。

案例(保存为 ds_seq.py):

shoplist = ['apple', 'mango', 'carrot', 'banana']
name = 'swaroop'

# Indexing or 'Subscription' operation #
# 索引或“下标(Subscription)”操作符 #
print('Item 0 is', shoplist[0])
print('Item 1 is', shoplist[1])
print('Item 2 is', shoplist[2])
print('Item 3 is', shoplist[3])
print('Item -1 is', shoplist[-1])
print('Item -2 is', shoplist[-2])
print('Character 0 is', name[0])

# Slicing on a list #
print('Item 1 to 3 is', shoplist[1:3])
print('Item 2 to end is', shoplist[2:])
print('Item 1 to -1 is', shoplist[1:-1])
print('Item start to end is', shoplist[:])

# 从某一字符串中切片 #
print('characters 1 to 3 is', name[1:3])
print('characters 2 to end is', name[2:])
print('characters 1 to -1 is', name[1:-1])
print('characters start to end is', name[:])

输出:

$ python ds_seq.py
Item 0 is apple
Item 1 is mango
Item 2 is carrot
Item 3 is banana
Item -1 is banana
Item -2 is carrot
Character 0 is s
Item 1 to 3 is ['mango', 'carrot']
Item 2 to end is ['carrot', 'banana']
Item 1 to -1 is ['mango', 'carrot']
Item start to end is ['apple', 'mango', 'carrot', 'banana']
characters 1 to 3 is wa
characters 2 to end is aroop
characters 1 to -1 is waroo
characters start to end is swaroop

它是如何工作的

首先,我们已经了解了如何通过使用索引来获取序列中的各个项目。这也被称作下标操作(Subscription Operation)。如上所示,每当你在方括号中为序列指定一个数字,Python 将获取序列中与该位置编号相对应的项目。要记得 Python 从 0 开始计数。因此 shoplist[0] 将获得 shoplist 序列中的第一个项目,而 shoplist[3] 将获得第四个项目。

索引操作也可以使用负数,在这种情况下,位置计数将从队列的末尾开始。因此,shoplist[-1] 指的是序列的最后一个项目,shoplist[-2] 将获取序列中倒数第二个项目。

你需要通过指定序列名称来进行序列操作,在指定时序列名称后面可以跟一对数字——这是可选的操作,这一对数字使用方括号括起,并使用冒号分隔。在这里需要注意,它与你至今为止使用的索引操作显得十分相像。但是你要记住数字是可选的,冒号却不是。

在切片操作中,第一个数字(冒号前面的那位)指的是切片开始的位置,第二个数字(冒号后面的那位)指的是切片结束的位置。如果第一位数字没有指定,Python 将会从序列的起始处开始操作。如果第二个数字留空,Python 将会在序列的末尾结束操作。要注意的是切片操作会在开始处返回 start,并在 end 前面的位置结束工作。也就是说,序列切片将包括起始位置,但不包括结束位置。

因此,shoplist[1:3] 返回的序列的一组切片将从位置 1 开始,包含位置 2 并在位置 3 时结束,因此,这块切片返回的是两个项目。类似地,shoplist[:] 返回的是整个序列。

你同样可以在切片操作中使用负数位置。使用负数时位置将从序列末端开始计算。例如,shoplist[:-1] 强返回一组序列切片,其中不包括序列的最后一项项目,但其它所有项目都包含其中。

你同样可以在切片操作中提供第三个参数,这一参数将被视为切片的步长(Step)(在默认情况下,步长大小为 1):

>>> shoplist = ['apple', 'mango', 'carrot', 'banana']
>>> shoplist[::1]
['apple', 'mango', 'carrot', 'banana']
>>> shoplist[::2]
['apple', 'carrot']
>>> shoplist[::3]
['apple', 'banana']
>>> shoplist[::-1]
['banana', 'carrot', 'mango', 'apple']

你会注意到当步长为 2 时,我们得到的是第 0、2、4…… 位项目。当步长为 3 时,我们得到的是第 0、3……位项目。

你可以在 Python 解释器中交互地尝试不同的切片方式的组合,这将帮助你立即看到结果。序列的一大优点在于你可以使用同样的方式访问元组、列表与字符串。

集合

集合(Set)是简单对象的无序集合(Collection)。当集合中的项目存在与否比起次序或其出现次数更加重要时,我们就会使用集合。

通过使用集合,你可以测试某些对象的资格或情况,检查它们是否是其它集合的子集,找到两个集合的交集,等等。

>>> bri = set(['brazil', 'russia', 'india'])
>>> 'india' in bri
True
>>> 'usa' in bri
False
>>> bric = bri.copy()
>>> bric.add('china')
>>> bric.issuperset(bri)
True
>>> bri.remove('russia')
>>> bri & bric # OR bri.intersection(bric)
{'brazil', 'india'}

它是如何工作的

这个案例几乎不言自明,因为它涉及的是学校所教授的数学里的基础集合知识。

引用2

当你创建了一个对象并将其分配给某个变量时,变量只会查阅(Refer)某个对象,并且它也不会代表对象本身。也就是说,变量名只是指向你计算机内存中存储了相应对象的那一部分。这叫作将名称绑定(Binding)给那一个对象。

一般来说,你不需要去关心这个,不过由于这一引用操作困难会产生某些微妙的效果,这是需要你注意的:

案例(保存为 ds_reference.py):

print('Simple Assignment')
shoplist = ['apple', 'mango', 'carrot', 'banana']
# mylist 只是指向同一对象的另一种名称
mylist = shoplist

# 我购买了第一项项目,所以我将其从列表中删除
del shoplist[0]

print('shoplist is', shoplist)
print('mylist is', mylist)
# 注意到 shoplist 和 mylist 二者都
# 打印出了其中都没有 apple 的同样的列表,以此我们确认
# 它们指向的是同一个对象

print('Copy by making a full slice')
# 通过生成一份完整的切片制作一份列表的副本
mylist = shoplist[:]
# 删除第一个项目
del mylist[0]

print('shoplist is', shoplist)
print('mylist is', mylist)
# 注意到现在两份列表已出现不同

输出:

$ python ds_reference.py
Simple Assignment
shoplist is ['mango', 'carrot', 'banana']
mylist is ['mango', 'carrot', 'banana']
Copy by making a full slice
shoplist is ['mango', 'carrot', 'banana']
mylist is ['carrot', 'banana']

它是如何工作的

大部分解释已经在注释中提供。

你要记住如果你希望创建一份诸如序列等复杂对象的副本(而非整数这种简单的对象(Object)),你必须使用切片操作来制作副本。如果你仅仅是将一个变量名赋予给另一个名称,那么它们都将“查阅”同一个对象,如果你对此不够小心,那么它将造成麻烦。

针对 Perl 程序员的提示

要记住列表的赋值语句不会创建一份副本。你必须使用切片操作来生成一份序列的副本。

有关字符串的更多内容

在早些时候我们已经详细讨论过了字符串。还有什么可以知道的吗?还真有,想必你还不知道字符串同样也是一种对象,并且它也具有自己的方法,可以做到检查字符串中的一部分或是去掉空格等几乎一切事情!

你在程序中使用的所有字符串都是 str 类下的对象。下面的案例将演示这种类之下一些有用的方法。要想获得这些方法的完成清单,你可以查阅 help(str)

案例(保存为 ds_str_methods.py):

# 这是一个字符串对象
name = 'Swaroop'

if name.startswith('Swa'):
    print('Yes, the string starts with "Swa"')

if 'a' in name:
    print('Yes, it contains the string "a"')

if name.find('war') != -1:
    print('Yes, it contains the string "war"')

delimiter = '_*_'
mylist = ['Brazil', 'Russia', 'India', 'China']
print(delimiter.join(mylist))

输出:

$ python ds_str_methods.py
Yes, the string starts with "Swa"
Yes, it contains the string "a"
Yes, it contains the string "war"
Brazil_*_Russia_*_India_*_China

它是如何工作的

在这里,我们会看见一此操作中包含了好多字符串方法。startswith 方法用于查找字符串是否以给定的字符串内容开头。in 运算符用以检查给定的字符串是否是查询的字符串中的一部分。

find 方法用于定位字符串中给定的子字符串的位置。如果找不到相应的子字符串,find 会返回 -1。str 类同样还拥有一个简洁的方法用以 联结(Join)序列中的项目,其中字符串将会作为每一项目之间的分隔符,并以此生成并返回一串更大的字符串。

总结

我们已经详细探讨了 Python 中内置的多种不同的数据结构。这些数据结构对于编写大小适中的 Python 程序而言至关重要。

现在我们已经具备了诸多有关 Python 的基本知识,接下来我们将会了解如何设计并编写一款真实的 Python 程序。


1. 第一个“第三个项目”可以是指元组中的元组。
2. 原文作“Reference”,沈洁元译本译作“参考”。此处译名尚存疑,如有更好的翻译建议还请指出。