- 内容提要
- 作者简介
- 技术评审者简介
- 致谢
- 译者序 会编程的人不一样
- 前言
- 本书的读者对象
- 编码规范
- 什么是编程
- 本书简介
- 下载和安装 Python
- 启动 IDLE
- 如何寻求帮助
- 聪明地提出编程问题
- 小结
- 第一部分 Python 编程基础
- 第1章 Python 基础
- 第2章 控制流
- 第3章 函数
- 第4章 列表
- 第5章 字典和结构化数据
- 第6章 字符串操作
- 第二部分 自动化任务
- 第7章 模式匹配与正则表达式
- 第8章 读写文件
- 第9章 组织文件
- 第10章 调试
- 第11章 从 Web 抓取信息
- 第12章 处理 Excel 电子表格
- 第13章 处理 PDF 和 Word 文档
- 第14章 处理 CSV 文件和 JSON 数据
- 第15章 保持时间、计划任务和启动程序
- 第16章 发送电子邮件和短信
- 第17章 操作图像
- 第18章 用 GUI 自动化控制键盘和鼠标
- 附录A 安装第三方模块
- 附录B 运行程序
- 附录C 习题答案
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| |X Turn for X. Move on which space? low-M O|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()函数,就会节约大量的时间!
你可以用自己喜欢的任何方法,用数据结构对事物建模,只要程序中其他代码能够正确处理这个数据模型。在刚开始编程时,不需要太担心数据建模的“正确”方式。随着经验增加,你可能会得到更有效的模型,但重要的是,该数据模型符合程序的需要。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论