在本章中,我将介绍字典数据类型,它提供了一种灵活的访问和组织数据的方式。然后,结合字典与前一章中关于列表的知识,你将学习如何创建一个数据结构,对井字棋盘建模。
5.1 字典数据类型
像列表一样,“字典”是许多值的集合。但不像列表的下标,字典的索引可以使用许多不同数据类型,不只是整数。字典的索引被称为“键”,键及其关联的值称为“键-值”对。
在代码中,字典输入时带花括号{}。在交互式环境中输入以下代码:
>>> myCat = {'size': 'fat', 'color': 'gray', 'disposition': 'loud'}
这将一个字典赋给myCat变量。这个字典的键是'size'、'color'和'disposition'。这些键相应的值是'fat'、'gray'和'loud'。可以通过它们的键访问这些值:
>>> myCat['size']'fat'>>> 'My cat has ' + myCat['color'] + ' fur.''My cat has gray fur.'
字典仍然可以用整数值作为键,就像列表使用整数值作为下标一样,但它们不必从0开始,可以是任何数字。
>>> spam = {12345: 'Luggage Combination', 42: 'The Answer'}
5.1.1 字典与列表
不像列表,字典中的表项是不排序的。名为spam的列表中,第一个表项是spam[0]。但字典中没有“第一个”表项。虽然确定两个列表是否相同时,表项的顺序很重要,但在字典中,键-值对输入的顺序并不重要。在交互式环境中输入以下代码:
>>> spam = ['cats', 'dogs', 'moose']>>> bacon = ['dogs', 'moose', 'cats']>>> spam == baconFalse>>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'}>>> ham = {'species': 'cat', 'age': '8', 'name': 'Zophie'}>>> eggs == hamTrue
因为字典是不排序的,所以不能像列表那样切片。
尝试访问字典中不存在的键,将导致KeyError出错信息。这很像列表的“越界”IndexError出错信息。在交互式环境中输入以下代码,并注意显示的出错信息,因为没有'color'键:
>>> spam = {'name': 'Zophie', 'age': 7}>>> spam['color']Traceback (most recent call last): File "", line 1, in spam['color']KeyError: 'color'
尽管字典是不排序的,但可以用任意值作为键,这一点让你能够用强大的方式来组织数据。假定你希望程序保存朋友生日的数据,就可以使用一个字典,用名字作为键,用生日作为值。打开一个新的文件编辑窗口,输入以下代码,并保存为birthdays.py:
❶ birthdays = {'Alice': 'Apr 1', 'Bob': 'Dec 12', 'Carol': 'Mar 4'} while True: print('Enter a name: (blank to quit)') name = input if name == '': break❷ if name in birthdays:❸ print(birthdays[name] + ' is the birthday of ' + name) else: print('I do not have birthday information for ' + name) print('What is their birthday?') bday = input❹ birthdays[name] = bday print('Birthday database updated.')
你创建了一个初始的字典,将它保存在birthdays中❶。用in关键字,可以看看输入的名字是否作为键存在于字典中❷,就像查看列表一样。如果该名字在字典中,你可以用方括号访问关联的值❸。如果不在,你可以用同样的方括号语法和赋值操作符添加它❹。
运行这个程序,结果看起来如下所示:
Enter a name: (blank to quit)AliceApr 1 is the birthday of AliceEnter a name: (blank to quit)EveI do not have birthday information for EveWhat is their birthday?Dec 5Birthday database updated.Enter a name: (blank to quit)EveDec 5 is the birthday of EveEnter a name: (blank to quit)
当然,在程序终止时,你在这个程序中输入的所有数据都丢失了。在第 8章中,你将学习如何将数据保存在硬盘的文件中。
5.1.2 keys、values和items方法
有3个字典方法,它们将返回类似列表的值,分别对应于字典的键、值和键-值对:keys、values和items。这些方法返回的值不是真正的列表,它们不能被修改,没有append方法。但这些数据类型(分别是dict_keys、dict_values和dict_items)可以用于for循环。为了看看这些方法的工作原理,请在交互式环境中输入以下代码:
>>> spam = {'color': 'red', 'age': 42}>>> for v in spam.values:print(v) red42
这里,for循环迭代了spam字典中的每个值。for循环也可以迭代每个键,或者键-值对:
>>> for k in spam.keys:print(k) colorage>>> for i in spam.items:print(i) ('color', 'red')('age', 42)
利用keys、values和items方法,循环分别可以迭代键、值或键-值对。请注意,items方法返回的dict_items值中,包含的是键和值的元组。
如果希望通过这些方法得到一个真正的列表,就把类似列表的返回值传递给 list函数。在交互式环境中输入以下代码:
>>> spam = {'color': 'red', 'age': 42}>>> spam.keysdict_keys(['color', 'age'])>>> list(spam.keys)['color', 'age']
list(spam.keys)代码行接受keys函数返回的dict_keys值,并传递给list。然后返回一个列表,即['color', 'age']。
也可以利用多重赋值的技巧,在for循环中将键和值赋给不同的变量。在交互式环境中输入以下代码:
>>> spam = {'color': 'red', 'age': 42}>>> for k, v in spam.items:print('Key: ' + k + ' Value: ' + str(v)) Key: age Value: 42Key: color Value: red
5.1.3 检查字典中是否存在键或值
回忆一下,前一章提到,in和not in操作符可以检查值是否存在于列表中。也可以利用这些操作符,检查某个键或值是否存在于字典中。在交互式环境中输入以下代码:
>>> spam = {'name': 'Zophie', 'age': 7}>>> 'name' in spam.keysTrue>>> 'Zophie' in spam.valuesTrue>>> 'color' in spam.keysFalse>>> 'color' not in spam.keysTrue>>> 'color' in spamFalse
请注意,在前面的例子中,'color' in spam本质上是一个简写版本。相当于'color' in spam.keys。这种情况总是对的:如果想要检查一个值是否为字典中的键,就可以用关键字in(或not in),作用于该字典本身。
5.1.4 get方法
在访问一个键的值之前,检查该键是否存在于字典中,这很麻烦。好在,字典有一个get方法,它有两个参数:要取得其值的键,以及如果该键不存在时,返回的备用值。
在交互式环境中输入以下代码:
>>> picnicItems = {'apples': 5, 'cups': 2}>>> 'I am bringing ' + str(picnicItems.get('cups', 0)) + ' cups.''I am bringing 2 cups.'>>> 'I am bringing ' + str(picnicItems.get('eggs', 0)) + ' eggs.''I am bringing 0 eggs.'
因为picnicItems字典中没有'egg'键,get方法返回的默认值是0。不使用get,代码就会产生一个错误消息,就像下面的例子:
>>> picnicItems = {'apples': 5, 'cups': 2}>>> 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.'Traceback (most recent call last): File "<pyshell#34>", line 1, in 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.'KeyError: 'eggs'
5.1.5 setdefault方法
你常常需要为字典中某个键设置一个默认值,当该键没有任何值时使用它。代码看起来像这样:
spam = {'name': 'Pooka', 'age': 5}if 'color' not in spam: spam['color'] = 'black'
setdefault方法提供了一种方式,在一行中完成这件事。传递给该方法的第一个参数,是要检查的键。第二个参数,是如果该键不存在时要设置的值。如果该键确实存在,方法就会返回键的值。在交互式环境中输入以下代码:
>>> spam = {'name': 'Pooka', 'age': 5}>>> spam.setdefault('color', 'black')'black'>>> spam{'color': 'black', 'age': 5, 'name': 'Pooka'}>>> spam.setdefault('color', 'white')'black'>>> spam{'color': 'black', 'age': 5, 'name': 'Pooka'}
第一次调用setdefault时,spam变量中的字典变为{'color': 'black', 'age': 5, 'name': 'Pooka'}。该方法返回值'black',因为现在该值被赋给键'color'。当spam.setdefault('color', 'white')接下来被调用时,该键的值“没有”被改变成'white',因为spam变量已经有名为'color'的键。
setdefault方法是一个很好的快捷方式,可以确保一个键存在。下面有一个小程序,计算一个字符串中每个字符出现的次数。打开一个文件编辑器窗口,输入以下代码,保存为characterCount.py:
message = 'It was a bright cold day in April, and the clocks were striking thirteen.'count = {}for character in message: count.setdefault(character, 0) count[character] = count[character] + 1print(count)
程序循环迭代message字符串中的每个字符,计算每个字符出现的次数。setdefault方法调用确保了键存在于count字典中(默认值是0),这样在执行count[character] = count[character] + 1时,就不会抛出KeyError错误。程序运行时,输出如下:
{' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, 'a': 4, 'c': 3, 'b': 1, 'e': 5, 'd': 3, 'g': 2, 'i':6, 'h': 3, 'k': 2, 'l': 3, 'o': 2, 'n': 4, 'p': 1, 's': 3, 'r': 5, 't': 6, 'w': 2, 'y': 1}
从输出可以看到,小写字母c出现了3次,空格字符出现了13次,大写字母A出现了1次。无论message变量中包含什么样的字符串,这个程序都能工作,即使该字符串有上百万的字符!
5.2 漂亮打印
如果程序中导入pprint模块,就可以使用pprint和pformat函数,它们将“漂亮打印”一个字典的字。如果想要字典中表项的显示比print的输出结果更干净,这就有用了。修改前面的characterCount.py程序,将它保存为prettyCharacterCount.py。
import pprintmessage = 'It was a bright cold day in April, and the clocks were strikingthirteen.'count = {}for character in message: count.setdefault(character, 0) count[character] = count[character] + 1pprint.pprint(count)
这一次,当程序运行时,输出看起来更干净,键排过序。
{' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, 'a': 4, 'b': 1, 'c': 3, 'd': 3, 'e': 5, 'g': 2, 'h': 3, 'i': 6, 'k': 2, 'l': 3, 'n': 4, 'o': 2, 'p': 1, 'r': 5, 's': 3, 't': 6, 'w': 2, 'y': 1}
如果字典本身包含嵌套的列表或字典,pprint.pprint函数就特别有用。
如果希望得到漂亮打印的文本作为字符串,而不是显示在屏幕上,那就调用pprint.pformat。下面两行代码是等价的:
pprint.pprint(someDictionaryValue)print(pprint.pformat(someDictionaryValue))
5.3 使用数据结构对真实世界建模
甚至在因特网之前,人们也有办法与世界另一边的某人下一盘国际象棋。每个棋手在自己家里放好一个棋盘,然后轮流向对方寄出明信片,描述每一着棋。要做到这一点,棋手需要一种方法,无二义地描述棋盘的状态,以及他们的着法。
在“代数记谱法”中,棋盘空间由一个数字和字母坐标确定,如图5-1所示。
图5-1 代数记谱法中棋盘的坐标
棋子用字母表示:K表示王,Q表示后,R表示车,B表示象,N表示马。描述一次移动,用棋子的字母和它的目的地坐标。一对这样的移动表示一个回合(白方先下),例如,棋谱2. Nf3 Nc6表明在棋局的第二回合,白方将马移动到f3,黑方将马移动到c6。
代数记谱法还有更多内容,但要点是你可以用它无二义地描述象棋游戏,不需要站在棋盘前。你的对手甚至可以在世界的另一边!实际上,如果你的记忆力很好,甚至不需要物理的棋具:只需要阅读寄来的棋子移动,更新心里想的棋盘。
计算机有很好的记忆力。现在计算机上的程序,很容易存储几百万个像'2. Nf3 Nc6'这样的字符串。这就是为什么计算机不用物理棋盘就能下象棋。它们用数据建模来表示棋盘,你可以编写代码来使用这个模型。
这里就可以用到列表和字典。可以用它们对真实世界建模,例如棋盘。作为第一个例子,我们将使用比国际象棋简单一点的游戏:井字棋。
5.3.1 井字棋盘
井字棋盘看起来像一个大的井字符号(#),有9个空格,可以包含X、O或空。要用字典表示棋盘,可以为每个空格分配一个字符串键,如图5-2所示。
图5-2 井字棋盘的空格和它们对应的键
可以用字符串值来表示,棋盘上每个空格有什么:'X'、'O'或' '(空格字符)。因此,需要存储9个字符串。可以用一个字典来做这事。带有键'top-R'的字符串表示右上角,带有键'low-L'的字符串表示左下角,带有键'mid-M'的字符串表示中间,以此类推。
这个字典就是表示井字棋盘的数据结构。将这个字典表示的棋盘保存在名为theBoard的变量中。打开一个文件编辑器窗口,输入以下代码,并保存为ticTacToe.py:
theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '}
保存在theBoard变量中的数据结构,表示了图5-3中的井字棋盘。
图5-3 一个空的井字棋盘
因为theBoard变量中每个键的值都是单个空格字符,所以这个字典表示一个完全干净的棋盘。如果玩家X选择了中间的空格,就可以用下面这个字典来表示棋盘:
theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '}
theBoard变量中的数据结构现在表示图5-4中的井字棋盘。
图5-4 第一着
一个玩家O获胜的棋盘上,他将O横贯棋盘的顶部,看起来像这样:
theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'}
theBoard变量中的数据结构现在表示图5-5中的井字棋盘。
图5-5 玩家O获胜
当然,玩家只看到打印在屏幕上的内容,而不是变量的内容。让我们创建一个函数,将棋盘字典打印到屏幕上。将下面代码添加到ticTacToe.py(新代码是黑体的):
theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '}def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R'])printBoard(theBoard)
运行这个程序时,printBoard将打印出空白井字棋盘。
| |-+-+-| |-+-+-| |
printBoard函数可以处理传入的任何井字棋数据结构。尝试将代码改成以下的样子:
theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M':'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R'])printBoard(theBoard)
现在运行该程序,新棋盘将打印在屏幕上。
O|O|O-+-+-X|X|-+-+-| |X
因为你创建了一个数据结构来表示井字棋盘,编写了printBoard中的代码来解释该数据结构,所以就有了一个程序,对井字棋盘进行了“建模”。也可以用不同的方式组织数据结构(例如,使用'TOP-LEFT'这样的键来代替'top-L'),但只要代码能处理你的数据结构,就有了正确工作的程序。
例如,printBoard函数预期井字棋数据结构是一个字典,包含所有9个空格的键。假如传入的字典缺少'mid-L'键,程序就不能工作了。
O|O|O-+-+-Traceback (most recent call last): File "ticTacToe.py", line 10, in <module> printBoard(theBoard) File "ticTacToe.py", line 6, in printBoard print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R'])KeyError: 'mid-L'
现在让我们添加代码,允许玩家输入他们的着法。修改ticTacToe.py程序如下所示:
theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) turn = 'X' for i in range(9):❶ printBoard(theBoard) print('Turn for ' + turn + '. Move on which space?')❷ move = input❸ theBoard[move] = turn❹ if turn == 'X': turn = 'O' else: turn = 'X' printBoard(theBoard)
新的代码在每一步新的着法之前,打印出棋盘❶,获取当前棋手的着法❷,相应地更新棋盘❸,然后改变当前棋手❹,进入到下一着。
运行该程序,它看起来像这样:
| |-+-+- | |-+-+- | |Turn for X. Move on which space?mid-M | |-+-+- |X|-+-+- | |Turn for O. Move on which space?low-L | |-+-+- |X|-+-+-O| |--_snip_--O|O|X-+-+-X|X|O-+-+-O| |XTurn for X. Move on which space? low-MO|O|X-+-+-X|X|O-+-+-O|X|X
这不是一个完整的井字棋游戏(例如,它并不检查玩家是否获胜),但这已足够展示如何在程序中使用数据结构。
注意
如果你很好奇,完整的井字棋程序的源代码在网上有介绍,网址是http://nostarch.com/automatestuff/。
5.3.2 嵌套的字典和列表
对井字棋盘建模相当简单:棋盘只需要一个字典,包含9个键值对。当你对复杂的事物建模时,可能发现字典和列表中需要包含其他字典和列表。列表适用于包含一组有序的值,字典适合于包含关联的键与值。例如,下面的程序使用字典包含其他字典,用于记录谁为野餐带来了什么食物。totalBrought函数可以读取这个数据结构,计算所有客人带来的食物的总数。
allGuests = {'Alice': {'apples': 5, 'pretzels': 12}, 'Bob': {'ham sandwiches': 3, 'apples': 2}, 'Carol': {'cups': 3, 'apple pies': 1}} def totalBrought(guests, item): numBrought = 0❶ for k, v in guests.items:❷ numBrought = numBrought + v.get(item, 0) return numBrought print('Number of things being brought:') print(' - Apples ' + str(totalBrought(allGuests, 'apples'))) print(' - Cups ' + str(totalBrought(allGuests, 'cups'))) print(' - Cakes ' + str(totalBrought(allGuests, 'cakes'))) print(' - Ham Sandwiches ' + str(totalBrought(allGuests, 'ham sandwiches'))) print(' - Apple Pies ' + str(totalBrought(allGuests, 'apple pies')))
在totalBrought函数中,for循环迭代guests中的每个键值对❶。在这个循环里,客人的名字字符串赋给k,他们带来的野餐食物的字典赋给v。如果食物参数是字典中存在的键,它的值(数量)就添加到numBrought❷。如果它不是键,get方法就返回0,添加到numBrought。
该程序的输出像这样:
Number of things being brought:- Apples 7- Cups 3- Cakes 0- Ham Sandwiches 3- Apple Pies 1
这似乎对一个非常简单的东西建模,你可能认为不需要费事去写一个程序来做到这一点。但是要认识到,这个函数totalBrought可以轻易地处理一个字典,其中包含数千名客人,每个人都带来了“数千种”不同的野餐食物。这样用这种数据结构来保存信息,并使用totalBrought函数,就会节约大量的时间!
你可以用自己喜欢的任何方法,用数据结构对事物建模,只要程序中其他代码能够正确处理这个数据模型。在刚开始编程时,不需要太担心数据建模的“正确”方式。随着经验增加,你可能会得到更有效的模型,但重要的是,该数据模型符合程序的需要。
5.4 小结
在本章中,你学习了字典的所有相关知识。列表和字典是这样的值,它们可以包含多个值,包括其他列表和字典。字典是有用的,因为你可以把一些项(键)映射到另一些项(值),它不像列表,只包含一系列有序的值。字典中的值是通过方括号访问的,像列表一样。字典不是只能使用整数下标,而是可以用各种数据类型作为键:整型、浮点型、字符串或元组。通过将程序中的值组织成数据结构,你可以创建真实世界事物的模型。井字棋盘就是这样一个例子。
这就介绍了Python编程的所有基本概念!在本书后面的部分,你将继续学习一些新概念,但现在你已学习了足够多的内容,可以开始编写一些有用的程序,让一些任务自动化。你可能不觉得自己有足够的Python知识,来实现页面下载、更新电子表格,或发送文本消息。但这就是Python模块要干的事!这些模块由其他程序员编写,提供了一些函数,让这些事情变得容易。所以让我们学习如何编写真正的程序,实现有用的自动化任务。
5.5 习题
1.空字典的代码是怎样的?
2.一个字典包含键'fow'和值42,看起来是怎样的?
3.字典和列表的主要区别是什么?
4.如果spam是{'bar': 100},你试图访问spam['foo'],会发生什么?
5.如果一个字典保存在spam中,表达式'cat' in spam和'cat' in spam.keys之间的区别是什么?
6.如果一个字典保存在变量中,表达式'cat' in spam和'cat' in spam.values之间的区别是什么?
7.下面代码的简洁写法是什么?
if 'color' not in spam: spam['color'] = 'black'
8.什么模块和函数可以用于“漂亮打印”字典值?
5.6 实践项目
作为实践,编程完成下列任务。
5.6.1 好玩游戏的物品清单
你在创建一个好玩的视频游戏。用于对玩家物品清单建模的数据结构是一个字典。其中键是字符串,描述清单中的物品,值是一个整型值,说明玩家有多少该物品。例如,字典值{'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}意味着玩家有1条绳索、6个火把、42枚金币等。
写一个名为displayInventory的函数,它接受任何可能的物品清单,并显示如下:
Inventory:12 arrow42 gold coin1 rope6 torch1 daggerTotal number of items: 62
提示
你可以使用for循环,遍历字典中所有的键。
# inventory.pystuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}def displayInventory(inventory): print("Inventory:") item_total = 0 for k, v in inventory.items:print(str(v) + ' ' + k)item_total += v print("Total number of items: " + str(item_total))displayInventory(stuff)
5.6.2 列表到字典的函数,针对好玩游戏物品清单
假设征服一条龙的战利品表示为这样的字符串列表:
dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
写一个名为addToInventory(inventory, addedItems)的函数,其中inventory参数是一个字典,表示玩家的物品清单(像前面项目一样),addedItems参数是一个列表,就像dragonLoot。
addToInventory函数应该返回一个字典,表示更新过的物品清单。请注意,列表可以包含多个同样的项。你的代码看起来可能像这样:
def addToInventory(inventory, addedItems): # your code goes hereinv = {'gold coin': 42, 'rope': 1}dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']inv = addToInventory(inv, dragonLoot)displayInventory(inv)
前面的程序(加上前一个项目中的displayInventory函数)将输出如下:
Inventory:45 gold coin1 rope1 ruby1 daggerTotal number of items: 48