Python-自动化指南-繁琐工作自动化-第三版-四-
Python 自动化指南(繁琐工作自动化)第三版(四)
原文:
automatetheboringstuff.com/译者:飞龙
7 字典和数据结构

本章介绍了字典数据类型,它提供了一种灵活的方式来访问和组织数据。通过将字典与上一章中你了解的列表知识相结合,你还将学习如何创建一个数据结构来模拟棋盘。
字典数据类型
就像列表一样,字典也是一个可变的多值集合。但与列表的索引不同,字典的索引可以使用多种不同的数据类型,而不仅仅是整数。这些字典索引被称为键,而与关联值一起的键被称为键值对。
在代码中,字典是用花括号{}括起来的。在交互式外壳中输入以下内容:
>>> my_cat = {'size': 'fat', 'color': 'gray', 'age': 17}
这将一个字典赋值给my_cat变量。这个字典的键是'size'、'color'和'age',相应的值分别是'fat'、'gray'和17。你可以通过它们的键来访问这些值:
>>> my_cat['size']
'fat'
>>> 'My cat has ' + my_cat['color'] + ' fur.'
'My cat has gray fur.'
使用字典,你可以在单个变量中存储关于同一事物的多个数据片段。这个my_cat变量包含三个不同的字符串,描述我的猫,我可以用它作为函数调用的参数或返回值,从而避免需要创建三个单独的变量。
字典仍然可以使用整数作为键,就像列表使用整数作为索引一样,但它们不必从0开始,可以是任何数字:
>>> spam = {12345: 'Luggage Combination', 42: 'The Answer'}
>>> spam[12345]
'Luggage Combination'
>>> spam[42]
'The Answer'
>>> spam[0]
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
KeyError: 0
字典有键,而不是索引。在这个例子中,虽然spam字典有整数键12345和42,但它不像列表那样有从0到41的索引。
比较字典和列表
与列表不同,字典中的项是无序的。列表spam中的第一个项是spam[0]。但在字典中没有“第一个”项。虽然项的顺序对于确定两个列表是否相同很重要,但你可以在任何顺序下输入字典的键值对。在交互式外壳中输入以下内容:
>>> spam = ['cats', 'dogs', 'moose']
>>> bacon = ['dogs', 'moose', 'cats']
>>> spam == bacon # The order of list items matters.
False
>>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'}
>>> ham = {'species': 'cat', 'age': '8', 'name': 'Zophie'}
>>> eggs == ham # The order of dictionary key-value pairs doesn't matter.
True
eggs和ham字典即使我们以不同的顺序输入它们的键值对,其值也是相同的。因为字典是无序的,它不是一个序列数据类型,不能像列表那样进行切片。
尝试访问字典中不存在的键将导致KeyError错误消息,就像列表的“越界”IndexError错误消息一样。在交互式外壳中输入以下内容,注意显示的错误消息,因为不存在'color'键:
>>> spam = {'name': 'Zophie', 'age': 7}
>>> spam['color']
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
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: # ❷
if name in birthdays: # ❷
else:
print('I do not have birthday information for ' + name)
print('What is their birthday?')
bday = input()
if name in birthdays: # ❷
print('Birthday database updated.')
代码创建了一个初始字典并将其存储在 birthdays ❶ 中。你可以使用 in 关键字检查输入的名字是否作为字典中的键存在,就像你为列表做的那样。如果名字在字典中,你可以使用方括号访问相关的值 ❷;如果不在,你可以使用相同的方括号语法结合赋值运算符添加它 ❸。
当你运行这个程序时,它看起来会是这样:
Enter a name: (blank to quit)
Alice
Apr 1 is the birthday of Alice
Enter a name: (blank to quit)
Eve
I do not have birthday information for Eve
What is their birthday?
Dec 5
Birthday database updated.
Enter a name: (blank to quit)
Eve
Dec 5 is the birthday of Eve
Enter a name: (blank to quit)
当然,你在这个程序中输入的所有数据在程序终止时都会被遗忘。你将在第十章学习如何将数据保存到硬盘上的文件中。
返回键和值
有三种字典方法会返回字典的键、值或键值对的列表-like 值:keys()、values() 和 items()。这些方法返回的值不是真正的列表:它们不能被修改,也没有 append() 方法。但这些数据类型(分别为 dict_keys、dict_values 和 dict_items)可以在 for 循环中使用。要查看这些方法的工作方式,在交互式壳中输入以下内容:
>>> spam = {'color': 'red', 'age': 42}
>>> for v in spam.values():
... print(v)
red
42
在这里,一个 for 循环遍历 spam 字典中的每个值。for 循环也可以遍历键或键值对:
>>> for k in spam.keys():
... print(k)
color
age
>>> 'color' in spam.keys()
True
>>> 'age' not in spam.keys()
False
>>> 'red' in spam.values()
True
>>> for i in spam.items():
... print(i)
('color', 'red')
('age', 42)
当你使用 keys()、values() 和 items() 方法时,for 循环可以分别遍历字典中的键、值或键值对,你可以使用 in 和 not in 操作符来确定值是否作为键或值存在于字典中。注意,items() 方法返回的 dict_items 值中的值是键和值的元组。
你还可以使用 in 和 not in 操作符与字典值本身来检查键的存在。这相当于使用这些操作符与 keys() 方法:
>>> 'color' in spam
True
>>> 'color' in spam.keys()
True
如果你想要从这些方法中获取实际的列表,将它们的列表-like 返回值传递给 list() 函数。在交互式壳中输入以下内容:
>>> spam = {'color': 'red', 'age': 42}
>>> spam.keys() # Returns a list-like dict_keys value
dict_keys(['color', 'age'])
>>> list(spam.keys()) # Returns an actual list value
['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: ' + str(k) + ' Value: ' + str(v))
Key: color Value: red
Key: age Value: 42
此代码创建了一个包含键'color'和'age'的字典,其值分别为'red'和42。for循环遍历由items()方法返回的元组:('color', 'red')和('age', 42)。变量k和v分别从这些元组中分配第一个(键)和第二个(值)值。循环体打印出每个键值对的k和v变量。
虽然你可以为键使用许多值,但你不能在字典中使用列表或字典作为键。这些数据类型是不可哈希的,这是本书范围之外的概念。如果你需要一个列表作为字典键,请使用元组代替。
检查键是否存在
在访问字典中键的值之前检查键的存在可能会很麻烦。幸运的是,字典有一个get()方法,它接受两个参数:要检索的值的键和一个回退值,如果该键不存在则返回该回退值。
在交互式外壳中输入以下内容:
>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items.get('cups', 0)) + ' cups.'
'I am bringing 2 cups.'
>>> 'I am bringing ' + str(picnic_items.get('eggs', 0)) + ' eggs.'
'I am bringing 0 eggs.'
因为picnic_items字典中没有'eggs'键,所以get()方法返回默认值0。如果不使用get(),代码将导致错误信息,如下面的示例所示:
>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
KeyError: 'eggs'
在访问键的值之前检查键的存在可以防止程序因错误信息而崩溃。
设置默认值
你通常只有在键尚未有值时才需要在字典中为某个键设置值。你的代码可能看起来像这样:
>>> spam = {'name': 'Pooka', 'age': 5}
>>> if 'color' not in spam:
... spam['color'] = 'black'
...
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
setdefault()方法提供了一种在单行代码中完成此操作的方法。传递给方法的第一个参数是要检查的键,第二个参数是在键不存在时在该键处设置的值。如果键存在,setdefault()方法返回键的值。在交互式外壳中输入以下内容:
>>> spam = {'name': 'Pooka', 'age': 5}
>>> spam.setdefault('color', 'black') # Sets 'color' key to 'black'
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
>>> spam.setdefault('color', 'white') # Does nothing
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
第一次调用setdefault()时,spam字典变为{'name': 'Pooka', 'age': 5, 'color': 'black'}。该方法返回值'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] + 1 ❷
print(count)
程序遍历message变量字符串中的每个字符,计算每个字符出现的次数。❶处的setdefault()方法调用确保键在count字典中(默认值为0),这样在执行count[character] = count[character] + 1时程序不会抛出KeyError错误。当你运行此程序时,输出将如下所示:
{'I': 1, 't': 6, ' ': 13, 'w': 2, 'a': 4, 's': 3, 'b': 1, 'r': 5, 'i': 6,
'g': 2, 'h': 3, 'c': 3, 'o': 2, 'l': 3, 'd': 3, 'y': 1, 'n': 4, 'A': 1,
'p': 1, ',': 1, 'e': 5, 'k': 2, '.': 1}
从输出中,你可以看到小写字母c出现了三次,空格字符出现了 13 次,大写字母A出现了一次。无论message变量中的字符串是什么,这个程序都会正常工作,即使字符串有数百万个字符长!
使用数据结构模拟现实世界事物
即使在互联网出现之前,人们也可以与世界另一端的人下棋。每位玩家都会在家设置一个棋盘,然后他们轮流通过邮寄明信片向对方描述自己的走法。为此,玩家需要一种明确描述棋盘状态和走法的方法。
在代数棋盘坐标中,棋盘上的方格由数字和字母坐标标识,如图 7-1 所示。

图 7-1:代数棋盘坐标
棋子使用字母:K代表国王,Q代表王后,R代表车,B代表象,N代表马。描述一个走法需要指定棋子的字母和其目的地的坐标。一对这样的走法描述了单次回合(白方先行)中发生的事情;例如,记号“2. Nf3 Nc6”表示在游戏的第二个回合中,白方将马移动到 f3,黑方将马移动到 c6。
代数记号法还有更多内容,但重点是你可以明确描述一局棋,而无需在棋盘前。你的对手甚至可能在世界另一端!实际上,如果你记忆力好,你甚至不需要物理棋盘:你只需阅读邮寄的棋步,并在你的想象中更新棋盘。
计算机有很好的记忆力。现代计算机上的程序可以轻松存储数十亿个字符串,例如'2. Nf3 Nc6'。这就是计算机可以在没有物理棋盘的情况下下棋的原因。它们通过模型来表示棋盘,你可以编写代码来处理这个模型以模拟棋局。
这就是列表和字典能派上用场的地方。例如,我们可以想出我们自己的标记法,使得 Python 字典{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}可以代表图 7-2 中的棋盘。

图 7-2:由字典{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}模拟的棋盘
让我们使用这个数据结构方案来创建我们自己的交互式棋盘程序。
项目 1:交互式棋盘模拟器
即使是最早的计算机也比任何人类计算得快得多,但当时人们认为棋是计算智能的真实展示。我们在这里不会创建自己的棋类程序。(那需要一本书!)但我们可以使用我们迄今为止讨论的内容创建一个交互式棋盘程序。
对于这个程序,你不需要了解棋的规则。只需知道棋是在一个 8×8 的棋盘上进行的,黑白棋子被称为兵、马、象、车、后和王。棋盘的左上角和右下角应该是白色,我们的程序假设输出窗口的背景是黑色(与纸张书籍的白色背景不同)。我们的棋盘程序只是一个带有棋子的棋盘;它甚至不强制执行棋子的移动规则。我们将使用文本字符来“绘制”棋盘,例如图 7-3 中所示。

图 7-3:棋盘程序的基于文本的非图形输出
图形会很好,并且会使棋子更容易识别,但我们已经捕获了关于棋子的所有信息,即使没有它们。这种基于文本的方法允许我们仅使用 print() 函数编写程序,并且不需要为我们的程序安装任何类型的图形库,如 Pygame(在我的书《用 Python 发明你自己的计算机游戏》中讨论过[No Starch Press, 2016])。
首先,我们需要设计一个数据结构来表示棋盘及其上任何可能的棋子配置。前一个章节的例子是有效的:棋盘是一个 Python 字典,具有字符串键 'a1' 到 'h8' 来表示棋盘上的方格。请注意,这些字符串总是两个字符长。此外,字母总是小写,并且总是在数字之前。这种特定性很重要;我们将在代码中使用这些细节。
为了表示棋子,我们也将使用两个字符的字符串,其中第一个字母是 'w' 或 'b' 来表示白色或黑色,第二个字母是 'P'、'N'、'B'、'R'、'Q' 或 'K' 来表示棋子的类型。图 7-4 显示了每个棋子及其字符串表示。

图 7-4:每个棋子的两字符字符串表示
Python 字典的键标识棋盘的方格,值标识该方格上的棋子。字典中不存在键表示空方格。字典非常适合存储此类信息:字典中的键只能使用一次,而棋盘上的方格一次只能有一个棋子。
第 1 步:设置程序
程序的前一部分导入了 sys 模块以使用其 exit() 函数,以及 copy 模块以使用其 copy() 函数。游戏开始时,白方和黑方各有 16 个棋子。STARTING_PIECES 常量将包含一个棋盘字典,其中所有正确的棋子都位于它们正确的起始位置:
import sys, copy
STARTING_PIECES = {'a8': 'bR', 'b8': 'bN', 'c8': 'bB', 'd8': 'bQ',
'e8': 'bK', 'f8': 'bB', 'g8': 'bN', 'h8': 'bR', 'a7': 'bP', 'b7': 'bP',
'c7': 'bP', 'd7': 'bP', 'e7': 'bP', 'f7': 'bP', 'g7': 'bP', 'h7': 'bP',
'a1': 'wR', 'b1': 'wN', 'c1': 'wB', 'd1': 'wQ', 'e1': 'wK', 'f1': 'wB',
'g1': 'wN', 'h1': 'wR', 'a2': 'wP', 'b2': 'wP', 'c2': 'wP', 'd2': 'wP',
'e2': 'wP', 'f2': 'wP', 'g2': 'wP', 'h2': 'wP'}
(这段代码有点难打。你可以从 autbor.com/3/chessboard.py) 复制并粘贴。)每当程序需要将棋盘重置为初始设置时,它可以使用 copy.copy() 函数复制 STARTING_PIECES。
第 2 步:创建棋盘模板
BOARD_TEMPLATE 变量将包含一个字符串,用作棋盘的模板。程序可以在打印之前将其中的单个棋子字符串插入其中。通过使用连续的三个双引号,我们可以创建跨越多行代码的 多行字符串。多行字符串以另一个三个双引号结束。这种 Python 语法比使用 \n 转义字符将所有内容放在单行上更容易。你将在下一章中了解更多关于多行字符串的内容。
BOARD_TEMPLATE = """
a b c d e f g h
____ ____ ____ ____ ____ ____ ____ ____
|||||| |||||| |||||| |||||| |
8 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
7 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
6 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
5 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
4 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
3 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
2 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
1 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
"""
WHITE_SQUARE = '||'
BLACK_SQUARE = ' '
大括号对表示字符串中的位置,我们将在此处插入如 'wR' 或 'bQ' 的棋子字符串。如果该方格为空,程序将插入 WHITE_SQUARE 或 BLACK_SQUARE 字符串,我将在讨论 print_chessboard() 函数时详细解释。
第 3 步:打印当前棋盘
我们将定义一个 print_chessboard() 函数,它接受棋盘字典,然后在屏幕上打印出反映该棋盘上棋子的棋盘。我们将对 BOARD_TEMPLATE 字符串调用 format() 字符串方法,将一个字符串列表传递给该方法。format() 方法返回一个新的字符串,其中 BOARD_TEMPLATE 中的 {} 对应的字符串被传递的列表中的字符串替换。你将在下一章中了解更多关于 format() 的内容。
让我们看看 print_chessboard() 中的代码:
def print_chessboard(board):
squares = []
is_white_square = True
for y in '87654321':
for x in 'abcdefgh':
#print(x, y, is_white_square) # DEBUG: Show coordinates
棋盘上有 64 个方格,BOARD_TEMPLATE 字符串中有 64 个 {} 对。我们必须构建一个包含 64 个字符串的列表来替换这些 {} 对。我们将这个列表存储在 squares 变量中。列表中的字符串代表棋子,如 'wB' 和 'bQ',或者空方格。根据空方格是白色还是黑色,我们必须使用 WHITE_SQUARE 字符串 ('||') 或 BLACK_SQUARE 字符串 (' ')。我们将使用布尔值在 is_white_square 变量中跟踪哪些方格是白色的,哪些是黑色的。
两个嵌套的 for 循环将遍历棋盘上的所有 64 个方格,从左上角的方格开始,向右到左,然后从上到下。左上角的方格是一个白方格,因此我们将 is_white_square 设置为 True。记住,for 循环可以遍历由 range() 提供的整数、列表中的值或字符串中的单个字符。在这两个 for 循环中,y 和 x 变量分别取自字符串 '87654321' 和 'abcdefgh' 中的字符。要查看代码遍历方格的顺序(以及每个方格的颜色),在运行程序之前取消注释 print(x, y, is_white_square) 这行代码。
for 循环内部的代码构建了一个包含适当字符串的 squares 列表:
if x + y in board.keys():
squares.append(board[x + y])
else:
if is_white_square:
squares.append(WHITE_SQUARE)
else:
squares.append(BLACK_SQUARE)
is_white_square = not is_white_square
is_white_square = not is_white_square
print(BOARD_TEMPLATE.format(*squares))
x 和 y 循环变量中的字符串连接在一起形成一个两位数的方格字符串。例如,如果 x 是 'a',y 是 '8',那么 x + y 的结果为 'a8',而 x + y in board.keys() 检查这个字符串是否作为键存在于棋盘字典中。如果是,代码将那个方格的棋子字符串追加到 squares 列表的末尾。
如果不是,代码必须根据 is_white_square 中的值在 WHITE_SQUARE 或 BLACK_SQUARE 中追加空白方格字符串。一旦代码处理完这个棋盘方格,它将 is_white_square 的布尔值切换为其相反值(因为下一个方格的颜色将是相反的)。在完成最外层 for 循环的行之后,变量需要再次切换。
循环完成后,squares 列表包含 64 个字符串。然而,format() 字符串方法不接收单个列表参数,而是接收每个 {} 对应的一个字符串参数来替换。squares 旁边的星号 * 告诉 Python 将列表中的值作为单独的参数传递。这有点微妙,但想象一下,你有一个列表 spam = ['cat', 'dog', 'rat']。如果你调用 print(spam),Python 将打印列表值,包括其方括号、引号和逗号。然而,调用 print(*spam) 等同于调用 print('cat', 'dog', 'rat'),它简单地打印 cat dog rat。我称这种语法为 星号语法。
print_chessboard() 函数被编写为与用于表示棋盘的特定数据结构一起工作:一个 Python 字典,其键为方格字符串,如 'a8',值为棋子字符串,如 'bQ'。如果我们设计的数据结构不同,我们也必须以不同的方式编写我们的函数。print_chessboard() 打印出基于文本的棋盘表示,但如果我们使用 Pygame 这样的图形库来渲染棋盘,我们仍然可以使用这个 Python 字典来表示棋盘配置。
第 4 步:操作棋盘
现在我们已经有了一种在 Python 字典中表示棋盘的方法,以及一个基于该字典显示棋盘的函数,让我们编写代码通过操作字典的键和值来移动棋盘上的棋子。在 print_chessboard() 函数的 def 块之后,程序的主要部分显示文本说明如何使用交互式棋盘程序:
print('Interactive Chessboard')
print('by Al Sweigart al@inventwithpython.com')
print()
print('Pieces:')
print(' w - White, b - Black')
print(' P - Pawn, N - Knight, B - Bishop, R - Rook, Q - Queen, K - King')
print('Commands:')
print(' move e2 e4 - Moves the piece at e2 to e4')
print(' remove e2 - Removes the piece at e2')
print(' set e2 wP - Sets square e2 to a white pawn')
print(' reset - Resets pieces back to their starting squares')
print(' clear - Clears the entire board')
print(' fill wP - Fills entire board with white pawns.')
print(' quit - Quits the program')
程序可以通过更改棋盘字典来移动棋子、移除棋子、设置带有棋子的方格、重置棋盘和清除棋盘:
main_board = copy.copy(STARTING_PIECES)
while True:
print_chessboard(main_board)
response = input('> ').split()
首先,main_board 变量接收 STARTING_PIECES 字典的副本,这是一个包含所有棋子在标准起始位置的字典。执行进入一个无限循环,允许用户输入命令。例如,如果用户在调用 input() 之后输入 move e2 e4,split() 方法返回列表 ['move', 'e2', 'e4'],然后程序将这个列表存储在 response 变量中。response 列表中的第一个项目,response[0],将是用户想要执行的命令:
if response[0] == 'move':
main_board[response[2]] = main_board[response[1]]
del main_board[response[1]]
如果用户输入类似 move e2 e4 的内容,那么 response[0] 是 'move'。我们可以通过首先将 main_board 中旧方格(在 response[1] 中)的棋子复制到新方格(在 response[2] 中)来“移动”一个棋子。然后,我们可以删除 main_board 中旧方格的键值对。这会产生一种棋子已经移动的效果(尽管我们不会在再次调用 print_chessboard() 之前看到这个变化)。
我们的交互式棋盘模拟器不会检查这是否是一个有效的移动。它只是执行用户给出的命令。如果用户输入类似 remove e2 的内容,程序将 response 设置为 ['remove', 'e2']:
elif response[0] == 'remove':
del main_board[response[1]]
通过从 main_board 中删除键值对,键为 response[1],我们使棋子从棋盘上消失。如果用户输入类似 set e2 wP 的内容以在 e2 添加一个白兵,程序将 response 设置为 ['set', 'e2', 'wP']:
elif response[0] == 'set':
main_board[response[1]] = response[2]
我们可以在 main_board 中创建一个新的键值对,键为 response[1],值为 response[2],以将这个棋子添加到棋盘上。如果用户输入 reset,response 简单地是 ['reset'],我们可以通过将 STARTING_PIECES 字典复制到 main_board 来将棋盘设置为初始配置:
elif response[0] == 'reset':
main_board = copy.copy(STARTING_PIECES)
如果用户输入 clear,response 简单地是 ['clear'],我们可以通过将 main_board 设置为空字典来从棋盘上移除所有棋子:
elif response[0] == 'clear':
main_board = {}
如果用户输入 fill wP,response 将是 ['fill', 'wP'],我们将所有 64 个方格更改为字符串 'wP':
elif response[0] == 'fill':
for y in '87654321':
for x in 'abcdefgh':
main_board[x + y] = response[1]
嵌套的 for 循环将遍历每一个方格,将 x + y 键设置为 response[1]。在棋盘上放置 64 个白棋子并没有真正的理由,但这个命令展示了我们可以多么容易地按照我们的意愿操作棋盘数据结构。最后,用户可以通过输入 quit 来退出程序:
elif response[0] == 'quit':
sys.exit()
执行命令并修改 main_board 后,执行将跳回到 while 循环的开始,以显示更改后的棋盘并接受用户的新命令。
这个交互式棋盘程序不限制你可以放置或移动的棋子。它只是使用字典来表示棋盘上的棋子,并有一个函数用于在屏幕上以类似棋盘的方式显示这样的字典。我们可以通过设计数据结构和编写与这些数据结构一起工作的函数来模拟所有现实世界的对象或过程。如果你想看到另一个使用数据结构模拟游戏棋盘的例子,我的另一本书 The Big Book of Small Python Projects(No Starch Press,2021)中有一个可工作的井字棋程序。
嵌套字典和列表
当你模拟更复杂的事物时,你可能发现你需要包含其他字典和列表的字典和列表。列表对于持有有序值序列很有用,而字典对于将键与值关联很有用。例如,这里有一个使用字典来包含客人带到野餐的物品的字典的程序。total_brought() 函数可以读取这个数据结构并计算每种物品类型的总数。在新的程序中输入以下代码,并将其保存为 guestpicnic.py:
all_guests = {'Alice': {'apples': 5, 'pretzels': 12},
'Bob': {'ham sandwiches': 3, 'apples': 2},
'Carol': {'cups': 3, 'apple pies': 1}}
def total_brought(guests, item):
num_brought = 0
if name in birthdays: # ❷
if name in birthdays: # ❷
return num_brought
print('Number of things being brought:')
print(' - Apples ' + str(total_brought(all_guests, 'apples')))
print(' - Cups ' + str(total_brought(all_guests, 'cups')))
print(' - Cakes ' + str(total_brought(all_guests, 'cakes')))
print(' - Ham Sandwiches ' + str(total_brought(all_guests, 'ham sandwiches')))
print(' - Apple Pies ' + str(total_brought(all_guests, 'apple pies')))
在 total_brought() 函数内部,for 循环遍历 guests 中的键值对 ❶。在循环内部,客人的名字字符串被分配给 k,他们带来的野餐物品的字典被分配给 v。如果物品参数作为字典中的键存在,其值(数量)将被添加到 num_brought ❷。如果它不是作为键存在,get() 方法将返回 0 以添加到 num_brought。
这个程序的输出看起来像这样:
Number of things being brought:
- Apples 7
- Cups 3
- Cakes 0
- Ham Sandwiches 3
- Apple Pies 1
模拟带到野餐的物品的数量可能看起来如此简单,以至于你不需要编写程序来做这件事。但要知道,这个 total_brought() 函数可以轻松地处理包含数千名客人、每人带来数千种不同野餐物品的字典。在这种情况下,将这个信息存储在数据结构中,并使用 total_brought() 函数,可以为你节省大量时间!
你可以用你喜欢的任何方式用数据结构来模拟事物,只要你的程序中的其余代码可以正确地与数据模型一起工作。当你刚开始编程时,不必太担心“正确”的数据建模方式。随着经验的积累,你可能会有更有效的模型;重要的是数据模型要满足你程序的需求。
概述
你在本章中学到了所有关于字典的知识。列表和字典是包含多个值的值,包括其他列表和字典。字典很有用,因为你可以将一个项目(键)映射到另一个项目(值),而列表只是按顺序包含一系列值。代码可以使用方括号访问字典内的值,就像访问列表一样。与整数索引不同,字典可以有各种数据类型的键:整数、浮点数、字符串或元组。通过将程序的价值组织到数据结构中,你可以创建现实世界对象的表示,例如本章中模拟的棋盘。
练习问题
-
空字典的代码看起来是什么样子?
-
具有键
'foo'和值42的字典值看起来是什么样子? -
字典和列表的主要区别是什么?
-
如果
spam是{'bar': 100},尝试访问spam['foo']会发生什么? -
如果字典存储在
spam中,表达式'cat' in spam和'cat' in spam.keys()之间有什么区别? -
如果字典存储在
spam中,表达式'cat' in spam和'cat' in spam.values()之间有什么区别? -
以下代码的快捷方式是什么?
if 'color' not in spam:
spam['color'] = 'black'
- 可以使用哪个模块和函数来“美化打印”字典值?
练习程序
为了练习,编写程序来完成以下任务。
国际象棋字典验证器
在本章中,我们使用了字典值 {'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'} 来表示棋盘。编写一个名为 isValidChessBoard() 的函数,该函数接受一个字典参数,并根据棋盘是否有效返回 True 或 False。
一个有效的棋盘将恰好有一个黑王和一个白王。每位玩家最多可以有 16 个棋子,其中只有 8 个可以是被子,所有棋子都必须位于从 '1a' 到 '8h' 的有效方格上。也就是说,棋子不能位于方格 '9z' 上。棋子名称应以 'w' 或 'b' 开头,分别代表白棋或黑棋,后面跟 'pawn'、'knight'、'bishop'、'rook'、'queen' 或 'king'。此函数应检测是否有错误导致棋盘不正确。(这不是要求列表的详尽列表,但对于这个练习来说已经足够接近了。)
奇幻游戏库存
假设你正在创建一个中世纪幻想电子游戏。用于表示玩家库存的数据结构是一个字典,其键是描述库存物品的字符串,其值是整数,详细说明了玩家有多少个该物品。例如,字典值 {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} 表示玩家有一个绳索,六个火把,42 个金币,等等。
编写一个名为 display_inventory() 的函数,该函数将接受任何可能的“库存”并按以下方式显示:
Inventory:
12 arrow
42 gold coin
1 rope
6 torch
1 dagger
Total number of items: 62
提示:你可以使用一个 for 循环来遍历字典中的所有键。
stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}
def display_inventory(inventory):
print("Inventory:")
item_total = 0
for k, v in inventory.items():
# FILL THIS PART IN
print("Total number of items: " + str(item_total))
display_inventory(stuff)
列表到字典财宝转换
想象一下,同一个幻想电子游戏将被打败的龙财宝表示为一个字符串列表,如下所示:
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
编写一个名为 add_to_inventory(inventory, added_items) 的函数。inventory 参数是一个表示玩家库存的字典(如前一个项目所示),而 added_items 参数是一个列表,例如 dragon_loot。add_to_inventory() 函数应返回一个表示玩家更新后的库存的字典。注意,added_items 列表可以包含相同物品的多个实例。你的代码可能看起来像这样:
def add_to_inventory(inventory, added_items):
# Your code goes here.
inv = {'gold coin': 42, 'rope': 1}
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
inv = add_to_inventory(inv, dragon_loot)
display_inventory(inv)
前一个程序(包含你从前一个项目中继承的 display_inventory() 函数)将输出以下内容:
Inventory:
45 gold coin
1 rope
1 ruby
1 dagger
Total number of items: 48
字典数据类型
就像列表一样,一个 字典 是许多值的可变集合。但与列表的索引不同,字典的索引可以使用许多不同的数据类型,而不仅仅是整数。这些字典索引被称为 键,一个键及其关联的值被称为 键值对。
在代码中,字典是用花括号 ({}) 定义的。将以下内容输入到交互式外壳中:
>>> my_cat = {'size': 'fat', 'color': 'gray', 'age': 17}
这将一个字典赋值给 my_cat 变量。这个字典的键是 'size'、'color' 和 'age'。这些键的值分别是 'fat'、'gray' 和 17。你可以通过它们的键来访问这些值:
>>> my_cat['size']
'fat'
>>> 'My cat has ' + my_cat['color'] + ' fur.'
'My cat has gray fur.'
使用字典,你可以在单个变量中存储有关同一事物的多个数据。这个 my_cat 变量包含描述我的猫的三个不同的字符串,我可以用它作为函数调用的参数或返回值,从而避免需要创建三个单独的变量。
字典仍然可以使用整数作为键,就像列表使用整数作为索引一样,但它们不必从 0 开始,可以是任何数字:
>>> spam = {12345: 'Luggage Combination', 42: 'The Answer'}
>>> spam[12345]
'Luggage Combination'
>>> spam[42]
'The Answer'
>>> spam[0]
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
KeyError: 0
字典有键,而不是索引。在这个例子中,虽然 spam 中的字典有整数键 12345 和 42,但它没有像列表那样的索引 0 到 41。
比较字典和列表
与列表不同,字典中的项是无序的。列表 spam 中的第一个项将是 spam[0]。但在字典中没有“第一个”项。虽然项的顺序对于确定两个列表是否相同很重要,但你可以在任何顺序中输入字典的键值对。在交互式外壳中输入以下内容:
>>> spam = ['cats', 'dogs', 'moose']
>>> bacon = ['dogs', 'moose', 'cats']
>>> spam == bacon # The order of list items matters.
False
>>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'}
>>> ham = {'species': 'cat', 'age': '8', 'name': 'Zophie'}
>>> eggs == ham # The order of dictionary key-value pairs doesn't matter.
True
eggs 和 ham 字典即使我们以不同的顺序输入了它们的键值对,它们的值也是相同的。因为字典是无序的,它不是一个序列数据类型,不能像列表那样进行切片。
尝试访问一个在字典中不存在的键将导致 KeyError 错误消息,就像列表的“越界”IndexError 错误消息一样。在交互式外壳中输入以下内容,并注意显示的错误消息,因为不存在 'color' 键:
>>> spam = {'name': 'Zophie', 'age': 7}
>>> spam['color']
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
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: # ❷
if name in birthdays: # ❷
else:
print('I do not have birthday information for ' + name)
print('What is their birthday?')
bday = input()
if name in birthdays: # ❷
print('Birthday database updated.')
代码创建了一个初始字典并将其存储在 birthdays ❶ 中。你可以使用 in 关键字检查输入的名字是否作为键存在于字典中,就像你为列表做的那样。如果名字在字典中,你可以使用方括号访问相关的值 ❸;如果不在,你可以使用相同的方括号语法结合赋值运算符添加它 ❹。
当你运行这个程序时,它看起来会是这样:
Enter a name: (blank to quit)
Alice
Apr 1 is the birthday of Alice
Enter a name: (blank to quit)
Eve
I do not have birthday information for Eve
What is their birthday?
Dec 5
Birthday database updated.
Enter a name: (blank to quit)
Eve
Dec 5 is the birthday of Eve
Enter a name: (blank to quit)
当然,你在这个程序中输入的所有数据在程序终止时都会被遗忘。你将在第十章学习如何将数据保存到硬盘上的文件中。
返回键和值
三个字典方法将返回字典的键、值或键值对的类似列表的值:keys()、values() 和 items()。这些方法返回的值不是真正的列表:它们不能被修改,也没有 append() 方法。但这些数据类型(分别是 dict_keys、dict_values 和 dict_items)可以用于 for 循环。要查看这些方法的工作方式,请在交互式外壳中输入以下内容:
>>> spam = {'color': 'red', 'age': 42}
>>> for v in spam.values():
... print(v)
red
42
在这里,一个 for 循环遍历 spam 字典中的每个值。一个 for 循环也可以遍历键或键和值:
>>> for k in spam.keys():
... print(k)
color
age
>>> 'color' in spam.keys()
True
>>> 'age' not in spam.keys()
False
>>> 'red' in spam.values()
True
>>> for i in spam.items():
... print(i)
('color', 'red')
('age', 42)
当你使用 keys()、values() 和 items() 方法时,一个 for 循环可以分别遍历字典中的键、值或键值对。你可以使用 in 和 not in 操作符来确定值是否作为键或值存在于字典中。注意,items() 方法返回的 dict_items 值是键和值的元组。
你还可以使用in和not in运算符与字典值本身一起检查键的存在。这与使用keys()方法使用这些运算符是等效的:
>>> 'color' in spam
True
>>> 'color' in spam.keys()
True
如果你想要从这些方法中获取实际的列表,请将类似列表的返回值传递给list()函数。在交互式外壳中输入以下内容:
>>> spam = {'color': 'red', 'age': 42}
>>> spam.keys() # Returns a list-like dict_keys value
dict_keys(['color', 'age'])
>>> list(spam.keys()) # Returns an actual list value
['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: ' + str(k) + ' Value: ' + str(v))
Key: color Value: red
Key: age Value: 42
此代码创建了一个包含键'color'和'age'的字典,其值分别为'red'和42。for循环遍历items()方法返回的元组:('color', 'red')和('age', 42)。这两个变量k和v分别从这些元组中分配第一个(键)和第二个(值)值。循环体打印出每个键值对的k和v变量。
虽然你可以为键使用许多值,但你不能在字典中使用列表或字典作为键。这些数据类型是不可哈希的,这是本书范围之外的概念。如果你需要一个列表作为字典键,请使用元组代替。
检查键是否存在
在访问字典中键的值之前检查键的存在可能会很麻烦。幸运的是,字典有一个get()方法,它接受两个参数:要检索的值的键和一个回退值,如果该键不存在则返回该回退值。
在交互式外壳中输入以下内容:
>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items.get('cups', 0)) + ' cups.'
'I am bringing 2 cups.'
>>> 'I am bringing ' + str(picnic_items.get('eggs', 0)) + ' eggs.'
'I am bringing 0 eggs.'
因为picnic_items字典中没有'eggs'键,所以get()方法返回默认值0。如果不使用get(),代码将导致错误消息,如下例所示:
>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
KeyError: 'eggs'
在访问某个键的值之前检查该键的存在可以防止你的程序因错误消息而崩溃。
设置默认值
你通常会必须只为某个键设置值,如果该键尚未有值。你的代码可能看起来像这样:
>>> spam = {'name': 'Pooka', 'age': 5}
>>> if 'color' not in spam:
... spam['color'] = 'black'
...
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
setdefault()方法提供了一种在单行代码中完成此操作的方法。传递给该方法的第一个参数是要检查的键,第二个参数是在该键不存在时设置的值。如果键已存在,setdefault()方法返回键的值。在交互式外壳中输入以下内容:
>>> spam = {'name': 'Pooka', 'age': 5}
>>> spam.setdefault('color', 'black') # Sets 'color' key to 'black'
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
>>> spam.setdefault('color', 'white') # Does nothing
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
第一次调用setdefault()时,spam中的字典变为{'name': 'Pooka', 'age': 5, 'color': 'black'}。该方法返回值'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] + 1 ❷
print(count)
程序遍历 message 变量字符串中的每个字符,计算每个字符出现的频率。setdefault() 方法调用❶确保键存在于 count 字典中(默认值为 0),这样在执行 count[character] = count[character] + 1 时程序不会抛出 KeyError 错误。当你运行这个程序时,输出将如下所示:
{'I': 1, 't': 6, ' ': 13, 'w': 2, 'a': 4, 's': 3, 'b': 1, 'r': 5, 'i': 6,
'g': 2, 'h': 3, 'c': 3, 'o': 2, 'l': 3, 'd': 3, 'y': 1, 'n': 4, 'A': 1,
'p': 1, ',': 1, 'e': 5, 'k': 2, '.': 1}
从输出中,你可以看到小写字母 c 出现了三次,空格字符出现了 13 次,大写字母 A 出现了一次。无论 message 变量中的字符串是什么,这个程序都会工作,即使字符串有数百万个字符长!
比较字典和列表
与列表不同,字典中的项是无序的。列表 spam 中的第一个项将是 spam[0]。但在字典中没有任何“第一个”项。虽然项的顺序对于确定两个列表是否相同很重要,但你可以在任何顺序中输入字典的键值对。在交互式外壳中输入以下内容:
>>> spam = ['cats', 'dogs', 'moose']
>>> bacon = ['dogs', 'moose', 'cats']
>>> spam == bacon # The order of list items matters.
False
>>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'}
>>> ham = {'species': 'cat', 'age': '8', 'name': 'Zophie'}
>>> eggs == ham # The order of dictionary key-value pairs doesn't matter.
True
eggs 和 ham 字典即使我们以不同的顺序输入它们的键值对,也是相同的值。因为字典是无序的,它不是一个序列数据类型,不能像列表那样切片。
尝试访问字典中不存在的键将导致 KeyError 错误消息,就像列表的“越界”IndexError 错误消息一样。在交互式外壳中输入以下内容,并注意由于没有 'color' 键而出现的错误消息:
>>> spam = {'name': 'Zophie', 'age': 7}
>>> spam['color']
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
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: # ❷
if name in birthdays: # ❷
else:
print('I do not have birthday information for ' + name)
print('What is their birthday?')
bday = input()
if name in birthdays: # ❷
print('Birthday database updated.')
代码创建了一个初始字典并将其存储在 birthdays❶中。你可以使用 in 关键字检查输入的名称是否作为键存在于字典中,就像你为列表做的那样。如果名称在字典中,你可以使用方括号访问关联的值❷;如果不在这个字典中,你可以使用相同的方括号语法结合赋值运算符❹来添加它。
当你运行这个程序时,它看起来会是这样:
Enter a name: (blank to quit)
Alice
Apr 1 is the birthday of Alice
Enter a name: (blank to quit)
Eve
I do not have birthday information for Eve
What is their birthday?
Dec 5
Birthday database updated.
Enter a name: (blank to quit)
Eve
Dec 5 is the birthday of Eve
Enter a name: (blank to quit)
当然,你在这个程序中输入的所有数据在程序终止时都会被遗忘。你将在第十章学习如何将数据保存到硬盘上的文件中。
返回键和值
三个字典方法将返回字典的键、值或键值对的类似列表的值:keys()、values()和items()。这些方法返回的值不是真正的列表:它们不能被修改,也没有append()方法。但这些数据类型(分别是dict_keys、dict_values和dict_items)可以在for循环中使用。要查看这些方法的工作原理,请在交互式外壳中输入以下内容:
>>> spam = {'color': 'red', 'age': 42}
>>> for v in spam.values():
... print(v)
red
42
在这里,一个for循环遍历spam字典中的每个值。for循环也可以遍历键或键和值:
>>> for k in spam.keys():
... print(k)
color
age
>>> 'color' in spam.keys()
True
>>> 'age' not in spam.keys()
False
>>> 'red' in spam.values()
True
>>> for i in spam.items():
... print(i)
('color', 'red')
('age', 42)
当你使用keys()、values()和items()方法时,for循环可以分别遍历字典中的键、值或键值对,你可以使用in和not in运算符来确定值是否作为键或值存在于字典中。请注意,items()方法返回的dict_items值中的值是键和值的元组。
你还可以使用in和not in运算符与字典值本身来检查键的存在。这相当于使用这些运算符与keys()方法:
>>> 'color' in spam
True
>>> 'color' in spam.keys()
True
如果你想要从这些方法中获取实际的列表,请将它们的类似列表的返回值传递给list()函数。在交互式外壳中输入以下内容:
>>> spam = {'color': 'red', 'age': 42}
>>> spam.keys() # Returns a list-like dict_keys value
dict_keys(['color', 'age'])
>>> list(spam.keys()) # Returns an actual list value
['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: ' + str(k) + ' Value: ' + str(v))
Key: color Value: red
Key: age Value: 42
此代码创建了一个包含键'color'和'age'的字典,其值分别为'red'和42。for循环遍历items()方法返回的元组:('color', 'red')和('age', 42)。两个变量k和v分别从这些元组中分配第一个(键)和第二个(值)的值。循环体打印出每个键值对的k和v变量。
虽然你可以为键使用许多值,但你不能使用列表或字典作为字典的键。这些数据类型是不可哈希的,这是一个超出本书范围的概念。如果你需要一个列表作为字典键,请使用元组代替。
检查键是否存在
在访问键的值之前检查字典中是否存在该键可能会很麻烦。幸运的是,字典有一个get()方法,它接受两个参数:要检索的值的键和一个回退值,如果该键不存在则返回。
在交互式外壳中输入以下内容:
>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items.get('cups', 0)) + ' cups.'
'I am bringing 2 cups.'
>>> 'I am bringing ' + str(picnic_items.get('eggs', 0)) + ' eggs.'
'I am bringing 0 eggs.'
因为picnic_items字典中没有'eggs'键,所以get()方法返回默认值0。如果不使用get(),代码将导致错误消息,如下面的示例所示:
>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
KeyError: 'eggs'
在访问键的值之前检查键的存在可以防止程序因错误信息而崩溃。
设置默认值
你经常需要仅在某个键尚未具有值时在该字典中为该键设置一个值。你的代码可能看起来像这样:
>>> spam = {'name': 'Pooka', 'age': 5}
>>> if 'color' not in spam:
... spam['color'] = 'black'
...
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
setdefault()方法提供了一种在单行代码中完成此操作的方法。传递给方法的第一个参数是要检查的键,第二个参数是在该键不存在时设置的值。如果键已存在,setdefault()方法返回键的值。在交互式外壳中输入以下内容:
>>> spam = {'name': 'Pooka', 'age': 5}
>>> spam.setdefault('color', 'black') # Sets 'color' key to 'black'
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
>>> spam.setdefault('color', 'white') # Does nothing
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
第一次调用setdefault()时,spam字典变为{'name': 'Pooka', 'age': 5, 'color': 'black'}。该方法返回值'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] + 1 ❷
print(count)
程序遍历message变量字符串中的每个字符,计算每个字符出现的次数。❶ setdefault()方法调用确保键存在于count字典中(默认值为0),这样在执行count[character] = count[character] + 1时程序不会抛出KeyError错误。当你运行这个程序时,输出将看起来像这样:
{'I': 1, 't': 6, ' ': 13, 'w': 2, 'a': 4, 's': 3, 'b': 1, 'r': 5, 'i': 6,
'g': 2, 'h': 3, 'c': 3, 'o': 2, 'l': 3, 'd': 3, 'y': 1, 'n': 4, 'A': 1,
'p': 1, ',': 1, 'e': 5, 'k': 2, '.': 1}
从输出中,你可以看到小写字母c出现了三次,空格字符出现了 13 次,大写字母A出现了一次。无论message变量内部是什么字符串,即使字符串有数百万个字符长,这个程序都会正常工作!
使用数据结构模拟现实世界的事物
即使在互联网出现之前,人们也可以与世界另一端的人下棋。每位玩家都会在自己的家中设置一个棋盘,然后他们轮流通过邮寄明信片向对方描述自己的走法。为此,玩家需要一种方法来明确描述棋盘的状态和他们的走法。
在代数象棋记法中,棋盘上的方格通过数字和字母坐标来识别,如图 7-1 所示。

图 7-1:代数象棋记法中棋盘的坐标
棋子使用字母:K代表王,Q代表后,R代表车,B代表象,N代表马。描述一个移动需要指定棋子的字母和其目的地的坐标。一对这样的移动描述了单次回合中发生的事情(白方先行);例如,记号“2. Nf3 Nc6”表示在游戏的第二个回合中,白方移动了一个马到 f3,黑方移动了一个马到 c6。
代数记法比这还要复杂一些,但重点是你可以明确地描述一盘棋,而无需在棋盘前。你的对手甚至可能在世界另一端!实际上,如果你记忆力好,你甚至不需要一个物理棋盘:你只需阅读邮寄的棋步,并在你的想象中更新棋盘。
计算机有很好的记忆力。现代计算机上的程序可以轻松存储数十亿个字符串,例如'2. Nf3 Nc6'。这就是计算机可以在没有物理棋盘的情况下下棋的原因。它们通过模型来表示棋盘,你可以编写代码来处理这个模型以模拟棋局。
这就是列表和字典能派上用场的地方。例如,我们可以想出我们自己的符号,这样 Python 字典{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}就能代表图 7-2 中的棋盘。

图 7-2:由字典{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}表示的棋盘
让我们使用这个数据结构方案来创建我们自己的交互式棋盘程序。
项目 1:交互式棋盘模拟器
即使是最早的计算机也能比任何人类更快地完成计算,但当时人们认为下棋是计算智能的真实展示。在这里我们不会创建自己的下棋程序。(那需要一本书来介绍!)但我们可以用我们到目前为止讨论的内容创建一个交互式棋盘程序。
你不需要了解棋的规则来运行这个程序。只需知道棋是在一个 8×8 的棋盘上进行的,有黑白棋子,称为兵、马、象、车、后和王。棋盘的左上角和右下角应该是白色,我们的程序假设输出窗口的背景是黑色(与纸张书籍的白色背景不同)。我们的棋盘程序只是一个带有棋子的棋盘;它甚至不强制执行棋子的移动规则。我们将使用文本字符来“绘制”棋盘,如图 7-3 所示。

图 7-3:棋盘程序的基于文本的非图形输出
图形会很棒,会使棋子更容易识别,但我们已经捕获了关于棋子的所有信息,而无需它们。这种基于文本的方法允许我们仅使用print()函数编写程序,并且不需要安装任何类型的图形库,如 Pygame(在我的书《用 Python 自己发明电脑游戏》[No Starch Press,2016]中讨论过],用于我们的程序。
首先,我们需要设计一种数据结构,可以表示棋盘及其上任何可能的棋子配置。上一节中的示例有效:棋盘是一个 Python 字典,具有字符串键'a1'到'h8'来表示棋盘上的方格。请注意,这些字符串总是两个字符长。此外,字母始终是小写,并且总是在数字之前。这种特定性很重要;我们将在代码中使用这些细节。
为了表示棋子,我们还将使用双字符字符串,其中第一个字母是表示白方的'w'或表示黑方的'b'的小写字母,第二个字母是表示棋子类型的'P'、'N'、'B'、'R'、'Q'或'K'的大写字母。图 7-4 显示了每个棋子及其字符串表示。

图 7-4:每个棋子的双字符字符串表示
Python 字典的键标识棋盘上的方格,值标识该方格上的棋子。字典中不存在键表示空方格。字典非常适合存储此类信息:字典中的键只能使用一次,并且棋盘上的方格一次只能有一个棋子。
第 1 步:设置程序
程序的第一部分导入sys模块以使用其exit()函数和copy模块以使用其copy()函数。游戏开始时,白方和黑方各有 16 枚棋子。STARTING_PIECES常量将保存一个棋盘字典,其中包含所有正确的棋子在其正确的初始位置:
import sys, copy
STARTING_PIECES = {'a8': 'bR', 'b8': 'bN', 'c8': 'bB', 'd8': 'bQ',
'e8': 'bK', 'f8': 'bB', 'g8': 'bN', 'h8': 'bR', 'a7': 'bP', 'b7': 'bP',
'c7': 'bP', 'd7': 'bP', 'e7': 'bP', 'f7': 'bP', 'g7': 'bP', 'h7': 'bP',
'a1': 'wR', 'b1': 'wN', 'c1': 'wB', 'd1': 'wQ', 'e1': 'wK', 'f1': 'wB',
'g1': 'wN', 'h1': 'wR', 'a2': 'wP', 'b2': 'wP', 'c2': 'wP', 'd2': 'wP',
'e2': 'wP', 'f2': 'wP', 'g2': 'wP', 'h2': 'wP'}
(这段代码有点难打。你可以从 autbor.com/3/chessboard.py) 复制并粘贴。)每当程序需要将棋盘重置为初始设置时,它可以使用 copy.copy() 函数复制 STARTING_PIECES。
第 2 步:创建棋盘模板
BOARD_TEMPLATE 变量将包含一个字符串,该字符串充当棋盘的模板。程序可以在打印之前将其中的单个棋子字符串插入其中。通过使用连续的三个双引号,我们可以创建跨越多行代码的 多行字符串。多行字符串以另一个三个双引号结束。这种 Python 语法比使用 \n 转义字符将所有内容放在单行上更容易。你将在下一章中了解更多关于多行字符串的内容。
BOARD_TEMPLATE = """
a b c d e f g h
____ ____ ____ ____ ____ ____ ____ ____
|||||| |||||| |||||| |||||| |
8 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
7 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
6 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
5 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
4 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
3 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
2 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
1 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
"""
WHITE_SQUARE = '||'
BLACK_SQUARE = ' '
一对大括号表示字符串中的位置,我们将在此处插入如 'wR' 或 'bQ' 的棋子字符串。如果方格为空,程序将插入 WHITE_SQUARE 或 BLACK_SQUARE 字符串,我将在讨论 print_chessboard() 函数时详细介绍。
第 3 步:打印当前棋盘
我们将定义一个 print_chessboard() 函数,该函数接受棋盘字典,然后在屏幕上打印出反映该棋盘上棋子的棋盘。我们将对 BOARD_TEMPLATE 字符串调用 format() 字符串方法,将方法传递一个字符串列表。format() 方法返回一个新的字符串,其中 BOARD_TEMPLATE 中的 {} 对被传递的列表中的字符串替换。你将在下一章中了解更多关于 format() 的内容。
让我们看看 print_chessboard() 中的代码:
def print_chessboard(board):
squares = []
is_white_square = True
for y in '87654321':
for x in 'abcdefgh':
#print(x, y, is_white_square) # DEBUG: Show coordinates
棋盘上有 64 个方格,BOARD_TEMPLATE 字符串中有 64 个 {} 对。我们必须构建一个包含 64 个字符串的列表来替换这些 {} 对。我们将此列表存储在 squares 变量中。列表中的字符串表示棋子,如 'wB' 和 'bQ',或空方格。根据空方格是白色还是黑色,我们必须使用 WHITE_SQUARE 字符串 ('||') 或 BLACK_SQUARE 字符串 (' ')。我们将使用 is_white_square 变量中的布尔值来跟踪哪些方格是白色的,哪些是黑色的。
两个嵌套的 for 循环将遍历棋盘上的所有 64 个方格,从左上角开始,向右到左,然后从上到下。左上角的方格是白色方格,因此我们将 is_white_square 设置为 True。记住,for 循环可以遍历由 range() 提供的整数、列表中的值或字符串中的单个字符。在这两个 for 循环中,y 和 x 变量分别取自字符串 '87654321' 和 'abcdefgh'。要查看代码遍历方格的顺序(以及每个方格的颜色),在运行程序之前取消注释 print(x, y, is_white_square) 代码行。
for循环内部的代码通过适当的字符串构建 squares列表:
if x + y in board.keys():
squares.append(board[x + y])
else:
if is_white_square:
squares.append(WHITE_SQUARE)
else:
squares.append(BLACK_SQUARE)
is_white_square = not is_white_square
is_white_square = not is_white_square
print(BOARD_TEMPLATE.format(*squares))
x和y循环变量的字符串连接在一起形成一个两位数的方格字符串。例如,如果x是'a',y是'8',那么x + y的结果是'a8',而x + y in board.keys()检查这个字符串是否作为键存在于棋盘字典中。如果是,代码将这个方格的棋子字符串追加到 squares列表的末尾。
如果不是这样,代码必须在WHITE_SQUARE或BLACK_SQUARE中追加空方括号字符串,具体取决于is_white_square中的值。一旦代码完成对这个棋盘方格的处理,它就会将is_white_square中的布尔值切换到其相反值(因为下一个方格的颜色将是相反的)。在完成最外层for循环的最后一行后,需要再次切换这个变量。
循环结束后,squares列表包含 64 个字符串。然而,format()字符串方法不接收单个列表参数,而是接收每个{}对的一个字符串参数来替换。squares旁边的星号*告诉 Python 将这个列表中的值作为单独的参数传递。这有点微妙,但想象一下,你有一个列表spam = ['cat', 'dog', 'rat']。如果你调用print(spam),Python 会打印出列表值,包括其方括号、引号和逗号。然而,调用print(*spam)等同于调用print('cat', 'dog', 'rat'),它简单地打印出cat dog rat。我称这种语法为星号语法。
print_chessboard()函数被编写来与用于表示棋盘的特定数据结构一起工作:一个 Python 字典,其键是方格字符串,如'a8',值是棋子字符串,如'bQ'。如果我们设计的数据结构不同,我们也必须以不同的方式编写我们的函数。print_chessboard()打印出基于该字典的文本表示的棋盘,但如果我们使用像 Pygame 这样的图形库来渲染棋盘,我们仍然可以使用这个 Python 字典来表示棋盘配置。
第 4 步:操作棋盘
现在我们有了在 Python 字典中表示棋盘的方法以及根据该字典显示棋盘的函数,让我们编写代码通过操作字典的键和值来在棋盘上移动棋子。在print_chessboard()函数的def块之后,程序的主要部分显示文本说明如何使用交互式棋盘程序:
print('Interactive Chessboard')
print('by Al Sweigart al@inventwithpython.com')
print()
print('Pieces:')
print(' w - White, b - Black')
print(' P - Pawn, N - Knight, B - Bishop, R - Rook, Q - Queen, K - King')
print('Commands:')
print(' move e2 e4 - Moves the piece at e2 to e4')
print(' remove e2 - Removes the piece at e2')
print(' set e2 wP - Sets square e2 to a white pawn')
print(' reset - Resets pieces back to their starting squares')
print(' clear - Clears the entire board')
print(' fill wP - Fills entire board with white pawns.')
print(' quit - Quits the program')
程序可以通过改变棋盘字典来移动棋子、移除棋子、设置带有棋子的方格、重置棋盘和清除棋盘:
main_board = copy.copy(STARTING_PIECES)
while True:
print_chessboard(main_board)
response = input('> ').split()
首先,main_board变量接收STARTING_PIECES字典的一个副本,这是一个包含所有棋子在标准起始位置的字典。执行进入一个无限循环,允许用户输入命令。例如,如果用户在调用input()后输入move e2 e4,split()方法将返回列表['move', 'e2', 'e4'],然后程序将这个列表存储在response变量中。response列表中的第一个项目,response[0],将是用户想要执行的命令:
if response[0] == 'move':
main_board[response[2]] = main_board[response[1]]
del main_board[response[1]]
如果用户输入类似move e2 e4的内容,那么response[0]将是'move'。我们可以通过首先将main_board中旧方格(在response[1]中)的棋子复制到新方格(在response[2]中)来“移动”一个棋子。然后,我们可以删除main_board中旧方格的键值对。这会产生一种效果,好像棋子已经移动了(尽管我们只有在再次调用print_chessboard()时才会看到这个变化)。
我们的交互式棋盘模拟器不会检查这个移动是否有效。它只是执行用户给出的命令。如果用户输入类似remove e2的内容,程序会将response设置为['remove', 'e2']:
elif response[0] == 'remove':
del main_board[response[1]]
通过从main_board中删除键response[1]的键值对,我们使棋子从棋盘上消失。如果用户输入类似set e2 wP的内容来在 e2 位置添加一个白棋子,程序将response设置为['set', 'e2', 'wP']:
elif response[0] == 'set':
main_board[response[1]] = response[2]
我们可以在main_board中创建一个新的键值对,键为response[1],值为response[2],以将这个棋子添加到棋盘上。如果用户输入 reset,response将简单地变为['reset'],我们可以通过将STARTING_PIECES字典复制到main_board来将棋盘设置为起始配置:
elif response[0] == 'reset':
main_board = copy.copy(STARTING_PIECES)
如果用户输入 clear,response将简单地变为['clear'],我们可以通过将main_board设置为空字典来移除棋盘上的所有棋子:
elif response[0] == 'clear':
main_board = {}
如果用户输入 fill wP,response将是['fill', 'wP'],我们将所有 64 个方格更改为字符串'wP':
elif response[0] == 'fill':
for y in '87654321':
for x in 'abcdefgh':
main_board[x + y] = response[1]
嵌套的for循环会遍历每一个方格,将x + y键设置为response[1]。在棋盘上放置 64 个白棋子并没有真正的理由,但这个命令展示了我们可以多么容易地按照我们的意愿操作棋盘数据结构。最后,用户可以通过输入 quit 来退出程序:
elif response[0] == 'quit':
sys.exit()
执行命令并修改main_board后,执行会跳回到while循环的起始位置,以显示更改后的棋盘并接受用户的新命令。
这个交互式棋盘程序不限制你可以放置或移动的棋子。它只是使用字典来表示棋盘上的棋子,并有一个函数用于在屏幕上以类似棋盘的方式显示这样的字典。我们可以通过设计数据结构和编写与这些数据结构一起工作的函数来模拟所有现实世界中的对象或过程。如果你想看另一个用数据结构模拟棋盘的例子,我的另一本书《Python 小项目大全书》(No Starch Press,2021 年)中有一个可用的井字棋程序。
第 1 步:设置程序
程序的第一部分导入了 sys 模块以使用其 exit() 函数,以及 copy 模块以使用其 copy() 函数。游戏开始时,白方和黑方各有 16 个棋子。STARTING_PIECES 常量将保存一个包含所有正确位置棋子的棋盘字典:
import sys, copy
STARTING_PIECES = {'a8': 'bR', 'b8': 'bN', 'c8': 'bB', 'd8': 'bQ',
'e8': 'bK', 'f8': 'bB', 'g8': 'bN', 'h8': 'bR', 'a7': 'bP', 'b7': 'bP',
'c7': 'bP', 'd7': 'bP', 'e7': 'bP', 'f7': 'bP', 'g7': 'bP', 'h7': 'bP',
'a1': 'wR', 'b1': 'wN', 'c1': 'wB', 'd1': 'wQ', 'e1': 'wK', 'f1': 'wB',
'g1': 'wN', 'h1': 'wR', 'a2': 'wP', 'b2': 'wP', 'c2': 'wP', 'd2': 'wP',
'e2': 'wP', 'f2': 'wP', 'g2': 'wP', 'h2': 'wP'}
(这段代码有点难打。你可以从 autbor.com/3/chessboard.py) 复制并粘贴。)每当程序需要将棋盘重置为起始设置时,它可以使用 copy.copy() 函数复制 STARTING_PIECES。
第 2 步:创建棋盘模板
BOARD_TEMPLATE 变量将包含一个作为棋盘模板的字符串。程序可以在打印之前将单个棋子字符串插入其中。通过使用连续的三个双引号,我们可以创建一个跨越多行代码的 多行字符串。多行字符串以另一个三个双引号结束。这种 Python 语法比尝试使用 \n 转义字符将所有内容放在一行上更容易。你将在下一章中了解更多关于多行字符串的内容。
BOARD_TEMPLATE = """
a b c d e f g h
____ ____ ____ ____ ____ ____ ____ ____
|||||| |||||| |||||| |||||| |
8 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
7 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
6 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
5 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
4 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
3 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
|||||| |||||| |||||| |||||| |
2 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
||||||____||||||____||||||____||||||____|
| |||||| |||||| |||||| ||||||
1 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
|____||||||____||||||____||||||____||||||
"""
WHITE_SQUARE = '||'
BLACK_SQUARE = ' '
一对大括号表示字符串中的位置,我们将在此处插入如 'wR' 或 'bQ' 的棋子字符串。如果方格为空,程序将插入 WHITE_SQUARE 或 BLACK_SQUARE 字符串,我将在讨论 print_chessboard() 函数时更详细地解释。
第 3 步:打印当前棋盘
我们将定义一个 print_chessboard() 函数,该函数接受棋盘字典,然后在屏幕上打印出反映该棋盘上棋子的棋盘。我们将对 BOARD_TEMPLATE 字符串调用 format() 字符串方法,传递一个字符串列表给该方法。format() 方法返回一个新的字符串,其中 BOARD_TEMPLATE 中的 {} 对被传递的列表中的字符串替换。你将在下一章中了解更多关于 format() 的内容。
让我们来看看 print_chessboard() 函数中的代码:
def print_chessboard(board):
squares = []
is_white_square = True
for y in '87654321':
for x in 'abcdefgh':
#print(x, y, is_white_square) # DEBUG: Show coordinates
棋盘上有 64 个方格,BOARD_TEMPLATE字符串中有 64 个{}对。我们必须构建一个包含 64 个字符串的列表来替换这些{}对。我们将这个列表存储在squares变量中。列表中的字符串代表棋子,如'wB'和'bQ',或者空白方格。根据空白方格是白色还是黑色,我们必须使用WHITE_SQUARE字符串('||')或BLACK_SQUARE字符串(' ')。我们将使用is_white_square变量中的布尔值来跟踪哪些方格是白色的,哪些是黑色的。
两个嵌套的for循环将遍历棋盘上的所有 64 个方格,从左上角的方格开始,向右到左,然后从上到下。左上角的方格是一个白色方格,因此我们将is_white_square初始化为True。记住,for循环可以遍历由range()提供的整数、列表中的值或字符串中的单个字符。在这两个for循环中,y和x变量分别取自字符串'87654321'和'abcdefgh'中的字符。为了查看代码遍历方格的顺序(以及每个方格的颜色),在运行程序之前取消注释print(x, y, is_white_square)代码行。
for循环内部的代码构建了包含适当字符串的squares列表:
if x + y in board.keys():
squares.append(board[x + y])
else:
if is_white_square:
squares.append(WHITE_SQUARE)
else:
squares.append(BLACK_SQUARE)
is_white_square = not is_white_square
is_white_square = not is_white_square
print(BOARD_TEMPLATE.format(*squares))
x和y循环变量中的字符串连接在一起形成一个两位数的方格字符串。例如,如果x是'a',y是'8',那么x + y的结果是'a8',而x + y in board.keys()检查这个字符串是否作为键存在于棋盘字典中。如果是,代码将把该方格的棋子字符串追加到squares列表的末尾。
如果不是这样,代码必须根据is_white_square中的值在WHITE_SQUARE或BLACK_SQUARE中追加空白方格字符串。一旦代码处理完这个棋盘方格,它将is_white_square的布尔值切换为其相反值(因为下一个方格的颜色将是相反的)。在完成最外层for循环的行之后,需要再次切换这个变量。
循环完成后,squares列表包含 64 个字符串。然而,format()字符串方法不接收单个列表参数,而是接收每个{}对的一个字符串参数来替换。列表旁边的星号*告诉 Python 将这个列表中的值作为单独的参数传递。这有点微妙,但想象一下你有一个列表spam = ['cat', 'dog', 'rat']。如果你调用print(spam),Python 将打印列表值,包括其方括号、引号和逗号。然而,调用print(*spam)等同于调用print('cat', 'dog', 'rat'),它简单地打印cat dog rat。我称这种语法为星号语法。
print_chessboard()函数被编写为与用于表示棋盘的特定数据结构一起工作:一个 Python 字典,其键是方格字符串,如'a8',值是棋子字符串,如'bQ'。如果我们设计的数据结构不同,我们也必须以不同的方式编写我们的函数。print_chessboard()打印出基于该字典的棋盘的文本表示,但如果我们使用像 Pygame 这样的图形库来渲染棋盘,我们仍然可以使用这个 Python 字典来表示棋盘配置。
第 4 步:操作棋盘
现在我们有了在 Python 字典中表示棋盘的方法,以及一个基于该字典显示棋盘的函数,让我们编写代码通过操作字典的键和值来移动棋盘上的棋子。在print_chessboard()函数的def块之后,程序的主要部分显示文本说明如何使用交互式棋盘程序:
print('Interactive Chessboard')
print('by Al Sweigart al@inventwithpython.com')
print()
print('Pieces:')
print(' w - White, b - Black')
print(' P - Pawn, N - Knight, B - Bishop, R - Rook, Q - Queen, K - King')
print('Commands:')
print(' move e2 e4 - Moves the piece at e2 to e4')
print(' remove e2 - Removes the piece at e2')
print(' set e2 wP - Sets square e2 to a white pawn')
print(' reset - Resets pieces back to their starting squares')
print(' clear - Clears the entire board')
print(' fill wP - Fills entire board with white pawns.')
print(' quit - Quits the program')
程序可以通过更改棋盘字典来移动棋子、移除棋子、设置带有棋子的方格、重置棋盘和清除棋盘:
main_board = copy.copy(STARTING_PIECES)
while True:
print_chessboard(main_board)
response = input('> ').split()
首先,main_board变量接收STARTING_PIECES字典的一个副本,这是一个包含所有棋子在标准起始位置的字典。执行进入一个无限循环,允许用户输入命令。例如,如果用户在调用input()后输入move e2 e4,则split()方法返回列表['move', 'e2', 'e4'],然后程序将其存储在response变量中。response列表中的第一个项目,response[0],将是用户想要执行的命令:
if response[0] == 'move':
main_board[response[2]] = main_board[response[1]]
del main_board[response[1]]
如果用户输入类似move e2 e4的内容,那么response[0]将是'move'。我们可以通过首先将main_board中旧方格(在response[1]中)的棋子复制到新方格(在response[2]中)来“移动”一个棋子从一方格到另一方格。然后,我们可以删除main_board中旧方格的键值对。这会产生一种效果,好像棋子已经移动了(尽管我们不会在再次调用print_chessboard()之前看到这个变化)。
我们的交互式棋盘模拟器不会检查这是否是一个有效的移动。它只是执行用户给出的命令。如果用户输入类似remove e2的内容,程序将response设置为['remove', 'e2']:
elif response[0] == 'remove':
del main_board[response[1]]
通过从main_board中删除键response[1]的键值对,我们使棋子从棋盘上消失。如果用户输入类似set e2 wP的内容以在 e2 添加一个白兵,程序将response设置为['set', 'e2', 'wP']:
elif response[0] == 'set':
main_board[response[1]] = response[2]
我们可以在main_board中使用键response[1]和值response[2]创建一个新的键值对,以将这个棋子添加到棋盘上。如果用户输入 reset,则response将是['reset'],我们可以通过将STARTING_PIECES字典复制到main_board来设置棋盘的起始配置:
elif response[0] == 'reset':
main_board = copy.copy(STARTING_PIECES)
如果用户输入 clear,response将简单地是['clear'],我们可以通过将main_board设置为空字典来从棋盘上移除所有棋子:
elif response[0] == 'clear':
main_board = {}
如果用户输入 fill wP,response将是['fill', 'wP'],我们将所有 64 个方格更改为字符串'wP':
elif response[0] == 'fill':
for y in '87654321':
for x in 'abcdefgh':
main_board[x + y] = response[1]
嵌套的for循环将遍历每个方格,将x + y键设置为response[1]。在棋盘上放置 64 个白兵实际上没有真正的理由,但这个命令展示了如何轻松地以我们想要的方式操作棋盘数据结构。最后,用户可以通过输入 quit 来退出程序:
elif response[0] == 'quit':
sys.exit()
执行命令并修改main_board后,执行将跳回到while循环的开始,以显示更改后的棋盘并接受用户的新命令。
这个交互式棋盘程序不限制你可以放置或移动的棋子。它只是使用字典作为棋盘上棋子的表示,并有一个函数用于在屏幕上以类似棋盘的方式显示这样的字典。我们可以通过设计数据结构和编写与这些数据结构一起工作的函数来模拟所有现实世界的对象或过程。如果你想看到另一个使用数据结构模拟游戏棋盘的例子,我的另一本书《Python 小项目大全书》(No Starch Press,2021 年)有一个可工作的井字棋程序。
嵌套字典和列表
随着你对更复杂的事物进行建模,你可能发现你需要包含其他字典和列表的字典和列表。列表对于持有有序值序列很有用,而字典对于将键与值关联很有用。例如,这里有一个使用字典来包含客人带到野餐的物品的字典的程序。total_brought()函数可以读取这种数据结构并计算每种物品类型的总数。在新的程序中输入以下代码,并将其保存为guestpicnic.py:
all_guests = {'Alice': {'apples': 5, 'pretzels': 12},
'Bob': {'ham sandwiches': 3, 'apples': 2},
'Carol': {'cups': 3, 'apple pies': 1}}
def total_brought(guests, item):
num_brought = 0
if name in birthdays: # ❷
if name in birthdays: # ❷
return num_brought
print('Number of things being brought:')
print(' - Apples ' + str(total_brought(all_guests, 'apples')))
print(' - Cups ' + str(total_brought(all_guests, 'cups')))
print(' - Cakes ' + str(total_brought(all_guests, 'cakes')))
print(' - Ham Sandwiches ' + str(total_brought(all_guests, 'ham sandwiches')))
print(' - Apple Pies ' + str(total_brought(all_guests, 'apple pies')))
在total_brought()函数中,for循环遍历guests中的键值对❶。在循环内部,客人的名字字符串被分配给k,他们带来的野餐物品的字典被分配给v。如果物品参数作为字典中的键存在,其值(数量)将被添加到num_brought❷。如果它不是作为键存在,get()方法将返回0以添加到num_brought。
这个程序的输出看起来像这样:
Number of things being brought:
- Apples 7
- Cups 3
- Cakes 0
- Ham Sandwiches 3
- Apple Pies 1
带到野餐的物品数量可能看起来很简单,以至于你不需要编写程序来做这件事。但请记住,这个相同的total_brought()函数可以轻松地处理包含数千名客人、每人带来数千种不同野餐物品的字典。在这种情况下,将此信息存储在数据结构中,并使用total_brought()函数,将为您节省大量时间!
你可以用你喜欢的任何方式使用数据结构来模拟事物,只要你的程序中的其余代码能够正确地与数据模型一起工作。当你刚开始编程时,不要过于担心“正确”的数据建模方式。随着经验的积累,你可能会有更有效的模型;重要的是数据模型能够满足你程序的需求。
摘要
你在本章中学到了所有关于字典的知识。列表和字典是可以包含多个值的值,包括其他列表和字典。字典很有用,因为你可以将一个项目(键)映射到另一个项目(值),而列表只是按顺序包含一系列值。代码可以使用方括号访问字典内的值,就像访问列表一样。与整数索引不同,字典可以有各种数据类型的键:整数、浮点数、字符串或元组。通过将程序的价值组织到数据结构中,你可以创建现实世界对象的表示,例如本章中模拟的棋盘。
练习问题
-
空字典的代码看起来是什么样子的?
-
键为
'foo'、值为42的字典值看起来是什么样子的? -
字典和列表的主要区别是什么?
-
如果
spam是{'bar': 100},尝试访问spam['foo']会发生什么? -
如果一个字典存储在
spam中,'cat' in spam和'cat' in spam.keys()这两个表达式的区别是什么? -
如果一个字典存储在
spam中,'cat' in spam和'cat' in spam.values()这两个表达式的区别是什么? -
以下代码的快捷方式是什么?
if 'color' not in spam:
spam['color'] = 'black'
- 哪个模块和函数可以用来“美化打印”字典值?
练习程序
为了练习,编写程序来完成以下任务。
棋盘字典验证器
在本章中,我们使用了字典值{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}来表示棋盘。编写一个名为isValidChessBoard()的函数,该函数接受一个字典参数,并根据棋盘是否有效返回True或False。
一个有效的棋盘将恰好有一个黑王和一个白王。每位玩家最多可以有 16 个棋子,其中只有 8 个可以是被子,所有棋子都必须位于从'1a'到'8h'的有效方格上。也就是说,棋子不能位于方格'9z'上。棋子名称应以'w'或'b'开头,分别代表白棋或黑棋,后面跟'pawn'、'knight'、'bishop'、'rook'、'queen'或'king'。这个函数应该检测是否有错误导致棋盘不正确。(这不是要求列表的详尽列表,但对于这个练习来说已经足够接近了。)
奇幻游戏库存
假设你正在创建一个中世纪幻想电子游戏。用于模拟玩家存货的数据结构是一个字典,其键是描述存货中物品的字符串,其值是整数,详细说明了玩家有多少个该物品。例如,字典值 {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} 表示玩家有一个绳索,六个火把,42 个金币,等等。
编写一个名为 display_inventory() 的函数,该函数将接受任何可能的“存货”并按以下方式显示:
Inventory:
12 arrow
42 gold coin
1 rope
6 torch
1 dagger
Total number of items: 62
提示:你可以使用一个 for 循环来遍历字典中的所有键。
stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}
def display_inventory(inventory):
print("Inventory:")
item_total = 0
for k, v in inventory.items():
# FILL THIS PART IN
print("Total number of items: " + str(item_total))
display_inventory(stuff)
列表到字典的财宝转换
想象一下,同一个幻想电子游戏将被打败的龙财宝表示为字符串列表,如下所示:
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
编写一个名为 add_to_inventory(inventory, added_items) 的函数。inventory 参数是一个表示玩家存货的字典(如前一个项目所示),而 added_items 参数是一个列表,如 dragon_loot。add_to_inventory() 函数应该返回一个表示玩家更新后的存货的字典。注意,added_items 列表可以包含相同物品的多个实例。你的代码可能看起来像这样:
def add_to_inventory(inventory, added_items):
# Your code goes here.
inv = {'gold coin': 42, 'rope': 1}
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
inv = add_to_inventory(inv, dragon_loot)
display_inventory(inv)
之前的程序(包含你之前项目中编写的 display_inventory() 函数)会输出以下内容:
Inventory:
45 gold coin
1 rope
1 ruby
1 dagger
Total number of items: 48
国际象棋字典验证器
在本章中,我们使用字典值 {'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'} 来表示棋盘。编写一个名为 isValidChessBoard() 的函数,该函数接受一个字典参数,并根据棋盘是否有效返回 True 或 False。
一个有效的棋盘将恰好有一个黑王和一个白王。每个玩家最多可以有 16 个棋子,其中只有 8 个是兵,所有棋子都必须位于从 '1a' 到 '8h' 的有效方格上。也就是说,棋子不能位于方格 '9z' 上。棋子名称应以 'w' 或 'b' 开头,以表示白棋或黑棋,后面跟 'pawn'、'knight'、'bishop'、'rook'、'queen' 或 'king'。此函数应检测当错误导致棋盘不正确时的情况。(这不是要求列表的详尽无遗,但对于这个练习来说已经足够接近了。)
幻想游戏存货
假设你正在创建一个中世纪幻想电子游戏。用于模拟玩家存货的数据结构是一个字典,其键是描述存货中物品的字符串,其值是整数,详细说明了玩家有多少个该物品。例如,字典值 {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} 表示玩家有一个绳索,六个火把,42 个金币,等等。
编写一个名为 display_inventory() 的函数,该函数将接受任何可能的“存货”并按以下方式显示:
Inventory:
12 arrow
42 gold coin
1 rope
6 torch
1 dagger
Total number of items: 62
提示:你可以使用一个 for 循环来遍历字典中的所有键。
stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}
def display_inventory(inventory):
print("Inventory:")
item_total = 0
for k, v in inventory.items():
# FILL THIS PART IN
print("Total number of items: " + str(item_total))
display_inventory(stuff)
列表到字典的财宝转换
想象一下,同一个幻想视频游戏将被打败的龙财宝表示为字符串列表,如下所示:
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
编写一个名为 add_to_inventory(inventory, added_items) 的函数。inventory 参数是一个字典,表示玩家的库存(如前一个项目所示),而 added_items 参数是一个列表,如 dragon_loot。add_to_inventory() 函数应该返回一个表示玩家更新后的库存的字典。注意,added_items 列表可以包含相同物品的多个实例。你的代码可能看起来像这样:
def add_to_inventory(inventory, added_items):
# Your code goes here.
inv = {'gold coin': 42, 'rope': 1}
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
inv = add_to_inventory(inv, dragon_loot)
display_inventory(inv)
之前的程序(包含你在上一个项目中编写的 display_inventory() 函数)会输出以下内容:
Inventory:
45 gold coin
1 rope
1 ruby
1 dagger
Total number of items: 48
第二部分 自动化任务
8 字符串和文本编辑

文本是你程序将处理的最常见数据形式之一。你已经知道如何使用+运算符连接两个字符串值,但你还能做更多,比如从字符串值中提取部分字符串,添加或删除空格,将字母转换为小写或大写,并检查字符串是否格式正确。你甚至可以编写 Python 代码来访问用于复制和粘贴文本的剪贴板。
在本章中,你将学习所有这些内容以及更多。然后,你将通过一个编程项目来自动化添加项目符号到文本的枯燥工作。
字符串处理
让我们看看 Python 如何让你在代码中编写、打印和访问字符串的一些方法。
字符串字面量
虽然字符串 值 存储在程序的内存中,但实际出现在我们代码中的字符串值被称为 字符串字面量。在 Python 代码中编写字符串字面量看起来很简单:它们以单引号开始和结束,字符串值的文本位于其中。但如何在字符串中使用引号呢?输入 'That is Alice's cat.' 不会工作,因为 Python 会认为字符串在 Alice 之后结束,并将其余部分(s cat.')视为无效的 Python 代码。幸运的是,有多种方法可以编写字符串字面量。在运行程序的环境中,字符串 指的是字符串值,当我们谈论输入 Python 源代码时,它指的是字符串字面量。
双引号
字符串字面量可以以双引号或单引号开始和结束。使用双引号的一个好处是字符串中可以包含单引号字符。在交互式 shell 中输入以下内容:
>>> spam = "That is Alice's cat."
因为字符串以双引号开始,Python 知道单引号是字符串的一部分,而不是标记字符串的结束。然而,如果你需要在字符串中使用单引号和双引号,你需要使用转义字符。
转义字符
转义字符 允许你使用在字符串字面量中通常无法放入的字符。转义字符由一个反斜杠(\)后跟你要添加到字符串中的字符组成。例如,\' 是单引号的转义字符,\n 是换行符的转义字符。(尽管由两个字符组成,但它通常被称为单个转义字符。)你可以在以单引号开始和结束的字符串中使用这种语法。要查看转义字符如何工作,在交互式 shell 中输入以下内容:
>>> spam = 'Say hi to Bob\'s mother.'
Python 知道,由于 Bob\'s 中的单引号前面有一个反斜杠,它不是一个表示字符串值结束的单引号。转义字符 \' 和 \" 分别允许你在字符串中放置单引号和双引号。
表 8-1 列出了你可以使用的转义字符。
表 8-1:转义字符
| 转义字符 | 打印为 ... |
|---|---|
\' |
单引号 |
\" |
双引号 |
\t |
制表符 |
\n |
换行符(行中断) |
\\ |
反斜杠 |
为了练习使用这些,请在交互式外壳中输入以下内容:
>>> print("Hello there!\nHow are you?\nI\'m doing fine.")
Hello there!
How are you?
I'm doing fine.
请记住,由于反斜杠 \ 开头是一个转义字符,如果你想在字符串中有一个实际的反斜杠,你必须使用 \\ 转义字符。
原始字符串
你可以在字符串字面量的开始引号前放置一个 r 来使其成为原始字符串字面量。原始字符串通过忽略所有转义字符,使得输入包含反斜杠的字符串值变得更容易。例如,在交互式外壳中输入以下内容:
>>> print(r'The file is in C:\Users\Alice\Desktop')
The file is in C:\Users\Alice\Desktop
因为这是一个原始字符串,Python 将反斜杠视为字符串的一部分,而不是转义字符的开始:
>>> print('Hello...\n\n...world!') # Without a raw string
Hello...
...world!
>>> print(r'Hello...\n\n...world!') # With a raw string
Hello...\n\n...world!
原始字符串在字符串值包含许多反斜杠时很有用,例如用于 Windows 文件路径的字符串,如 r'C:\Users\Al\Desktop' 或下一章中描述的正则表达式字符串。
多行字符串
虽然你可以使用 \n 转义字符在字符串中插入换行符,但通常使用多行字符串更容易。Python 中的多行字符串以三个单引号或三个双引号开始和结束。在“三引号”之间的任何引号、制表符或换行符都被视为字符串的一部分。Python 的代码块缩进规则不适用于多行字符串内的行。
例如,打开文件编辑器并输入以下内容:
print('''Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob''')
将此程序保存为 feedcat.py 并运行。输出将类似于以下内容:
Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob
注意,Eve's 中的单引号字符不需要转义。在多行字符串中转义单引号和双引号是可选的:
print("Dear Alice,\n\nCan you feed Eve's cat this weekend?\n\nSincerely,\nBob")
这个 print() 调用打印了相同的文本,但没有使用多行字符串。
多行注释
当哈希字符(#)标记了该行剩余部分的注释开始时,多行字符串通常用于跨越多行的注释:
"""This is a test Python program.
Written by Al Sweigart al@inventwithpython.com
This program was designed for Python 3, not Python 2.
"""
def say_hello():
"""This function prints hello.
It does not return anything."""
print('Hello!')
此示例中的多行字符串是有效的 Python 代码。
索引和切片
字符串使用索引和切片的方式与列表相同。你可以将字符串 'Hello, world!' 视为一个列表,每个字符串中的字符作为一个具有相应索引和负索引的项目:
' H e l l o , w o r l d ! '
0 1 2 3 4 5 6 7 8 9 10 11 12
-13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
空格和感叹号包含在字符计数中,因此 'Hello, world!' 是 13 个字符长,从索引 0 的 H 到索引 12 的 !。
在交互式外壳中输入以下内容:
>>> greeting = 'Hello, world!'
>>> greeting[0]
'H'
>>> greeting [4]
'o'
>>> greeting[-1]
'!'
>>> greeting[0:5]
'Hello'
>>> greeting[:5]
'Hello'
>>> greeting[7:-1]
'world'
>>> greeting[7:]
'world!'
如果你指定一个索引,你将得到字符串中该位置的字符。如果你指定一个从一个索引到另一个索引的范围,起始索引是包含的,而结束索引是不包含的。这就是为什么,如果 greeting 是 'Hello, world!',那么 greeting[0:5] 评估为 'Hello'。从 greeting[0:5] 获取的子字符串将包括从 greeting[0] 到 greeting[4] 的所有内容,省略了索引 5 处的逗号和索引 6 处的空格。这与 range(5) 导致 for 循环迭代到但不包括 5 是相似的。
注意,切片字符串不会修改原始字符串。你可以在一个变量中捕获一个切片,并在另一个变量中保存。尝试在交互式外壳中输入以下内容:
>>> greeting = 'Hello, world!'
>>> greeting_slice = greeting[0:5]
>>> greeting_slice
'Hello'
>>> greeting
'Hello, world!'
通过切片并将结果子字符串存储在另一个变量中,你可以同时拥有整个字符串和子字符串,以便快速、方便地访问。#### in 和 not in 操作符
你可以使用 in 和 not in 操作符与字符串,就像你可以与列表值一样。使用 in 或 not in 连接的两个字符串的表达式将评估为布尔值 True 或 False。在交互式外壳中输入以下内容:
>>> 'Hello' in 'Hello, World'
True
>>> 'Hello' in 'Hello'
True
>>> 'HELLO' in 'Hello, World'
False
>>> '' in 'spam'
True
>>> 'cats' not in 'cats and dogs'
False
这些表达式测试第一个字符串(包括其大小写)是否可以在第二个字符串中找到。
F-字符串
在编程中,将字符串放入其他字符串是一个常见的操作。到目前为止,我们一直在使用 + 操作符和字符串连接来完成这个操作:
>>> name = 'Al'
>>> age = 4000
>>> 'Hello, my name is ' + name + '. I am ' + str(age) + ' years old.'
'Hello, my name is Al. I am 4000 years old.'
>>> 'In ten years I will be ' + str(age + 10)
'In ten years I will be 4010'
然而,这需要大量的繁琐输入。一个更简单的方法是使用 f-字符串,它允许你在字符串中放置变量名或整个表达式。像原始字符串中的 r 前缀一样,f-字符串在起始引号之前有一个 f 前缀。在交互式外壳中输入以下内容:
>>> name = 'Al'
>>> age = 4000
>>> f'My name is {name}. I am {age} years old.'
'My name is Al. I am 4000 years old.'
>>> f'In ten years I will be {age + 10}'
'In ten years I will be 4010'
大括号 {} 之间的所有内容都被解释为传递给 str() 的参数,并在字符串中间使用 + 操作符连接。如果你需要在 f-string 中使用字面量的大括号字符,请使用两个大括号:
>>> name = 'Zophie'
>>> f'{name}'
'Zophie'
>>> f'{{name}}' # Double curly brackets are literal curly brackets.
'{name}'
F-字符串是 Python 中一个有用的特性,但语言仅在版本 3.6 中添加了它们。在较老的 Python 代码中,你可能会遇到其他技术。
F-字符串的替代方案:%s 和 format()
3.6 版本之前的 Python 有其他方法将字符串放入其他字符串中。第一种是 字符串插值,其中字符串包含一个 %s 格式说明符,Python 会将其替换为另一个字符串。例如,在交互式外壳中输入以下内容:
>>> name = 'Al'
>>> age = 4000
>>> 'My name is %s. I am %s years old.' % (name, age)
'My name is Al. I am 4000 years old.'
>>> 'In ten years I will be %s' % (age + 10)
'In ten years I will be 4010'
Python 将将第一个 %s 替换为字符串后括号内的第一个值,第二个 %s 替换为第二个字符串,依此类推。如果你只需要插入一个或两个字符串,这和 f-字符串一样有效,但当你需要插入多个字符串时,f-字符串通常更易读。
将字符串放入其他字符串中的另一种方法是使用 format() 字符串方法。您可以使用一对大括号来标记插入字符串的位置,就像字符串插值一样。在交互式外壳中输入以下内容:
>>> name = 'Al'
>>> age = 4000
>>> 'My name is {}. I am {} years old.'.format(name, age)
'My name is Al. I am 4000 years old.'
format() 方法比 %s 字符串插值有几个更多功能。您可以在大括号内放置索引整数(从 0 开始),以标记要插入 format() 的哪个参数。这在多次或顺序插入字符串时很有帮助:
>>> name = 'Al'
>>> age = 4000
>>> '{1} years ago, {0} was born and named {0}.'.format(name, age)
'4000 years ago, Al was born and named Al.'
大多数程序员更喜欢 f-strings 而不是这两种替代方案,但您仍然应该学习它们,因为您可能会在现有代码中遇到它们。
有用的字符串方法
几个字符串方法用于分析字符串或创建转换后的字符串值。本节描述了您将最常使用的方法。
改变大小写
upper() 和 lower() 字符串方法返回一个新的字符串,其中原始字符串中的所有字母分别被转换为大写或小写。字符串中的非字母字符保持不变。例如,在交互式外壳中输入以下内容:
>>> spam = 'Hello, world!'
>>> spam = spam.upper()
>>> spam
'HELLO, WORLD!'
>>> spam = spam.lower()
>>> spam
'hello, world!'
注意,这些方法不会改变字符串本身,而是返回新的字符串值。如果您想改变原始字符串,您必须在字符串上调用 upper() 或 lower(),然后将新字符串赋值给存储原始字符串的变量。这就是为什么您必须使用 spam = spam.upper() 来改变 spam 中的字符串,而不是简单地写下 spam.upper()。(这与变量 eggs 包含值 10 的情况相同。写下 eggs + 3 不会改变 eggs 的值,但 eggs = eggs + 3 会。)
upper() 和 lower() 方法在您需要执行不区分大小写的比较时很有帮助。例如,字符串 'great' 和 'GREat' 并不相等,但在以下小程序中,用户可以输入 Great、GREAT 或 grEAT,因为代码将字符串转换为小写:
print('How are you?')
feeling = input()
if feeling.lower() == 'great':
print('I feel great too.')
else:
print('I hope the rest of your day is good.')
当您运行此程序时,它会显示一个问题,并且输入任何 great 的变体,例如 GREat,都会输出 I feel great too. 在您的程序中添加代码以处理用户输入的变体或错误,例如不一致的大小写,会使您的程序更容易使用且不太可能失败:
How are you?
GREat
I feel great too.
isupper() 和 islower() 方法如果字符串至少有一个字母且所有字母都是大写或小写,则返回布尔值 True。否则,方法返回 False。在交互式外壳中输入以下内容,并注意每个方法调用返回的内容:
>>> spam = 'Hello, world!'
>>> spam.islower()
False
>>> spam.isupper()
False
>>> 'HELLO'.isupper()
True
>>> 'abc12345'.islower()
True
>>> '12345'.islower()
False
>>> '12345'.isupper()
False
由于 upper() 和 lower() 字符串方法本身返回字符串,因此您也可以在返回的字符串值上调用字符串方法:
>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.upper().lower()
'hello'
>>> 'Hello'.upper().lower().upper()
'HELLO'
>>> 'HELLO'.lower()
'hello'
>>> 'HELLO'.lower().islower()
True
执行此操作的表达式将看起来像一系列方法调用,如下所示。
检查字符串特征
除了islower()和isupper()之外,还有其他几个字符串方法以单词is开头。这些方法返回一个布尔值,描述字符串的性质。以下是一些常见的isX()字符串方法:
isalpha():如果字符串仅由字母组成且不是空白,则返回True
isalnum():如果字符串仅由字母和数字(字母数字)组成且不是空白,则返回True
isdecimal():如果字符串仅由数字字符组成且不是空白,则返回True
isspace():如果字符串仅由空格、制表符和换行符组成且不是空白,则返回True
istitle():如果字符串仅由以大写字母开头后跟仅小写字母的单词组成,则返回True
在交互式外壳中输入以下内容:
>>> 'hello'.isalpha()
True
>>> 'hello123'.isalpha()
False
>>> 'hello123'.isalnum()
True
>>> 'hello'.isalnum()
True
>>> '123'.isdecimal()
True
>>> ' '.isspace()
True
>>> 'This Is Title Case'.istitle()
True
当你需要验证用户输入时,isX()字符串方法非常有用。例如,以下程序会反复要求用户提供他们的年龄和密码,直到他们提供有效的输入。打开一个新的文件编辑窗口,并输入此程序,将其保存为validateInput.py:
while True:
print('Enter your age:')
age = input()
if age.isdecimal():
break
print('Please enter a number for your age.')
while True:
print('Select a new password (letters and numbers only):')
password = input()
if password.isalnum():
break
print('Passwords can only have letters and numbers.')
在第一个while循环中,我们要求用户输入他们的年龄,并将他们的输入存储在age中。如果age是一个有效的(十进制)值,我们就跳出这个第一个while循环,继续第二个循环,该循环要求输入密码。否则,我们通知用户他们需要输入一个数字,并再次要求他们输入他们的年龄。在第二个while循环中,我们要求输入密码,将用户的输入存储在password中,如果输入是字母数字的,就跳出循环。如果不是,我们不满意,因此我们告诉用户密码需要是字母数字的,并再次要求他们输入密码。
运行程序时,程序的输出如下所示:
Enter your age:
forty two
Please enter a number for your age.
Enter your age:
42
Select a new password (letters and numbers only):
secr3t!
Passwords can only have letters and numbers.
Select a new password (letters and numbers only):
secr3t
在变量上调用isdecimal()和isalnum(),我们可以测试存储在这些变量中的值是否为十进制或不是,以及是否为字母数字。在这里,这些测试帮助我们拒绝输入forty two但接受42,拒绝secr3t!但接受secr3t。
检查字符串的开始或结束
startswith()和endswith()方法在它们被调用的字符串值开始或结束(分别)与传递给方法的方法字符串相匹配时返回True;否则,它们返回False。在交互式外壳中输入以下内容:
>>> 'Hello, world!'.startswith('Hello')
True
>>> 'Hello, world!'.endswith('world!')
True
>>> 'abc123'.startswith('abcdef')
False
>>> 'abc123'.endswith('12')
False
>>> 'Hello, world!'.startswith('Hello, world!')
True
>>> 'Hello, world!'.endswith('Hello, world!')
True
这些方法是在你需要检查字符串的第一部分或最后一部分是否与另一个字符串相等,而不是整个字符串相等时,相对于等于运算符(==)的有用替代方案。
字符串的连接和分割
当你需要将一系列字符串连接成一个单独的字符串值时,join()方法非常有用。我们在一个字符串上调用join()方法,并传递一个字符串列表,它返回传入列表中每个字符串的连接。例如,在交互式外壳中输入以下内容:
>>> ', '.join(['cats', 'rats', 'bats'])
'cats, rats, bats'
>>> ' '.join(['My', 'name', 'is', 'Simon'])
'My name is Simon'
>>> 'ABC'.join(['My', 'name', 'is', 'Simon'])
'MyABCnameABCisABCSimon'
注意,在 join() 被调用的字符串上插入的是列表参数中的每个字符串之间。例如,当我们对 ', ' 字符串调用 join(['cats', 'rats', 'bats']) 时,它返回字符串 'cats, rats, bats'。
记住,我们是在一个字符串值上调用 join() 并传递一个列表值。(很容易错误地以相反的方式调用它。)split() 方法的工作方式相反:我们是在一个字符串值上调用它,它返回一个字符串列表。在交互式外壳中输入以下内容:
>>> 'My name is Simon'.split()
['My', 'name', 'is', 'Simon']
默认情况下,该方法会在找到空格、制表符或换行符等空白字符的地方分割字符串 'My name is Simon'。这些空白字符不包括在返回列表中的字符串中。你可以向 split() 方法传递一个分隔符字符串,以指定不同的字符串来分割。例如,在交互式外壳中输入以下内容:
>>> 'MyABCnameABCisABCSimon'.split('ABC')
['My', 'name', 'is', 'Simon']
>>> 'My name is Simon'.split('m')
['My na', 'e is Si', 'on']
split() 的一个常见用途是沿着换行符分割多行字符串。例如,在交互式外壳中输入以下内容:
>>> spam = '''Dear Alice,
... There is a milk bottle in the fridge
... that is labeled "Milk Experiment."
...
... Please do not drink it.
... Sincerely,
... Bob'''
...
>>> spam.split('\n')
['Dear Alice,', 'There is a milk bottle in the fridge',
'that is labeled "Milk Experiment."', '', 'Please do not drink it.',
'Sincerely,', 'Bob']
将 split() 的参数设置为 '\n' 允许我们根据换行符将存储在 spam 中的多行字符串分割,并返回一个列表,其中每个项目对应于字符串的一行。
文本的对齐和居中
rjust() 和 ljust() 字符串方法返回它们被调用的字符串的填充版本,其中插入空格以对齐文本。这两个方法的第一参数是一个整数长度,用于对齐字符串。在交互式外壳中输入以下内容:
>>> 'Hello'.rjust(10)
' Hello'
>>> 'Hello'.rjust(20)
' Hello'
>>> 'Hello, World'.rjust(20)
' Hello, World'
>>> 'Hello'.ljust(10)
'Hello '
代码 'Hello'.rjust(10) 表示我们想在总长度为 10 的字符串中右对齐 'Hello'。'Hello' 是五个字符,所以会在其左边添加五个空格,从而得到一个包含 'Hello' 右对齐的 10 个字符的字符串。
rjust() 和 ljust() 的可选第二个参数将指定一个除空格字符之外的填充字符。在交互式外壳中输入以下内容:
>>> 'Hello'.rjust(20, '*')
'***Hello'
>>> 'Hello'.ljust(20, '-')
'Hello---------------'
center() 字符串方法与 ljust() 和 rjust() 类似,但它是将文本居中,而不是将其左对齐或右对齐。在交互式外壳中输入以下内容:
>>> 'Hello'.center(20)
' Hello '
>>> 'Hello'.center(20, '=')
'=======Hello========'
现在打印的文本是居中的。
移除空白字符
有时你可能想从字符串的左侧、右侧或两侧去除空白字符(空格、制表符和换行符)。strip() 字符串方法将返回一个不包含任何空白字符的新字符串,而 lstrip() 和 rstrip() 方法将分别从左侧和右侧移除空白字符。在交互式外壳中输入以下内容:
>>> spam = ' Hello, World '
>>> spam.strip()
'Hello, World'
>>> spam.lstrip()
'Hello, World '
>>> spam.rstrip()
' Hello, World'
可选地,一个字符串参数将指定要去除的末尾字符。在交互式外壳中输入以下内容:
>>> spam = 'SpamSpamBaconSpamEggsSpamSpam'
>>> spam.strip('ampS')
'BaconSpamEggs'
当传递参数 'ampS' 给 strip() 函数时,它会从存储在 spam 中的字符串两端移除 a、m、p 和 S 的出现。传递给 strip() 的字符串中字符的顺序并不重要:strip('ampS') 会与 strip('mapS') 或 strip('Spam') 做相同的事情。
字符的数字编码点
计算机以 字节(二进制数字的字符串)的形式存储信息,这意味着我们需要能够将文本转换为数字。由于这个要求,每个文本字符都有一个对应的数字值,称为 Unicode 编码点。例如,字符 'A' 的数字编码点是 65,'4' 是 52,'!' 是 33。你可以使用 ord() 函数获取单字符字符串的编码点,使用 chr() 函数获取整数编码点的单字符字符串。在交互式外壳中输入以下内容:
>>> ord('A')
65
>>> ord('4')
52
>>> ord('!')
33
>>> chr(65)
'A'
这些函数在你需要对字符进行排序或执行数学运算时很有用:
>>> ord('B')
66
>>> ord('A') < ord('B')
True
>>> chr(ord('A'))
'A'
>>> chr(ord('A') + 1)
'B'
Unicode 和编码点还有更多内容,但这些细节超出了本书的范围。如果你想了解更多,我推荐观看或阅读 Ned Batchelder 的 2012 年 PyCon 演讲,“实用 Unicode,或者我如何停止痛苦?”在 nedbatchelder.com/text/unipain.html。
当字符串被写入文件或通过互联网发送时,从文本到字节的转换称为 编码。有几个 Unicode 编码标准,但最流行的是 UTF-8。如果你需要选择 Unicode 编码,99% 的情况下 'utf-8' 是正确的答案。Tom Scott 有一个名为“Characters, Symbols and the Unicode Miracle”的 Computerphile 视频,在 youtu.be/MijmeoH9LT4 中解释了 UTF-8。
复制和粘贴字符串
pyperclip 模块有 copy() 和 paste() 函数,可以将文本发送到你的计算机剪贴板并从剪贴板接收文本。将程序的输出发送到剪贴板将使其容易粘贴到电子邮件、文字处理软件或其他软件中。
pyperclip 模块不是 Python 的一部分。要安装它,请遵循附录 A 中安装第三方包的说明。安装 pyperclip 后,在交互式外壳中输入以下内容:
>>> import pyperclip
>>> pyperclip.copy('Hello, world!')
>>> pyperclip.paste()
'Hello, world!'
当然,如果你的程序之外的东西改变了剪贴板的内容,paste() 函数将返回那个其他值。例如,如果我复制了这句话到剪贴板然后调用 paste(),它看起来会是这样:
>>> pyperclip.paste()
'For example, if I copied this sentence to the clipboard and then called
paste(), it would look like this:'
剪贴板是一个在不需要通过input()调用提示输入文本的情况下输入和接收大量文本的绝佳方式。例如,假设你想要一个程序将文本转换为交替大小写的字母。你可以将想要交替的文本复制到剪贴板,然后运行这个程序,这个程序将文本转换成交替大小写的文本并放在剪贴板上。将以下代码输入到名为alternatingText.py的文件中:
import pyperclip
text = pyperclip.paste() # Get the text off the clipboard.
alt_text = '' # This string holds the alternating case.
make_uppercase = False
for character in text:
# Go through each character and add it to alt_text:
if make_uppercase:
alt_text += character.upper()
else:
alt_text += character.lower()
# Set make_uppercase to its opposite value:
make_uppercase = not make_uppercase
pyperclip.copy(alt_text) # Put the result on the clipboard.
print(alt_text) # Print the result on the screen too.
如果你将一些文本复制到剪贴板(例如,这个句子)并运行这个程序,输出和剪贴板内容将变成这样:
iF YoU CoPy sOmE TeXt tO ThE ClIpBoArD (fOr iNsTaNcE, tHiS SeNtEnCe) AnD
RuN ThIs pRoGrAm, ThE OuTpUt aNd cLiPbOaRd cOnTeNtS BeCoMe ThIs:
pyperclip模块与剪贴板交互的能力为你提供了一个简单的方法来将文本输入和输出到你的程序中。
项目 2:为维基标记添加项目符号
当编辑维基百科文章时,你可以通过将每个列表项单独一行并在其前面放置一个星号来创建一个项目符号列表。但假设你有一个非常长的列表,你想要为其添加项目符号。你可以逐行在每行的开头输入这些星号。或者,你可以使用一个简短的 Python 脚本来自动化这个任务。
bulletPointAdder.py脚本将从剪贴板获取文本,在每行的开头添加一个星号和一个空格,然后将这个新文本粘贴到剪贴板。例如,假设我将以下文本(用于维基百科文章“List of Lists of Lists”)复制到剪贴板:
Lists of animals
Lists of aquarium life
Lists of biologists by author abbreviation
Lists of cultivars
然后,如果我运行了bulletPointAdder.py程序,剪贴板将包含以下内容:
* Lists of animals
* Lists of aquarium life
* Lists of biologists by author abbreviation
* Lists of cultivars
这个带有星号前缀的文本已准备好粘贴到维基百科文章中作为项目符号列表。
第 1 步:从剪贴板复制和粘贴
你希望bulletPointAdder.py程序执行以下操作:
-
从剪贴板粘贴文本。
-
对其进行一些操作。
-
将新文本复制到剪贴板。
操作文本有点棘手,但复制和粘贴相当直接:它们只涉及pyperclip.copy()和pyperclip.paste()函数。现在,让我们编写调用这些函数的程序部分。将以下内容保存为bulletPointAdder.py:
import pyperclip
text = pyperclip.paste()
# TODO: Separate lines and add stars.
pyperclip.copy(text)
TODO注释是一个提醒,你应该最终完成程序的这个部分。下一步是实际实现这个程序片段。
第 2 步:分离文本行
pyperclip.paste()的调用返回剪贴板上的所有文本作为一个大字符串。如果我们使用“List of Lists of Lists”的例子,存储在text中的字符串将看起来像这样:
'Lists of animals\nLists of aquarium life\nLists of biologists by author
abbreviation\nLists of cultivars'
这个字符串中的\n换行符导致它在打印或从剪贴板粘贴时显示为多行。这个字符串值中有许多“行”。你想要在每个这些行的开头添加一个星号。
你可以编写代码在字符串中搜索每个\n换行符,然后在之后添加星号。但使用split()方法返回一个字符串列表,每个字符串对应原始字符串中的一行,然后在列表中的每个字符串前面添加星号会更简单。
编辑你的程序,使其看起来像以下内容:
import pyperclip
text = pyperclip.paste()
# Separate lines and add stars.
lines = text.split('\n')
for i in range(len(lines)): # Loop through all indexes in the "lines" list.
lines[i] = '* ' + lines[i] # Add a star to each string in the "lines" list.
pyperclip.copy(text)
我们沿着文本的新行分割文本,以得到一个列表,其中每个项目是文本的一行。我们将列表存储在lines中,然后遍历lines中的项目。对于每一行,我们在行首添加一个星号和一个空格。现在lines中的每个字符串都以星号开头。
第 3 步:连接修改后的行
lines列表现在包含以星号开头的修改后的行。但pyperclip.copy()期望一个字符串值,而不是字符串值的列表。为了得到这个单个字符串值,将lines传递给join()方法,以从列表的字符串中得到一个单独的字符串:
import pyperclip
text = pyperclip.paste()
# Separate lines and add stars.
lines = text.split('\n')
for i in range(len(lines)): # Loop through all indexes in the "lines" list.
lines[i] = '* ' + lines[i] # Add a star to each string in the "lines" list.
text = '\n'.join(lines)
pyperclip.copy(text)
当这个程序运行时,它会将剪贴板上的文本替换为每行开头带有星号的文本。现在程序已经完成,你可以尝试使用剪贴板上的文本运行它。
即使你不需要自动化这个特定的任务,你也可能想要自动化其他类型的文本操作,例如从行尾删除尾随空格或将文本转换为大写或小写。无论你的需求是什么,你都可以使用剪贴板作为输入和输出。
短小精悍的程序:猪拉丁语
猪拉丁语是一种愚蠢的虚构语言,它改变了英语单词。如果一个单词以元音开头,就在它的末尾添加yay。如果一个单词以辅音或辅音群(如ch或gr)开头,那么这个辅音或辅音群就被移动到单词的末尾,并跟着ay。
让我们编写一个猪拉丁语程序,输出类似以下内容:
Enter the English message to translate into pig latin:
My name is AL SWEIGART and I am 4,000 years old.
Ymay amenay isyay ALYAY EIGARTSWAY andyay Iyay amyay 4,000 yearsyay oldyay.
这个程序通过使用本章介绍的方法修改字符串来工作。将以下源代码输入到文件编辑器中,并将文件保存为pigLat.py:
# English to pig latin
print('Enter the English message to translate into pig latin:')
message = input()
VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')
pig_latin = [] # A list of the words in pig latin
for word in message.split():
# Separate the non-letters at the start of this word:
prefix_non_letters = ''
while len(word) > 0 and not word[0].isalpha():
prefix_non_letters += word[0]
word = word[1:]
if len(word) == 0:
pig_latin.append(prefix_non_letters)
continue
# Separate the non-letters at the end of this word:
suffix_non_letters = ''
while not word[-1].isalpha():
suffix_non_letters = word[-1] + suffix_non_letters
word = word[:-1]
# Remember if the word was in uppercase or title case:
was_upper = word.isupper()
was_title = word.istitle()
word = word.lower() # Make the word lowercase for translation.
# Separate the consonants at the start of this word:
prefix_consonants = ''
while len(word) > 0 and not word[0] in VOWELS:
prefix_consonants += word[0]
word = word[1:]
# Add the pig latin ending to the word:
if prefix_consonants != '':
word += prefix_consonants + 'ay'
else:
word += 'yay'
# Set the word back to uppercase or title case:
if was_upper:
word = word.upper()
if was_title:
word = word.title()
# Add the non-letters back to the start or end of the word.
pig_latin.append(prefix_non_letters + word + suffix_non_letters)
# Join all the words back together into a single string:
print(' '.join(pig_latin))
让我们逐行查看这段代码,从顶部开始:
# English to pig latin
print('Enter the English message to translate into pig latin:')
message = input()
VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')
首先,我们要求用户输入要翻译成猪拉丁语的英文文本。同时,我们创建一个常量,其中包含每个小写元音(以及y)作为一个字符串元组。我们稍后会使用这个变量。
接下来,我们创建pig_latin变量来存储我们将其翻译成猪拉丁语的单词:
pig_latin = [] # A list of the words in pig latin
for word in message.split():
# Separate the non-letters at the start of this word:
prefix_non_letters = ''
while len(word) > 0 and not word[0].isalpha():
prefix_non_letters += word[0]
word = word[1:]
if len(word) == 0:
pig_latin.append(prefix_non_letters)
continue
我们需要每个单词都是自己的字符串,所以我们调用message.split()来获取一个单词作为单独字符串的列表。字符串'My name is AL SWEIGART and I am 4,000 years old.'会导致split()返回['My', 'name', 'is', 'AL', 'SWEIGART', 'and', 'I', 'am', '4,000', 'years', 'old.']。
我们还需要从每个单词的开头和结尾删除任何非字母字符,以便像'old.'这样的字符串翻译成'oldyay.'而不是'old.yay'。我们将这些非字母字符保存到一个名为prefix_non_letters的变量中。
# Separate the non-letters at the end of this word:
suffix_non_letters = ''
while not word[-1].isalpha():
suffix_non_letters += word[-1] + suffix_non_letters
word = word[:-1]
一个在单词的第一个字符上调用isalpha()的循环确定我们是否应该从单词中删除一个字符并将其连接到prefix_non_letters的末尾。如果整个单词由非字母字符组成,如'4,000',我们可以简单地将其附加到pig_latin列表中,并继续翻译下一个单词。我们还需要保存word字符串末尾的非字母字符。此代码与前面的循环类似。
接下来,我们确保程序记住单词是否为大写或标题格式,这样我们就可以在将单词翻译成猪拉丁语后将其恢复:
# Remember if the word was in uppercase or title case:
was_upper = word.isupper()
was_title = word.istitle()
word = word.lower() # Make the word lowercase for translation.
对于for循环中的其余代码,我们将处理word的小写版本。
为了将像sweigart这样的单词转换为eigart-sway,我们需要从word的开头移除所有的辅音:
# Separate the consonants at the start of this word:
prefix_consonants = ''
while len(word) > 0 and not word[0] in VOWELS:
prefix_consonants += word[0]
word = word[1:]
我们使用一个与从word开头移除非字母字符的循环类似的循环,但现在我们正在移除辅音并将它们存储在一个名为prefix_consonants的变量中。
如果单词开头有任何辅音,它们现在在prefix_consonants中,我们应该将这个变量和字符串'ay'连接到word的末尾。否则,我们可以假设word以元音开头,我们只需要连接'yay':
# Add the pig latin ending to the word:
if prefix_consonants != '':
word += prefix_consonants + 'ay'
else:
word += 'yay'
回想一下,我们使用word = word.lower()将word设置为它的小写版本。如果word最初是大写或标题格式,此代码将word转换回其原始格式:
# Set the word back to uppercase or title case:
if was_upper:
word = word.upper()
if was_title:
word = word.title()
在for循环结束时,我们将单词及其原始的任何非字母前缀或后缀附加到pig_latin列表中:
# Add the non-letters back to the start or end of the word.
pig_latin.append(prefix_non_letters + word + suffix_non_letters)
# Join all the words back together into a single string:
print(' '.join(pig_latin))
在这个循环完成后,我们通过调用join()方法将字符串列表合并成一个单独的字符串,然后将这个单独的字符串传递给print()以在屏幕上显示我们的猪拉丁语。
摘要
文本是常见的数据形式,Python 附带了许多有用的字符串方法来处理存储在字符串值中的文本。你将在你编写的几乎每个 Python 程序中使用索引、切片和字符串方法。
你现在编写的程序看起来并不太复杂;它们没有带有图像和彩色文本的图形用户界面(GUI)。到目前为止,你使用print()显示文本,并使用input()让用户输入文本。然而,用户可以通过剪贴板快速输入大量文本。这种能力为编写操作大量文本的程序提供了有用的途径。这些基于文本的程序可能没有闪亮的窗口或图形,但它们可以快速完成大量有用的工作。
另一种操作大量文本的方法是直接从硬盘驱动器读取和写入文件。你将在第十章学习如何使用 Python 做到这一点。
这就涵盖了 Python 编程的所有基本概念!你将在本书的其余部分继续学习新的概念,但现在你已经足够了解,可以开始编写一些有用的程序来自动化任务。如果你想看到由你迄今为止学到的基本概念构建的短小简单的 Python 程序的集合,你可以阅读我的另一本书,《Python 小项目大全书》(No Starch Press,2021)。尝试手动复制每个程序的源代码,然后对其进行修改,看看它们如何影响程序的行为。一旦你理解了程序的工作原理,尝试从头开始重新创建程序。你不需要精确地重新创建源代码;只需关注程序做什么,而不是它是如何做的。
你可能认为你没有足够的 Python 知识去做像下载网页、更新电子表格或发送短信这样的任务,但这就是 Python 模块的作用所在!这些由其他程序员编写的模块提供了使你能够轻松完成所有这些任务的函数。在下一章中,你将学习如何编写用于执行有用自动化任务的真正程序。
练习问题
-
转义字符是什么?
-
\n和\t转义字符分别代表什么? -
你如何在字符串中放置一个反斜杠
\字符? -
字符串值
"Howl's Moving Castle"是一个有效的字符串。为什么单词Howl's中的单引号字符没有转义就不是问题呢? -
如果你不想在字符串中放置
\n,你如何编写包含换行符的字符串? -
以下表达式计算结果是什么?
-
'Hello, world!'[1] -
'Hello, world!'[0:5] -
'Hello, world!'[:5] -
'Hello, world!'[3:]
- 以下表达式计算结果是什么?
-
'Hello'.upper() -
'Hello'.upper().isupper() -
'Hello'.upper().lower()
- 以下表达式计算结果是什么?
-
'Remember, remember, the fifth of November.'.split() -
'-'.join('There can be only one.'.split())
-
你可以使用哪些字符串方法来实现字符串的右对齐、左对齐和居中对齐?
-
你如何从字符串的开始或结束处删除空白字符?
练习程序:表格打印器
为了练习,编写一个名为 printTable() 的函数,该函数接受一个字符串列表的列表,并以每列右对齐的方式显示一个组织良好的表格。假设所有内部列表都将包含相同数量的字符串。例如,值可能看起来像这样:
tableData = [['apples', 'oranges', 'cherries', 'banana'],
['Alice', 'Bob', 'Carol', 'David'],
['dogs', 'cats', 'moose', 'goose']]
你的 printTable() 函数将打印以下内容:
apples Alice dogs
oranges Bob cats
cherries Carol moose
banana David goose
提示:你的代码首先必须找到每个内部列表中最长的字符串,这样整个列才能足够宽,以容纳所有字符串。你可以将每列的最大宽度存储为一个整数列表。printTable()函数可以从colWidths = [0] * len(tableData)开始,这将创建一个包含与tableData中内部列表数量相同个数的0值的列表。这样,colWidths[0]可以存储tableData[0]中最长字符串的宽度,colWidths[1]可以存储tableData[1]中最长字符串的宽度,依此类推。然后你可以找到colWidths列表中的最大值,以确定传递给rjust()字符串方法的整数宽度。
字符串处理
让我们来看看 Python 如何让你在代码中编写、打印和访问字符串的一些方法。
字符串字面量
当字符串值存储在程序的内存中时,我们代码中实际出现的字符串值被称为字符串字面量。在 Python 代码中编写字符串字面量看起来很简单:它们以单个引号开始和结束,字符串值的文本位于其中。但是,如何在字符串中使用引号呢?输入'That is Alice's cat.'不会工作,因为 Python 会认为字符串在Alice之后结束,并将剩余的(s cat.')视为无效的 Python 代码。幸运的是,有多种方法可以编写字符串字面量。在运行程序的情况下,字符串指的是字符串值,当我们谈论输入 Python 源代码时,它指的是字符串字面量。
双引号
字符串字面量可以以双引号或单引号开始和结束。使用双引号的一个好处是字符串中可以包含单引号字符。请在交互式 shell 中输入以下内容:
>>> spam = "That is Alice's cat."
因为字符串以双引号开头,Python 知道单引号是字符串的一部分,而不是标记字符串的结束。然而,如果你需要在字符串中使用单引号和双引号,你需要使用转义字符。
转义字符
转义字符允许你使用在字符串字面量中其他情况下无法放入的字符。转义字符由一个反斜杠(\)后跟要添加到字符串中的字符组成。例如,\'是单引号的转义字符,\n是换行符的转义字符。(尽管由两个字符组成,但它通常被称为单个转义字符。)你可以在以单引号开始和结束的字符串中使用此语法。要查看转义字符如何工作,请在交互式 shell 中输入以下内容:
>>> spam = 'Say hi to Bob\'s mother.'
Python 知道,由于Bob\'s中的单引号前面有一个反斜杠,它不是一个表示字符串值结束的单引号。转义字符\'和\"分别允许你在字符串中放置单引号和双引号。
表 8-1 列出了你可以使用的转义字符。
表 8-1:转义字符
| 转义字符 | 打印为 ... |
|---|---|
\' |
单引号 |
\" |
双引号 |
\t |
制表符 |
\n |
换行符(行中断) |
\\ |
反斜杠 |
为了练习使用这些,将以下内容输入到交互式外壳中:
>>> print("Hello there!\nHow are you?\nI\'m doing fine.")
Hello there!
How are you?
I'm doing fine.
请记住,由于反斜杠 \ 开启了一个转义字符,如果你想在字符串中包含实际的反斜杠,你必须使用 \\ 转义字符。
原始字符串
你可以在字符串字面量的开头引号前放置一个 r 来使其成为一个原始字符串字面量。原始字符串通过忽略所有转义字符,使得输入包含反斜杠的字符串值变得更容易。例如,将以下内容输入到交互式外壳中:
>>> print(r'The file is in C:\Users\Alice\Desktop')
The file is in C:\Users\Alice\Desktop
因为这是一个原始字符串,Python 将反斜杠视为字符串的一部分,而不是转义字符的开始:
>>> print('Hello...\n\n...world!') # Without a raw string
Hello...
...world!
>>> print(r'Hello...\n\n...world!') # With a raw string
Hello...\n\n...world!
如果你的字符串值包含许多反斜杠,原始字符串非常有用,例如用于 Windows 文件路径的字符串,如 r'C:\Users\Al\Desktop' 或下一章中描述的正则表达式字符串。
多行字符串
虽然你可以使用 \n 转义字符在字符串中插入换行符,但通常使用多行字符串更容易。Python 中的多行字符串以三个单引号或三个双引号开始和结束。任何位于“三引号”之间的引号、制表符或换行符都被视为字符串的一部分。Python 的代码块缩进规则不适用于多行字符串内部的行。
例如,打开文件编辑器并输入以下内容:
print('''Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob''')
将此程序保存为 feedcat.py 并运行它。输出将如下所示:
Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob
注意到 Eve's 中的单引号不需要转义。在多行字符串中转义单引号和双引号是可选的:
print("Dear Alice,\n\nCan you feed Eve's cat this weekend?\n\nSincerely,\nBob")
这个 print() 调用打印了相同的文本,但没有使用多行字符串。
多行注释
虽然哈希字符 (#) 标记了该行剩余部分的注释开始,但多行字符串通常用于跨越多行的注释:
"""This is a test Python program.
Written by Al Sweigart al@inventwithpython.com
This program was designed for Python 3, not Python 2.
"""
def say_hello():
"""This function prints hello.
It does not return anything."""
print('Hello!')
此示例中的多行字符串是有效的 Python 代码。
索引和切片
字符串使用索引和切片的方式与列表相同。你可以将字符串 'Hello, world!' 视为一个列表,每个字符串中的字符作为一个具有相应索引和负索引的项目:
' H e l l o , w o r l d ! '
0 1 2 3 4 5 6 7 8 9 10 11 12
-13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
空格和感叹号包含在字符计数中,所以 'Hello, world!' 是 13 个字符长,从索引 0 的 H 到索引 12 的 !。
将以下内容输入到交互式外壳中:
>>> greeting = 'Hello, world!'
>>> greeting[0]
'H'
>>> greeting [4]
'o'
>>> greeting[-1]
'!'
>>> greeting[0:5]
'Hello'
>>> greeting[:5]
'Hello'
>>> greeting[7:-1]
'world'
>>> greeting[7:]
'world!'
如果你指定一个索引,你将得到字符串中该位置的字符。如果你指定一个从一索引到另一个索引的范围,起始索引是包含在内的,而结束索引则不是。这就是为什么,如果greeting是'Hello, world!',那么greeting[0:5]的结果是'Hello'。从greeting[0:5]得到的子字符串将包括从greeting[0]到greeting[4]的所有内容,省略了索引5处的逗号和索引6处的空格。这和range(5)导致for循环迭代到但不包括5是类似的。
注意,切片字符串不会修改原始字符串。你可以在一个变量中捕获一个切片,并将其存储在另一个变量中。尝试在交互式外壳中输入以下内容:
>>> greeting = 'Hello, world!'
>>> greeting_slice = greeting[0:5]
>>> greeting_slice
'Hello'
>>> greeting
'Hello, world!'
通过切片并将结果子字符串存储在另一个变量中,你可以同时拥有整个字符串和子字符串,以便快速、方便地访问。#### in 和 not in 运算符
你可以使用in和not in运算符与字符串,就像你可以与列表值一样。使用in或not in连接的两个字符串的表达式将评估为布尔值True或False。在交互式外壳中输入以下内容:
>>> 'Hello' in 'Hello, World'
True
>>> 'Hello' in 'Hello'
True
>>> 'HELLO' in 'Hello, World'
False
>>> '' in 'spam'
True
>>> 'cats' not in 'cats and dogs'
False
这些表达式测试第一个字符串(包括其大小写)是否可以在第二个字符串中找到。
字符串字面量
虽然字符串值存储在程序的内存中,但我们在代码中实际出现的字符串值被称为字符串字面量。在 Python 代码中编写字符串字面量看起来很简单:它们以单个引号开始和结束,字符串值的文本位于其中。但是,你如何在字符串中放入引号?输入'That is Alice's cat.'不会工作,因为 Python 会认为字符串在Alice之后结束,并将其余部分(s cat.')视为无效的 Python 代码。幸运的是,有多种方式可以编写字符串字面量。在运行程序的情况下,字符串指的是字符串值,当我们谈论输入 Python 源代码时,它指的是字符串字面量。
双引号
字符串字面量可以以双引号或单引号开始和结束。使用双引号的一个好处是字符串中可以包含单引号字符。在交互式外壳中输入以下内容:
>>> spam = "That is Alice's cat."
因为字符串以双引号开始,Python 知道单引号是字符串的一部分,而不是标记字符串的结束。然而,如果你需要在字符串中使用单引号和双引号,你需要使用转义字符。
转义字符
一个 转义字符 允许你使用在字符串字面量中通常无法放入的字符。转义字符由一个反斜杠(\)后跟你要添加到字符串中的字符组成。例如,\' 是单引号的转义字符,\n 是换行符的转义字符。(尽管它由两个字符组成,但它通常被称为单个转义字符。)你可以在以单引号开始和结束的字符串中使用此语法。要查看转义字符是如何工作的,请在交互式外壳中输入以下内容:
>>> spam = 'Say hi to Bob\'s mother.'
Python 知道,由于 Bob\'s 中的单引号前面有一个反斜杠,它不是一个用来结束字符串值的单引号。转义字符 \' 和 \" 分别允许你在字符串中放置单引号和双引号。
表 8-1 列出了你可以使用的转义字符。
表 8-1:转义字符
| 转义字符 | 打印为 ... |
|---|---|
\' |
单引号 |
\" |
双引号 |
\t |
制表符 |
\n |
换行符(行中断) |
\\ |
反斜杠 |
为了练习使用这些,请在交互式外壳中输入以下内容:
>>> print("Hello there!\nHow are you?\nI\'m doing fine.")
Hello there!
How are you?
I'm doing fine.
请记住,由于反斜杠 \ 开始于转义字符,如果你想在字符串中有一个实际的反斜杠,你必须使用 \\ 转义字符。
原始字符串
你可以在字符串字面量的开始引号前放置一个 r 来使其成为一个原始字符串字面量。一个 原始字符串 通过忽略所有转义字符,使得输入包含反斜杠的字符串值变得更容易。例如,请在交互式外壳中输入以下内容:
>>> print(r'The file is in C:\Users\Alice\Desktop')
The file is in C:\Users\Alice\Desktop
因为这是一个原始字符串,Python 将反斜杠视为字符串的一部分,而不是转义字符的开始:
>>> print('Hello...\n\n...world!') # Without a raw string
Hello...
...world!
>>> print(r'Hello...\n\n...world!') # With a raw string
Hello...\n\n...world!
如果你的字符串值包含许多反斜杠,例如用于 Windows 文件路径的字符串 r'C:\Users\Al\Desktop' 或下一章中描述的正则表达式字符串,原始字符串非常有用。
多行字符串
虽然你可以使用 \n 转义字符在字符串中插入换行符,但通常使用多行字符串更容易。Python 中的多行字符串以三个单引号或三个双引号开始和结束。任何位于“三引号”之间的引号、制表符或换行符都被视为字符串的一部分。Python 的缩进规则对于多行字符串内部的行不适用。
例如,打开文件编辑器并输入以下内容:
print('''Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob''')
将此程序保存为 feedcat.py 并运行它。输出将如下所示:
Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob
注意,在 Eve's 中的单引号字符不需要转义。在多行字符串中转义单引号和双引号是可选的:
print("Dear Alice,\n\nCan you feed Eve's cat this weekend?\n\nSincerely,\nBob")
这个 print() 调用打印了相同的文本,但没有使用多行字符串。
多行注释
当哈希字符(#)标记了该行剩余部分的注释开始时,多行字符串通常用于跨越多行的注释:
"""This is a test Python program.
Written by Al Sweigart al@inventwithpython.com
This program was designed for Python 3, not Python 2.
"""
def say_hello():
"""This function prints hello.
It does not return anything."""
print('Hello!')
此例中的多行字符串是有效的 Python 代码。
双引号
字符串字面量可以以双引号或单引号开始和结束。使用双引号的一个好处是字符串中可以包含单引号字符。请在交互式外壳中输入以下内容:
>>> spam = "That is Alice's cat."
因为字符串以双引号开头,Python 知道单引号是字符串的一部分,而不是标记字符串的结束。然而,如果你需要在字符串中使用单引号和双引号,你需要使用转义字符。
转义字符
转义字符让你可以使用在字符串字面量中通常无法放入的字符。转义字符由一个反斜杠(\)后跟你要添加到字符串中的字符组成。例如,\'是单引号的转义字符,\n是换行符的转义字符。(尽管它由两个字符组成,但它通常被称为单个转义字符。)你可以在以单引号开始和结束的字符串中使用此语法。要查看转义字符是如何工作的,请在交互式外壳中输入以下内容:
>>> spam = 'Say hi to Bob\'s mother.'
Python 知道,由于Bob\'s中的单引号前面有一个反斜杠,它不是一个用来结束字符串值的单引号。转义字符\'和\"分别让你在字符串中放入单引号和双引号。
表 8-1 列出了你可以使用的转义字符。
表 8-1:转义字符
| 转义字符 | 打印为 ... |
|---|---|
\' |
单引号 |
\" |
双引号 |
\t |
制表符 |
\n |
换行符(行中断) |
\\ |
反斜杠 |
为了练习使用这些,请在交互式外壳中输入以下内容:
>>> print("Hello there!\nHow are you?\nI\'m doing fine.")
Hello there!
How are you?
I'm doing fine.
请记住,因为\反斜杠开始一个转义字符,如果你想在你的字符串中有一个实际的反斜杠,你必须使用\\转义字符。
原始字符串
你可以在字符串字面量的开始引号前放置一个r,使其成为一个原始字符串字面量。原始字符串使得输入包含反斜杠的字符串值变得更容易,因为它忽略了所有的转义字符。例如,请在交互式外壳中输入以下内容:
>>> print(r'The file is in C:\Users\Alice\Desktop')
The file is in C:\Users\Alice\Desktop
因为这是一个原始字符串,Python 认为反斜杠是字符串的一部分,而不是转义字符的开始:
>>> print('Hello...\n\n...world!') # Without a raw string
Hello...
...world!
>>> print(r'Hello...\n\n...world!') # With a raw string
Hello...\n\n...world!
原始字符串在字符串值包含许多反斜杠时很有帮助,例如用于 Windows 文件路径的字符串,如r'C:\Users\Al\Desktop',或者下一章中描述的正则表达式字符串。
多行字符串
虽然你可以使用\n转义字符在字符串中插入换行符,但通常使用多行字符串会更简单。Python 中的多行字符串以三个单引号或三个双引号开始和结束。在“三引号”之间的任何引号、制表符或换行符都被视为字符串的一部分。Python 的代码块缩进规则不适用于多行字符串内部的行。
例如,打开文件编辑器并输入以下内容:
print('''Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob''')
将此程序保存为 feedcat.py 并运行它。输出将类似于以下内容:
Dear Alice,
Can you feed Eve's cat this weekend?
Sincerely,
Bob
注意,在 Eve's 中的单引号字符不需要转义。在多行字符串中,转义单引号和双引号是可选的:
print("Dear Alice,\n\nCan you feed Eve's cat this weekend?\n\nSincerely,\nBob")
这个 print() 调用打印了相同的文本,但没有使用多行字符串。
多行注释
虽然 hash 字符 (#) 标记了该行其余部分的注释的开始,但多行字符串通常用于跨越多行的注释:
"""This is a test Python program.
Written by Al Sweigart al@inventwithpython.com
This program was designed for Python 3, not Python 2.
"""
def say_hello():
"""This function prints hello.
It does not return anything."""
print('Hello!')
本例中的多行字符串是有效的 Python 代码。
索引和切片
字符串使用索引和切片的方式与列表相同。您可以将字符串 'Hello, world!' 视为一个列表,其中每个字符在字符串中都是一个具有相应索引和负索引的项目:
' H e l l o , w o r l d ! '
0 1 2 3 4 5 6 7 8 9 10 11 12
-13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
空格和感叹号包含在字符计数中,因此 'Hello, world!' 是 13 个字符长,从索引 0 的 H 到索引 12 的 !。
在交互式外壳中输入以下内容:
>>> greeting = 'Hello, world!'
>>> greeting[0]
'H'
>>> greeting [4]
'o'
>>> greeting[-1]
'!'
>>> greeting[0:5]
'Hello'
>>> greeting[:5]
'Hello'
>>> greeting[7:-1]
'world'
>>> greeting[7:]
'world!'
如果您指定一个索引,您将得到字符串中该位置的字符。如果您指定一个从一个索引到另一个索引的范围,则起始索引包含在内,而结束索引不包含。这就是为什么,如果 greeting 是 'Hello, world!',那么 greeting[0:5] 评估为 'Hello'。从 greeting[0:5] 获取的子字符串将包括从 greeting[0] 到 greeting[4] 的所有内容,省略了索引 5 的逗号和索引 6 的空格。这与 range(5) 导致 for 循环迭代到但不包括 5 类似。
注意,切片字符串不会修改原始字符串。您可以从一个变量中捕获一个切片并将其存储在另一个变量中。尝试在交互式外壳中输入以下内容:
>>> greeting = 'Hello, world!'
>>> greeting_slice = greeting[0:5]
>>> greeting_slice
'Hello'
>>> greeting
'Hello, world!'
通过切片并将结果子字符串存储在另一个变量中,您可以同时方便地访问整个字符串和子字符串。
in 和 not in 操作符
您可以使用 in 和 not in 操作符与字符串一起使用,就像您可以使用列表值一样。使用 in 或 not in 连接的两个字符串的表达式将评估为布尔值 True 或 False。在交互式外壳中输入以下内容:
>>> 'Hello' in 'Hello, World'
True
>>> 'Hello' in 'Hello'
True
>>> 'HELLO' in 'Hello, World'
False
>>> '' in 'spam'
True
>>> 'cats' not in 'cats and dogs'
False
这些表达式测试第一个字符串(包括其大小写)是否可以在第二个字符串中找到。
F-Strings
将字符串放入其他字符串是编程中的常见操作。到目前为止,我们一直使用 + 操作符和字符串连接来完成此操作:
>>> name = 'Al'
>>> age = 4000
>>> 'Hello, my name is ' + name + '. I am ' + str(age) + ' years old.'
'Hello, my name is Al. I am 4000 years old.'
>>> 'In ten years I will be ' + str(age + 10)
'In ten years I will be 4010'
然而,这需要大量的繁琐输入。一种更简单的方法是使用 f-strings,它允许您在字符串中放置变量名或整个表达式。与原始字符串中的 r 前缀一样,f-strings 在起始引号之前有一个 f 前缀。在交互式外壳中输入以下内容:
>>> name = 'Al'
>>> age = 4000
>>> f'My name is {name}. I am {age} years old.'
'My name is Al. I am 4000 years old.'
>>> f'In ten years I will be {age + 10}'
'In ten years I will be 4010'
花括号 {} 之间的所有内容都被解释为传递给 str() 的内容,并在字符串中间使用 + 操作符连接。如果您需要在 f-string 中使用字面量花括号字符,请使用两个花括号:
>>> name = 'Zophie'
>>> f'{name}'
'Zophie'
>>> f'{{name}}' # Double curly brackets are literal curly brackets.
'{name}'
F-strings 是 Python 中一个有用的特性,但语言仅在版本 3.6 中添加了它们。在较老的 Python 代码中,你可能遇到替代技术。
F-String 替代方案:%s 和 format()
3.6 版本之前的 Python 有其他方法将字符串放入其他字符串中。第一种是 字符串插值,其中字符串包含一个 %s 格式说明符,Python 会将其替换为另一个字符串。例如,在交互式外壳中输入以下内容:
>>> name = 'Al'
>>> age = 4000
>>> 'My name is %s. I am %s years old.' % (name, age)
'My name is Al. I am 4000 years old.'
>>> 'In ten years I will be %s' % (age + 10)
'In ten years I will be 4010'
Python 将替换字符串后的第一个 %s 与括号中第一个值,第二个 %s 与第二个字符串,依此类推。如果你只需要插入一个或两个字符串,这和 f-strings 一样有效,但如果你需要插入多个字符串,f-strings 通常更易读。
将字符串放入其他字符串的下一个方法是使用 format() 字符串方法。你可以使用一对大括号来标记插入字符串的位置,就像字符串插值一样。在交互式外壳中输入以下内容:
>>> name = 'Al'
>>> age = 4000
>>> 'My name is {}. I am {} years old.'.format(name, age)
'My name is Al. I am 4000 years old.'
format() 方法比 %s 字符串插值有更多功能。你可以在大括号内放置索引整数(从 0 开始),以标记 format() 应插入哪个参数。这在多次或顺序插入字符串时很有帮助:
>>> name = 'Al'
>>> age = 4000
>>> '{1} years ago, {0} was born and named {0}.'.format(name, age)
'4000 years ago, Al was born and named Al.'
大多数程序员更喜欢 f-strings 而不是这两种替代方案,但你应该学习它们,因为你在现有的代码中可能会遇到它们。
有用的字符串方法
几个字符串方法分析字符串或创建转换后的字符串值。本节描述了你会最常使用的方法。
改变大小写
upper() 和 lower() 字符串方法返回一个新的字符串,其中原始字符串中的所有字母分别被转换为大写或小写。字符串中的非字母字符保持不变。例如,在交互式外壳中输入以下内容:
>>> spam = 'Hello, world!'
>>> spam = spam.upper()
>>> spam
'HELLO, WORLD!'
>>> spam = spam.lower()
>>> spam
'hello, world!'
注意,这些方法不会改变字符串本身,而是返回新的字符串值。如果你想改变原始字符串,你必须对字符串调用 upper() 或 lower(),然后将新字符串赋值给存储原始字符串的变量。这就是为什么你必须使用 spam = spam.upper() 来改变 spam 中的字符串,而不是简单地写 spam.upper()。(这就像变量 eggs 包含值 10 一样。写 eggs + 3 不会改变 eggs 的值,但 eggs = eggs + 3 会。)
upper() 和 lower() 方法在需要执行不区分大小写的比较时很有帮助。例如,字符串 'great' 和 'GREat' 并不相等,但在以下小程序中,用户可以输入 Great、GREAT 或 grEAT,因为代码将字符串转换为小写:
print('How are you?')
feeling = input()
if feeling.lower() == 'great':
print('I feel great too.')
else:
print('I hope the rest of your day is good.')
当你运行此程序时,它会显示一个问题,输入 great 的任何变体,例如 GREat,将输出 I feel great too. 在你的程序中添加代码来处理用户输入的变体或错误,例如不一致的大小写,将使你的程序更容易使用且更不容易失败:
How are you?
GREat
I feel great too.
isupper() 和 islower() 方法将返回一个布尔值 True,如果字符串至少有一个字母,并且所有字母都是大写或小写,分别。否则,该方法返回 False。将以下内容输入到交互式壳中,并注意每个方法调用返回的内容:
>>> spam = 'Hello, world!'
>>> spam.islower()
False
>>> spam.isupper()
False
>>> 'HELLO'.isupper()
True
>>> 'abc12345'.islower()
True
>>> '12345'.islower()
False
>>> '12345'.isupper()
False
由于 upper() 和 lower() 字符串方法本身返回字符串,因此您也可以在那些返回的字符串值上调用字符串方法:
>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.upper().lower()
'hello'
>>> 'Hello'.upper().lower().upper()
'HELLO'
>>> 'HELLO'.lower()
'hello'
>>> 'HELLO'.lower().islower()
True
执行此操作的语句将看起来像一系列方法调用,如下所示。
检查字符串特征
除了 islower() 和 isupper() 之外,还有几个其他以单词 is 开头的字符串方法。这些方法返回一个布尔值,描述字符串的性质。以下是一些常见的 isX() 字符串方法:
isalpha() 如果字符串仅由字母组成且不是空白,则返回 True
isalnum() 如果字符串仅由字母和数字(字母数字)组成且不是空白,则返回 True
isdecimal() 如果字符串仅由数字字符组成且不是空白,则返回 True
isspace() 如果字符串仅由空格、制表符和换行符组成且不是空白,则返回 True
istitle() 如果字符串仅由以大写字母开头后跟仅小写字母的单词组成,则返回 True
将以下内容输入到交互式壳中:
>>> 'hello'.isalpha()
True
>>> 'hello123'.isalpha()
False
>>> 'hello123'.isalnum()
True
>>> 'hello'.isalnum()
True
>>> '123'.isdecimal()
True
>>> ' '.isspace()
True
>>> 'This Is Title Case'.istitle()
True
isX() 字符串方法在需要验证用户输入时很有用。例如,以下程序会反复要求用户提供他们的年龄和密码,直到他们提供有效的输入。打开一个新的文件编辑窗口并输入此程序,将其保存为 validateInput.py:
while True:
print('Enter your age:')
age = input()
if age.isdecimal():
break
print('Please enter a number for your age.')
while True:
print('Select a new password (letters and numbers only):')
password = input()
if password.isalnum():
break
print('Passwords can only have letters and numbers.')
在第一个 while 循环中,我们询问用户他们的年龄并将他们的输入存储在 age 中。如果 age 是一个有效的(十进制)值,我们就跳出这个第一个 while 循环并继续第二个,它要求输入密码。否则,我们通知用户他们需要输入一个数字,并再次要求他们输入他们的年龄。在第二个 while 循环中,我们要求输入密码,将用户的输入存储在 password 中,如果输入是字母数字的,就跳出循环。如果不是,我们不满意,因此我们告诉用户密码需要是字母数字的,并再次要求他们输入密码。
运行时,程序输出如下:
Enter your age:
forty two
Please enter a number for your age.
Enter your age:
42
Select a new password (letters and numbers only):
secr3t!
Passwords can only have letters and numbers.
Select a new password (letters and numbers only):
secr3t
在变量上调用 isdecimal() 和 isalnum(),我们可以测试存储在这些变量中的值是否为十进制或不是,以及是否为字母数字或不是。在这里,这些测试帮助我们拒绝输入 forty two 但接受 42,拒绝 secr3t! 但接受 secr3t。
检查字符串的开始或结束
startswith() 和 endswith() 方法如果它们被调用的字符串值(分别)以传递给方法的方法的字符串开始或结束,则返回 True;否则,它们返回 False。在交互式外壳中输入以下内容:
>>> 'Hello, world!'.startswith('Hello')
True
>>> 'Hello, world!'.endswith('world!')
True
>>> 'abc123'.startswith('abcdef')
False
>>> 'abc123'.endswith('12')
False
>>> 'Hello, world!'.startswith('Hello, world!')
True
>>> 'Hello, world!'.endswith('Hello, world!')
True
这些方法是在需要仅检查字符串的第一部分或最后一部分是否与另一个字符串相等时,而不是整个字符串相等的有用替代方法。
连接和分割字符串
join() 方法在需要将字符串列表连接成一个单独的字符串值时非常有用。我们可以在一个字符串上调用 join() 方法,并传递一个字符串列表,它将返回传入列表中每个字符串的连接。例如,在交互式外壳中输入以下内容:
>>> ', '.join(['cats', 'rats', 'bats'])
'cats, rats, bats'
>>> ' '.join(['My', 'name', 'is', 'Simon'])
'My name is Simon'
>>> 'ABC'.join(['My', 'name', 'is', 'Simon'])
'MyABCnameABCisABCSimon'
注意到 join() 被调用的字符串被插入到列表参数中的每个字符串之间。例如,当我们对 ', ' 字符串调用 join(['cats', 'rats', 'bats']) 时,它返回字符串 'cats, rats, bats'。
记住我们是在一个字符串值上调用 join() 并传递一个列表值。(很容易不小心反方向调用。)split() 方法的工作方式相反:我们调用它在一个字符串值上,它返回一个字符串列表。在交互式外壳中输入以下内容:
>>> 'My name is Simon'.split()
['My', 'name', 'is', 'Simon']
默认情况下,方法会在找到空格、制表符或换行符等空白字符的地方分割字符串 'My name is Simon'。这些空白字符不包括在返回列表中的字符串中。你可以向 split() 方法传递一个分隔符字符串,以指定不同的字符串进行分割。例如,在交互式外壳中输入以下内容:
>>> 'MyABCnameABCisABCSimon'.split('ABC')
['My', 'name', 'is', 'Simon']
>>> 'My name is Simon'.split('m')
['My na', 'e is Si', 'on']
split() 的一个常见用途是沿换行字符分割多行字符串。例如,在交互式外壳中输入以下内容:
>>> spam = '''Dear Alice,
... There is a milk bottle in the fridge
... that is labeled "Milk Experiment."
...
... Please do not drink it.
... Sincerely,
... Bob'''
...
>>> spam.split('\n')
['Dear Alice,', 'There is a milk bottle in the fridge',
'that is labeled "Milk Experiment."', '', 'Please do not drink it.',
'Sincerely,', 'Bob']
将 split() 方法的参数设置为 '\n' 允许我们根据换行符将存储在 spam 中的多行字符串分割,并返回一个列表,其中每个元素对应字符串的一行。
文本的对齐和居中
rjust() 和 ljust() 字符串方法返回它们被调用的字符串的填充版本,其中插入空格以使文本对齐。这两个方法的第一参数是一个整数长度,用于对齐的字符串。在交互式外壳中输入以下内容:
>>> 'Hello'.rjust(10)
' Hello'
>>> 'Hello'.rjust(20)
' Hello'
>>> 'Hello, World'.rjust(20)
' Hello, World'
>>> 'Hello'.ljust(10)
'Hello '
代码 'Hello'.rjust(10) 表示我们想在长度为 10 的字符串中将 'Hello' 右对齐。'Hello' 是五个字符,所以会在其左侧添加五个空格,得到一个包含 'Hello' 右对齐的 10 个字符的字符串。
rjust() 和 ljust() 方法的可选第二个参数将指定一个除空格字符以外的填充字符。在交互式外壳中输入以下内容:
>>> 'Hello'.rjust(20, '*')
'***Hello'
>>> 'Hello'.ljust(20, '-')
'Hello---------------'
center() 字符串方法的工作方式类似于 ljust() 和 rjust(),但它将文本居中,而不是将其左对齐或右对齐。在交互式外壳中输入以下内容:
>>> 'Hello'.center(20)
' Hello '
>>> 'Hello'.center(20, '=')
'=======Hello========'
现在打印的文本是居中的。
移除空白字符
有时你可能想要从字符串的左侧、右侧或两侧移除空白字符(空格、制表符和换行符)。strip() 字符串方法将返回一个不包含任何空白字符的新字符串(开头或结尾),而 lstrip() 和 rstrip() 方法将分别从左侧和右侧移除空白字符。在交互式外壳中输入以下内容:
>>> spam = ' Hello, World '
>>> spam.strip()
'Hello, World'
>>> spam.lstrip()
'Hello, World '
>>> spam.rstrip()
' Hello, World'
可选地,一个字符串参数将指定要移除的字符串两端的字符。在交互式外壳中输入以下内容:
>>> spam = 'SpamSpamBaconSpamEggsSpamSpam'
>>> spam.strip('ampS')
'BaconSpamEggs'
将 'ampS' 作为参数传递给 strip() 将告诉它从存储在 spam 中的字符串两端移除 a、m、p 和 S 的出现。传递给 strip() 的字符串中字符的顺序无关紧要:strip('ampS') 会做与 strip('mapS') 或 strip('Spam') 相同的事情。
改变大小写
upper() 和 lower() 字符串方法返回一个新字符串,其中原始字符串中的所有字母都转换为大写或小写,分别。字符串中的非字母字符保持不变。例如,在交互式外壳中输入以下内容:
>>> spam = 'Hello, world!'
>>> spam = spam.upper()
>>> spam
'HELLO, WORLD!'
>>> spam = spam.lower()
>>> spam
'hello, world!'
注意,这些方法不会改变字符串本身,而是返回新的字符串值。如果你想改变原始字符串,你必须对字符串调用 upper() 或 lower(),然后将新字符串赋值给存储原始字符串的变量。这就是为什么你必须使用 spam = spam.upper() 来改变 spam 中的字符串,而不是简单地写 spam.upper()。(这和变量 eggs 包含值 10 是一样的。写 eggs + 3 并不会改变 eggs 的值,但 eggs = eggs + 3 会。)
当你需要进行不区分大小写的比较时,upper() 和 lower() 方法非常有用。例如,字符串 'great' 和 'GREat' 并不相等,但在以下简短程序中,用户可以输入 Great、GREAT 或 grEAT,因为代码将字符串转换为小写:
print('How are you?')
feeling = input()
if feeling.lower() == 'great':
print('I feel great too.')
else:
print('I hope the rest of your day is good.')
当你运行此程序时,它会显示一个问题,并且输入 great 的任何变体,例如 GREat,都会输出 I feel great too. 在你的程序中添加代码来处理用户输入的变体或错误,例如不一致的大小写,会使你的程序更容易使用,并且不太可能失败:
How are you?
GREat
I feel great too.
isupper() 和 islower() 方法将返回一个布尔值 True,如果字符串至少有一个字母,并且所有字母都是大写或小写,分别。否则,该方法返回 False。在交互式外壳中输入以下内容,并注意每个方法调用返回的结果:
>>> spam = 'Hello, world!'
>>> spam.islower()
False
>>> spam.isupper()
False
>>> 'HELLO'.isupper()
True
>>> 'abc12345'.islower()
True
>>> '12345'.islower()
False
>>> '12345'.isupper()
False
由于 upper() 和 lower() 字符串方法本身返回字符串,因此你也可以在那些返回的字符串值上调用字符串方法:
>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.upper().lower()
'hello'
>>> 'Hello'.upper().lower().upper()
'HELLO'
>>> 'HELLO'.lower()
'hello'
>>> 'HELLO'.lower().islower()
True
执行此操作的表达式将看起来像一系列方法调用,如下所示。
检查字符串特征
除了 islower() 和 isupper() 之外,还有其他几个以单词 is 开头的字符串方法。这些方法返回一个布尔值,描述字符串的性质。以下是一些常见的 isX() 字符串方法:
isalpha() 返回 True 如果字符串仅由字母组成并且不是空白
isalnum() 返回 True 如果字符串仅由字母和数字(字母数字)组成并且不是空白
isdecimal() 返回 True 如果字符串仅由数字字符组成并且不是空白
isspace() 返回 True 如果字符串仅由空格、制表符和换行符组成并且不是空白
istitle() 返回 True 如果字符串仅由以大写字母开头后跟仅小写字母的单词组成
在交互式壳中输入以下内容:
>>> 'hello'.isalpha()
True
>>> 'hello123'.isalpha()
False
>>> 'hello123'.isalnum()
True
>>> 'hello'.isalnum()
True
>>> '123'.isdecimal()
True
>>> ' '.isspace()
True
>>> 'This Is Title Case'.istitle()
True
isX() 字符串方法在您需要验证用户输入时很有用。例如,以下程序会反复要求用户输入他们的年龄和密码,直到他们提供有效的输入。打开一个新的文件编辑窗口并输入此程序,将其保存为 validateInput.py:
while True:
print('Enter your age:')
age = input()
if age.isdecimal():
break
print('Please enter a number for your age.')
while True:
print('Select a new password (letters and numbers only):')
password = input()
if password.isalnum():
break
print('Passwords can only have letters and numbers.')
在第一个 while 循环中,我们要求用户输入他们的年龄并将他们的输入存储在 age 中。如果 age 是一个有效的(十进制)值,我们就跳出这个第一个 while 循环并继续第二个循环,该循环要求输入密码。否则,我们通知用户他们需要输入一个数字,并再次要求他们输入他们的年龄。在第二个 while 循环中,我们要求输入密码,将用户的输入存储在 password 中,如果输入是字母数字的,就跳出循环。如果不是,我们不满意,因此我们告诉用户密码需要是字母数字的,并再次要求他们输入密码。
当程序运行时,其输出看起来像这样:
Enter your age:
forty two
Please enter a number for your age.
Enter your age:
42
Select a new password (letters and numbers only):
secr3t!
Passwords can only have letters and numbers.
Select a new password (letters and numbers only):
secr3t
通过在变量上调用 isdecimal() 和 isalnum(),我们能够测试存储在那些变量中的值是否为十进制或不是,以及是否为字母数字或不是。在这里,这些测试帮助我们拒绝输入 forty two 但接受 42,拒绝 secr3t! 但接受 secr3t。
检查字符串的开始或结束
startswith() 和 endswith() 方法在它们被调用的字符串值开始或结束(分别)于传递给方法的方法时返回 True;否则,它们返回 False。在交互式壳中输入以下内容:
>>> 'Hello, world!'.startswith('Hello')
True
>>> 'Hello, world!'.endswith('world!')
True
>>> 'abc123'.startswith('abcdef')
False
>>> 'abc123'.endswith('12')
False
>>> 'Hello, world!'.startswith('Hello, world!')
True
>>> 'Hello, world!'.endswith('Hello, world!')
True
这些方法在您只需要检查字符串的第一部分或最后一部分是否与另一个字符串相等,而不是整个字符串相等时,是等于运算符 (==) 的有用替代品。
字符串的连接和分割
当您有一系列需要连接成一个单独的字符串值的字符串时,join() 方法很有用。我们调用字符串上的 join() 方法,并传递一个字符串列表,它返回传入列表中每个字符串的连接。例如,在交互式壳中输入以下内容:
>>> ', '.join(['cats', 'rats', 'bats'])
'cats, rats, bats'
>>> ' '.join(['My', 'name', 'is', 'Simon'])
'My name is Simon'
>>> 'ABC'.join(['My', 'name', 'is', 'Simon'])
'MyABCnameABCisABCSimon'
注意到在调用 join() 的字符串被插入到列表参数中的每个字符串之间。例如,当我们对 ', ' 字符串调用 join(['cats', 'rats', 'bats']) 时,它返回字符串 'cats, rats, bats'。
记住我们是在一个字符串值上调用 join() 并传递一个列表值。(很容易错误地以相反的方式调用它。)split() 方法的工作方式相反:我们调用它在一个字符串值上,它返回一个字符串列表。在交互式外壳中输入以下内容:
>>> 'My name is Simon'.split()
['My', 'name', 'is', 'Simon']
默认情况下,该方法会在找到空格、制表符或换行符等空白字符的地方拆分字符串 'My name is Simon'。这些空白字符不包括在返回列表中的字符串中。你可以向 split() 方法传递一个分隔符字符串来指定不同的字符串进行拆分。例如,在交互式外壳中输入以下内容:
>>> 'MyABCnameABCisABCSimon'.split('ABC')
['My', 'name', 'is', 'Simon']
>>> 'My name is Simon'.split('m')
['My na', 'e is Si', 'on']
split() 的一个常见用途是沿着换行符拆分多行字符串。例如,在交互式外壳中输入以下内容:
>>> spam = '''Dear Alice,
... There is a milk bottle in the fridge
... that is labeled "Milk Experiment."
...
... Please do not drink it.
... Sincerely,
... Bob'''
...
>>> spam.split('\n')
['Dear Alice,', 'There is a milk bottle in the fridge',
'that is labeled "Milk Experiment."', '', 'Please do not drink it.',
'Sincerely,', 'Bob']
将 split() 的参数设置为 '\n' 允许我们沿着新行拆分存储在 spam 中的多行字符串,并返回一个列表,其中每个项目对应字符串的一行。
文本的对齐和居中
rjust() 和 ljust() 字符串方法返回它们被调用的字符串的填充版本,其中插入空格以对齐文本。这两个方法的第一参数是一个整数长度,用于对齐的字符串。在交互式外壳中输入以下内容:
>>> 'Hello'.rjust(10)
' Hello'
>>> 'Hello'.rjust(20)
' Hello'
>>> 'Hello, World'.rjust(20)
' Hello, World'
>>> 'Hello'.ljust(10)
'Hello '
代码 'Hello'.rjust(10) 表示我们想在长度为 10 的字符串中将 'Hello' 右对齐。'Hello' 是五个字符,所以将在其左侧添加五个空格,得到一个包含 'Hello' 右对齐的 10 个字符的字符串。
rjust() 和 ljust() 的可选第二个参数将指定一个除空格字符以外的填充字符。在交互式外壳中输入以下内容:
>>> 'Hello'.rjust(20, '*')
'***Hello'
>>> 'Hello'.ljust(20, '-')
'Hello---------------'
center() 字符串方法与 ljust() 和 rjust() 类似,但它是将文本居中,而不是将其左对齐或右对齐。在交互式外壳中输入以下内容:
>>> 'Hello'.center(20)
' Hello '
>>> 'Hello'.center(20, '=')
'=======Hello========'
现在打印的文本已经居中。
移除空白字符
有时你可能想要从字符串的左侧、右侧或两侧去除空白字符(空格、制表符和换行符)。strip() 字符串方法将返回一个不包含任何空白字符的新字符串,而 lstrip() 和 rstrip() 方法将分别从左侧和右侧移除空白字符。在交互式外壳中输入以下内容:
>>> spam = ' Hello, World '
>>> spam.strip()
'Hello, World'
>>> spam.lstrip()
'Hello, World '
>>> spam.rstrip()
' Hello, World'
可选地,一个字符串参数将指定要去除的末尾字符。在交互式外壳中输入以下内容:
>>> spam = 'SpamSpamBaconSpamEggsSpamSpam'
>>> spam.strip('ampS')
'BaconSpamEggs'
使用 strip() 函数并传入参数 'ampS' 将会指示它从存储在 spam 中的字符串两端移除 a、m、p 和 S 的出现。传递给 strip() 函数的字符串中字符的顺序并不重要:strip('ampS') 会与 strip('mapS') 或 strip('Spam') 做相同的事情。
字符的数值编码点
计算机将信息存储为 字节(二进制数字的字符串),这意味着我们需要能够将文本转换为数字。由于这个要求,每个文本字符都有一个对应的数值,称为 Unicode 编码点。例如,字符 'A' 的数值编码点是 65,'4' 是 52,'!' 是 33。你可以使用 ord() 函数来获取单字符字符串的编码点,使用 chr() 函数来获取整数编码点的单字符字符串。在交互式外壳中输入以下内容:
>>> ord('A')
65
>>> ord('4')
52
>>> ord('!')
33
>>> chr(65)
'A'
当你需要对字符进行排序或执行数学运算时,这些函数很有用:
>>> ord('B')
66
>>> ord('A') < ord('B')
True
>>> chr(ord('A'))
'A'
>>> chr(ord('A') + 1)
'B'
Unicode 和编码点的内容远不止这些,但这些细节超出了本书的范围。如果你想了解更多,我建议观看或阅读 Ned Batchelder 的 2012 年 PyCon 演讲,“Pragmatic Unicode, or How Do I Stop the Pain?”,位于 nedbatchelder.com/text/unipain.html。
当字符串被写入文件或通过互联网发送时,从文本到字节的转换被称为 编码。有几种 Unicode 编码标准,但最流行的是 UTF-8。如果你需要选择 Unicode 编码,99% 的情况下 'utf-8' 是正确的答案。Tom Scott 有一个名为“Characters, Symbols and the Unicode Miracle”的 Computerphile 视频位于 youtu.be/MijmeoH9LT4,它特别解释了 UTF-8。
复制和粘贴字符串
pyperclip 模块包含 copy() 和 paste() 函数,可以将文本发送到你的计算机剪贴板并从剪贴板接收文本。将程序的输出发送到剪贴板将使其容易粘贴到电子邮件、文字处理器或其他软件中。
pyperclip 模块不是 Python 的一部分。要安装它,请遵循附录 A 中安装第三方包的说明。安装 pyperclip 后,在交互式外壳中输入以下内容:
>>> import pyperclip
>>> pyperclip.copy('Hello, world!')
>>> pyperclip.paste()
'Hello, world!'
当然,如果你的程序外部的内容改变了剪贴板的内容,paste() 函数将返回那个其他值。例如,如果我复制了这句话到剪贴板然后调用 paste(),它看起来会是这样:
>>> pyperclip.paste()
'For example, if I copied this sentence to the clipboard and then called
paste(), it would look like this:'
剪贴板是一个极佳的方式,可以在不通过 input() 调用提示输入文本的情况下输入和接收大量文本。例如,假设你想要一个程序将文本转换为交替大小写的字母。你可以将想要交替的文本复制到剪贴板,然后运行此程序,该程序将交替大小写的文本放在剪贴板。将以下代码输入到名为 alternatingText.py 的文件中:
import pyperclip
text = pyperclip.paste() # Get the text off the clipboard.
alt_text = '' # This string holds the alternating case.
make_uppercase = False
for character in text:
# Go through each character and add it to alt_text:
if make_uppercase:
alt_text += character.upper()
else:
alt_text += character.lower()
# Set make_uppercase to its opposite value:
make_uppercase = not make_uppercase
pyperclip.copy(alt_text) # Put the result on the clipboard.
print(alt_text) # Print the result on the screen too.
如果你将一些文本复制到剪贴板(例如,这个句子)并运行此程序,输出和剪贴板内容将变为如下:
iF YoU CoPy sOmE TeXt tO ThE ClIpBoArD (fOr iNsTaNcE, tHiS SeNtEnCe) AnD
RuN ThIs pRoGrAm, ThE OuTpUt aNd cLiPbOaRd cOnTeNtS BeCoMe ThIs:
pyperclip 模块与剪贴板交互的能力为你提供了将文本输入和输出到程序中的简单方法。
项目 2:向维基标记添加项目符号
在编辑维基百科文章时,你可以通过将每个列表项单独放在一行并在其前面放置一个星号来创建一个项目符号列表。但假设你有一个非常大的列表,你想要为其添加项目符号。你可以逐行在每行的开头键入这些星号。或者,你可以使用一个简短的 Python 脚本来自动化这个任务。
bulletPointAdder.py 脚本将从剪贴板获取文本,在每个行的开头添加一个星号和空格,然后将这段新文本粘贴回剪贴板。例如,假设我将以下文本(用于维基百科文章“List of Lists of Lists”)复制到剪贴板:
Lists of animals
Lists of aquarium life
Lists of biologists by author abbreviation
Lists of cultivars
然后,如果我运行 bulletPointAdder.py 程序,剪贴板将包含以下内容:
* Lists of animals
* Lists of aquarium life
* Lists of biologists by author abbreviation
* Lists of cultivars
这个带有星号前缀的文本已准备好粘贴到维基百科文章中作为项目符号列表。
第一步:从剪贴板复制和粘贴
你希望 bulletPointAdder.py 程序执行以下操作:
-
从剪贴板粘贴文本。
-
对其进行一些操作。
-
将新文本复制到剪贴板。
操作文本有点棘手,但复制和粘贴相当直接:它们只涉及 pyperclip.copy() 和 pyperclip.paste() 函数。现在,让我们编写调用这些函数的程序部分。输入以下内容,将程序保存为 bulletPointAdder.py:
import pyperclip
text = pyperclip.paste()
# TODO: Separate lines and add stars.
pyperclip.copy(text)
TODO 注释是一个提醒,你应该最终完成程序的这个部分。下一步是实际实现这个程序片段。
第二步:分离文本行
pyperclip.paste() 的调用返回剪贴板上的所有文本作为一个大字符串。如果我们使用“List of Lists of Lists”示例,存储在 text 中的字符串将看起来像这样:
'Lists of animals\nLists of aquarium life\nLists of biologists by author
abbreviation\nLists of cultivars'
这个字符串中的 \n 换行符会导致打印或从剪贴板粘贴时显示为多行。这个字符串值中有很多“行”。你想要在每个这些行的开头添加一个星号。
你可以编写代码来搜索字符串中的每个 \n 换行符,然后在之后添加星号。但使用 split() 方法来返回一个字符串列表,每个字符串对应原始字符串中的一行,然后在列表中的每个字符串前面添加星号会更简单。
修改你的程序,使其看起来如下所示:
import pyperclip
text = pyperclip.paste()
# Separate lines and add stars.
lines = text.split('\n')
for i in range(len(lines)): # Loop through all indexes in the "lines" list.
lines[i] = '* ' + lines[i] # Add a star to each string in the "lines" list.
pyperclip.copy(text)
我们沿着文本的换行符分割文本,以获得一个列表,其中每个项目是文本的一行。我们将列表存储在 lines 中,然后遍历 lines 中的项目。对于每一行,我们在行的开头添加一个星号和一个空格。现在 lines 中的每个字符串都以星号开头。
第 3 步:连接修改后的行
lines 列表现在包含以星号开头的修改后的行。但 pyperclip.copy() 期望一个单个字符串值,而不是字符串值的列表。为了得到这个单个字符串值,将 lines 传递给 join() 方法,以从列表的字符串中获取一个单一的字符串:
import pyperclip
text = pyperclip.paste()
# Separate lines and add stars.
lines = text.split('\n')
for i in range(len(lines)): # Loop through all indexes in the "lines" list.
lines[i] = '* ' + lines[i] # Add a star to each string in the "lines" list.
text = '\n'.join(lines)
pyperclip.copy(text)
当这个程序运行时,它会将剪贴板上的文本替换为每行开头都有星号的文本。现在程序已经完成,你可以尝试使用复制到剪贴板的文本运行它。
即使你不需要自动化这个特定的任务,你也可能想要自动化其他类型的文本操作,例如从行尾删除尾随空格或将文本转换为大写或小写。无论你的需求是什么,你都可以使用剪贴板作为输入和输出。
第 1 步:从剪贴板复制和粘贴
你希望 bulletPointAdder.py 程序执行以下操作:
-
从剪贴板粘贴文本。
-
对其进行一些操作。
-
将新文本复制到剪贴板。
操作文本有点棘手,但复制和粘贴相当直接:它们只涉及 pyperclip.copy() 和 pyperclip.paste() 函数。现在,让我们编写调用这些函数的程序部分。输入以下内容,将程序保存为 bulletPointAdder.py:
import pyperclip
text = pyperclip.paste()
# TODO: Separate lines and add stars.
pyperclip.copy(text)
TODO 注释是一个提醒,你应该最终完成这个程序的这部分。下一步是实际实现这个程序片段。
第 2 步:分离文本行
pyperclip.paste() 的调用返回剪贴板上的所有文本作为一个大字符串。如果我们使用“列表的列表的列表”示例,存储在 text 中的字符串将看起来像这样:
'Lists of animals\nLists of aquarium life\nLists of biologists by author
abbreviation\nLists of cultivars'
这个字符串中的 \n 换行符导致它在打印或从剪贴板粘贴时显示为多行。这个字符串值中有许多“行”。你想要在每个这些行的开头添加一个星号。
你可以编写代码来搜索字符串中的每个 \n 换行符,然后在之后添加星号。但使用 split() 方法来返回一个字符串列表,每个字符串对应原始字符串中的一行,然后添加星号到列表中的每个字符串前面会更简单。
修改你的程序,使其看起来如下所示:
import pyperclip
text = pyperclip.paste()
# Separate lines and add stars.
lines = text.split('\n')
for i in range(len(lines)): # Loop through all indexes in the "lines" list.
lines[i] = '* ' + lines[i] # Add a star to each string in the "lines" list.
pyperclip.copy(text)
我们将文本按其换行符分割,得到一个列表,其中每个项目是文本的一行。我们将列表存储在 lines 中,然后遍历 lines 中的项目。对于每一行,我们在行的开头添加一个星号和一个空格。现在 lines 中的每个字符串都以星号开头。
第 3 步:连接修改后的行
lines 列表现在包含以星号开头的修改后的行。但是 pyperclip.copy() 期望一个单一的字符串值,而不是字符串值的列表。为了得到这个单一的字符串值,将 lines 传递给 join() 方法,以从列表的字符串中得到一个单一的字符串:
import pyperclip
text = pyperclip.paste()
# Separate lines and add stars.
lines = text.split('\n')
for i in range(len(lines)): # Loop through all indexes in the "lines" list.
lines[i] = '* ' + lines[i] # Add a star to each string in the "lines" list.
text = '\n'.join(lines)
pyperclip.copy(text)
当这个程序运行时,它会将剪贴板上的文本替换为每行开头带有星号的文本。现在程序就完成了,你可以尝试使用剪贴板上的文本运行它。
即使你不需要自动化这个特定的任务,你也可能想要自动化其他类型的文本操作,例如从行尾删除尾随空格或将文本转换为大写或小写。无论你的需求是什么,你都可以使用剪贴板作为输入和输出。
简短程序:猪拉丁语
猪拉丁语是一种愚蠢的虚构语言,它改变了英语单词。如果一个单词以元音开头,就在它的末尾添加 yay。如果一个单词以辅音或辅音群(如 ch 或 gr)开头,那么这个辅音或辅音群就被移动到单词的末尾,并跟着 ay。
让我们编写一个猪拉丁语程序,它会输出类似以下的内容:
Enter the English message to translate into pig latin:
My name is AL SWEIGART and I am 4,000 years old.
Ymay amenay isyay ALYAY EIGARTSWAY andyay Iyay amyay 4,000 yearsyay oldyay.
这个程序通过使用本章介绍的方法来修改字符串来工作。将以下源代码输入到文件编辑器中,并将文件保存为 pigLat.py:
# English to pig latin
print('Enter the English message to translate into pig latin:')
message = input()
VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')
pig_latin = [] # A list of the words in pig latin
for word in message.split():
# Separate the non-letters at the start of this word:
prefix_non_letters = ''
while len(word) > 0 and not word[0].isalpha():
prefix_non_letters += word[0]
word = word[1:]
if len(word) == 0:
pig_latin.append(prefix_non_letters)
continue
# Separate the non-letters at the end of this word:
suffix_non_letters = ''
while not word[-1].isalpha():
suffix_non_letters = word[-1] + suffix_non_letters
word = word[:-1]
# Remember if the word was in uppercase or title case:
was_upper = word.isupper()
was_title = word.istitle()
word = word.lower() # Make the word lowercase for translation.
# Separate the consonants at the start of this word:
prefix_consonants = ''
while len(word) > 0 and not word[0] in VOWELS:
prefix_consonants += word[0]
word = word[1:]
# Add the pig latin ending to the word:
if prefix_consonants != '':
word += prefix_consonants + 'ay'
else:
word += 'yay'
# Set the word back to uppercase or title case:
if was_upper:
word = word.upper()
if was_title:
word = word.title()
# Add the non-letters back to the start or end of the word.
pig_latin.append(prefix_non_letters + word + suffix_non_letters)
# Join all the words back together into a single string:
print(' '.join(pig_latin))
让我们逐行查看这段代码,从顶部开始:
# English to pig latin
print('Enter the English message to translate into pig latin:')
message = input()
VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')
首先,我们要求用户输入要翻译成猪拉丁语的英文文本。同时,我们创建一个常量,它包含所有小写元音(以及 y)作为一个字符串元组。我们稍后会使用这个变量。
接下来,我们创建一个名为 pig_latin 的变量来存储我们将其翻译成猪拉丁语的单词:
pig_latin = [] # A list of the words in pig latin
for word in message.split():
# Separate the non-letters at the start of this word:
prefix_non_letters = ''
while len(word) > 0 and not word[0].isalpha():
prefix_non_letters += word[0]
word = word[1:]
if len(word) == 0:
pig_latin.append(prefix_non_letters)
continue
我们需要每个单词都是自己的字符串,所以我们调用 message.split() 来获取单词作为单独字符串的列表。字符串 'My name is AL SWEIGART and I am 4,000 years old.' 将导致 split() 返回 ['My', 'name', 'is', 'AL', 'SWEIGART', 'and', 'I', 'am', '4,000', 'years', 'old.']。
我们还需要从每个单词的开头和结尾删除任何非字母字符,以便像 'old.' 这样的字符串翻译成 'oldyay.' 而不是 'old.yay'。我们将这些非字母字符保存到名为 prefix_non_letters 的变量中。
# Separate the non-letters at the end of this word:
suffix_non_letters = ''
while not word[-1].isalpha():
suffix_non_letters += word[-1] + suffix_non_letters
word = word[:-1]
一个调用 isalpha() 的循环,它检查单词中的第一个字符,以确定我们是否应该从单词中移除一个字符并将其连接到 prefix_non_letters 的末尾。如果整个单词由非字母字符组成,如 '4,000',我们可以简单地将其附加到 pig_latin 列表中,并继续翻译下一个单词。我们还需要保存 word 字符串末尾的非字母字符。此代码与前面的循环类似。
接下来,我们确保程序记住单词是否为大写或标题样式,这样我们就可以在将单词翻译成猪拉丁语后将其恢复:
# Remember if the word was in uppercase or title case:
was_upper = word.isupper()
was_title = word.istitle()
word = word.lower() # Make the word lowercase for translation.
在 for 循环的其余代码中,我们将处理 word 的小写版本。
要将像 sweigart 这样的单词转换为 eigart-sway,我们需要从 word 的开头移除所有的辅音:
# Separate the consonants at the start of this word:
prefix_consonants = ''
while len(word) > 0 and not word[0] in VOWELS:
prefix_consonants += word[0]
word = word[1:]
我们使用一个与从 word 的开头移除非字母字符的循环类似的循环,但现在我们正在移除辅音并将它们存储在一个名为 prefix_consonants 的变量中。
如果单词的开头有任何辅音,它们现在都在 prefix_consonants 中,我们应该将这个变量和字符串 'ay' 连接到 word 的末尾。否则,我们可以假设 word 以元音开头,我们只需要连接 'yay':
# Add the pig latin ending to the word:
if prefix_consonants != '':
word += prefix_consonants + 'ay'
else:
word += 'yay'
回想一下,我们使用 word = word.lower() 将 word 设置为其小写版本。如果 word 最初是大写或标题样式,此代码将 word 转换回其原始格式:
# Set the word back to uppercase or title case:
if was_upper:
word = word.upper()
if was_title:
word = word.title()
在 for 循环的末尾,我们将单词及其原始的任何非字母前缀或后缀附加到 pig_latin 列表中:
# Add the non-letters back to the start or end of the word.
pig_latin.append(prefix_non_letters + word + suffix_non_letters)
# Join all the words back together into a single string:
print(' '.join(pig_latin))
在这个循环完成后,我们通过调用 join() 方法将字符串列表合并成一个单独的字符串,然后将这个单独的字符串传递给 print() 来在屏幕上显示我们的猪拉丁语。
摘要
文本是常见的数据形式,Python 提供了许多有用的字符串方法来处理存储在字符串值中的文本。你将在你编写的几乎每一个 Python 程序中使用索引、切片和字符串方法。
你现在编写的程序看起来并不太复杂;它们没有带有图像和彩色文本的图形用户界面(GUI)。到目前为止,你使用 print() 显示文本,并使用 input() 让用户输入文本。然而,用户可以通过剪贴板快速输入大量文本。这种能力为编写操作大量文本的程序提供了一个有用的途径。这些基于文本的程序可能没有闪亮的窗口或图形,但它们可以快速完成大量有用的工作。
另一种操作大量文本的方法是直接从硬盘驱动器读取和写入文件。你将在第十章学习如何使用 Python 来做这件事。
那就差不多涵盖了 Python 编程的所有基本概念!你将在本书的其余部分继续学习新的概念,但现在你已经足够了解,可以开始编写一些有用的程序来自动化任务。如果你想看到由你迄今为止学到的基本概念构建的短小简单的 Python 程序的集合,你可以阅读我的另一本书,《Python 小项目大全书》(No Starch Press,2021)。尝试手动复制每个程序的字源代码,然后进行修改以查看它们如何影响程序的行为。一旦你理解了程序的工作原理,尝试从头开始重新创建程序。你不需要精确地重新创建源代码;只需关注程序做什么,而不是它是如何做的。
你可能认为你没有足够的 Python 知识来做诸如下载网页、更新电子表格或发送短信之类的事情,但这就是 Python 模块的作用!这些由其他程序员编写的模块提供了使你能够轻松完成所有这些事情的功能。在下一章中,你将学习如何编写用于执行有用自动化任务的真正程序。
练习问题
-
转义字符是什么?
-
\n和\t转义字符代表什么? -
你如何在字符串中放置反斜杠字符
\? -
字符串值
"Howl's Moving Castle"是一个有效的字符串。为什么单词Howl's中的单引号字符没有转义不是问题? -
如果你不想在字符串中放置
\n,你该如何编写包含换行符的字符串? -
这些表达式评估结果是什么?
-
'Hello, world!'[1] -
'Hello, world!'[0:5] -
'Hello, world!'[:5] -
'Hello, world!'[3:]
- 这些表达式评估结果是什么?
-
'Hello'.upper() -
'Hello'.upper().isupper() -
'Hello'.upper().lower()
- 这些表达式评估结果是什么?
-
'Remember, remember, the fifth of November.'.split() -
'-'.join('There can be only one.'.split())
-
你可以使用哪些字符串方法来实现字符串的右对齐、左对齐和居中对齐?
-
你如何从字符串的开始或结束处删除空白字符?
练习程序:表格打印器
为了练习,编写一个名为 printTable() 的函数,该函数接受一个字符串列表的列表,并以每列右对齐的方式显示一个组织良好的表格。假设所有内部列表都将包含相同数量的字符串。例如,值可能看起来像这样:
tableData = [['apples', 'oranges', 'cherries', 'banana'],
['Alice', 'Bob', 'Carol', 'David'],
['dogs', 'cats', 'moose', 'goose']]
你的 printTable() 函数将打印以下内容:
apples Alice dogs
oranges Bob cats
cherries Carol moose
banana David goose
提示:你的代码首先需要找到每个内部列表中最长的字符串,以便整个列足够宽,可以容纳所有字符串。你可以将每列的最大宽度存储为一个整数列表。printTable() 函数可以从 colWidths = [0] * len(tableData) 开始,这将创建一个包含与 tableData 中内部列表数量相同数量的 0 值的列表。这样,colWidths[0] 可以存储 tableData[0] 中最长字符串的宽度,colWidths[1] 可以存储 tableData[1] 中最长字符串的宽度,依此类推。然后你可以找到 colWidths 列表中的最大值,以确定传递给 rjust() 字符串方法的整数宽度。


浙公网安备 33010602011771号