Automate the Boring Stuff with Python: Practical Programming for Total Beginners (2nd Edition)
Written by Al Sweigart.
The second edition is available on 2019.10.29
4.1 列表数据类型
列表 (list) 是一个值,在有序序列中包含多个值。术语列表值 (list value) 指的是列表本身 (该值可以存储在变量中,也可以像其他值一样,可以传递给函数),而不是列表值中的值。
列表以左方括号开始,以右方括号结束,即 []。列表中的值也称为表项 (item)。表项之间用逗号分隔 (也就是说,它们是逗号分隔的 (comma-delimited))。
spam = ['cat', 'bat', 'rat', 'elephant']
用下标取得列表中的单个值
print('The ' + spam[1] + ' ate ' + spam[0] + '.') # 打印 The bat ate cat.
使用的下标不能超出列表中值的个数。下标只能是整数,不能是浮点值。
列表也可以包含其他列表值。这些列表的列表中的值,可以通过多重下标来访问:
spam = [['cat', 'bat'], [10, 20, 30, 40, 50]]
print(spam[0]) # 打印 ['cat', 'bat']
print(spam[1][4]) # 打印 50
负数下标
整数值 −1 指的是列表中的最后一个下标,−2 指的是列表中倒数第二个下标,以此类推。
spam = ['cat', 'bat', 'rat', 'elephant']
print('The ' + spam[-1] + ' is afraid of the ' + spam[-3] + '.') # 打印 The elephant is afraid of the bat.
利用切片取得子列表
切片(slice)可以以新列表的形式从列表中取得多个值。切片输入在一对方括号中,像下标一样,但它有两个冒号分隔的整数。
在一个切片中,第一个整数是切片开始处的下标。第二个整数是切片结束处的下标。切片向上增长,直至第二个下标的值,但不包括它。
spam = ['cat', 'bat', 'rat', 'elephant']
print(spam[0:4]) # 打印 ['cat', 'bat', 'rat', 'elephant']
print(spam[1:3]) # 打印 ['bat', 'rat']
print(spam[0:-1]) # 打印 ['cat', 'bat', 'rat']
作为一种快捷方式,可以在切片中忽略冒号两边的一个或两个下表。省略第一个下表与使用 0 或列表的开头相同。省略第二个下表与使用列表的长度相同,它将切片到列表的末尾。
print(spam[:2]) # 打印 ['cat', 'bat']
print(spam[1:]) # 打印 ['bat', 'rat', 'elephant']
print(spam[:]) # 打印 ['cat', 'bat', 'rat', 'elephant']
用 len() 取得列表的长度
print(len(spam)) # 打印 4
用下标改变列表中的值
spam = ['cat', 'bat', 'rat', 'elephant']
spam[1] = 'aardvark'
print(spam) # 打印 ['cat', 'aardvark', 'rat', 'elephant']
spam[2] = spam[1]
print(spam) # 打印 ['cat', 'aardvark', 'aardvark', 'elephant']
spam[-1] = 12345
print(spam) # 打印 ['cat', 'aardvark', 'aardvark', 12345]
列表连接和列表复制
操作符 +
可以连接两个列表,得到一个新列表。操作符 *
可以用于一个列表和一个整数,实现列表的复制。
print([1, 2, 3] + ['A', 'B', 'C']) # 打印 [1, 2, 3, 'A', 'B', 'C']
print(['X', 'Y', 'Z'] * 3) # 打印 ['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z']
用 del 语句从列表中删除值
del 语句将删除列表中下表处的值。删除值之后列表中的所有值将向前移动一个下标。
spam = ['cat', 'bat', 'rat', 'elephant']
del spam[2]
print(spam) # 打印 ['cat', 'bat', 'elephant']
del 语句也可用于删除一个简单变量,作用就像是“取消赋值”语句。如果在删除之后试图使用该变量,就会遇到 NameError 错误,因为该变量已不再存在。
在实践中,基本上不需要删除简单变量。del 语句几乎总是用于删除列表中的值。
4.2 使用列表
# 使用列表,可以保存用户输入的任意多的猫
catNames = []
while True:
print('Enter the name of cat ' + str(len(catNames) + 1) + ' (Or enter nothing to stop.):')
name = input()
if name == '':
break
catNames = catNames + [name] # 列表连接
print('The cat names are:')
for name in catNames:
print(' ' + name)
列表用于循环
for i in [0, 1, 2, 3]:
print(i)
注意:在本书中使用术语类似列表(list-like),来指技术上称为序列(sequences)的数据类型。
一种常见的 Python 技术,是在 for 循环中使用使用 range(len(someList)) 来遍历列表的下标。
supplies = ['pens', 'staplers', 'flame-throwers', 'binders']
for i in range(len(supplies)):
print('Index ' + str(i) + ' in supplies is: ' + supplies[i])
in 和 not in 操作符
# 检查输入的宠物名字是否在列表中
myPets = ['Zophie', 'Pooka', 'Fat-tail']
print('Enter a pet name:')
name = input()
if name not in myPets:
print('I do not have a pet named ' + name)
else:
print(name + ' is my pet.')
多重赋值技巧
cat = ['fat', 'orange', 'loud']
size, color, disposition = cat
变量的数量和列表的长度必须完全相等,否则 Python 会给出错误 ValueError。
多重赋值技巧也可以用来交换两个变量中的值:
a, b = 'Alice', 'Bob'
a, b = b, a # 交换两个变量中的值
print(a) # 打印 'Bob'
print(b) # 打印 'Alice'
使用 enumerate() 函数
在循环的每次迭代中,enumerate()
将返回两个值:列表中项目的索引和列表本身中的项目。
>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for index, item in enumerate(supplies):
... print('Index ' + str(index) + ' in supplies is: ' + item)
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders
使用 random.choice() 函数和 random.shuffle() 函数
random.choice()
函数将从列表中返回一个随机选择的项目。
>>> import random
>>> pets = ['Dog', 'Cat', 'Moose']
>>> random.choice(pets)
'Dog'
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Cat'
可以认为 random.choice(someList)
是 someList[random.randint(0, len(someList)– 1])
的较短形式。
random.shuffle()
函数将对列表中的项目重新排序。该函数在原地修改列表,而不是返回新列表。
>>> import random
>>> people = ['Alice', 'Bob', 'Carol', 'David']
>>> random.shuffle(people)
>>> people
['Carol', 'David', 'Alice', 'Bob']
>>> random.shuffle(people)
>>> people
['Alice', 'David', 'Bob', 'Carol']
4.3 增强赋值操作符
表4-1 增强的赋值操作符
增强的赋值语句 | 等价的赋值语句 |
---|---|
spam += 1 | spam = spam + 1 |
spam -= 1 | spam = spam - 1 |
spam *= 1 | spam = spam * 1 |
spam /= 1 | spam = spam / 1 |
spam %= 1 | spam = spam % 1 |
操作符 += 还可以完成字符串和列表的连接,操作符 *= 可以完成字符串和列表的复制。
spam = 'Hello'
spam += ' world!'
print(spam) # 打印 'Hello world!'
bacon = ['Zophie']
bacon *= 3
print(bacon) # 打印 ['Zophie', 'Zophie', 'Zophie']
4.4 方法
方法(method)和函数是一样的,不同的是它被一个值“调用”。方法部分位于值之后,以句点分隔。
每种数据类型都有自己的一组方法。例如,列表数据类型有一些有用的方法,来查找、添加、删除和操作列表中的值。
使用 index() 方法在列表中查找值
列表值有一个 index() 方法,可以传入一个值,如果该值存在于列表中,就返回它的下标。如果该值不在列表中,Python 就报 ValueError。
spam = ['hello', 'hi', 'howdy', 'heyas']
print(spam.index('heyas')) # 打印 3
print(spam.index('howdy howdy howdy')) # 报错:ValueError: 'howdy howdy howdy' is not in list
当列表中有重复的值时,返回它第一次出现的下标。
spam = ['Zophie', 'Pooka', 'Fat-tail', 'Pooka']
print(spam.index('Pooka')) # 打印 1
使用 append() 和 insert() 方法向列表添加值
append() 方法将参数添加到列表的末尾。
insert() 方法可以在列表中的任意下标处插入一个值。insert() 的第一个参数是新值的下标,第二个参数是要插入的新值。
spam = ['cat', 'dog', 'bat']
spam.append('moose')
print(spam) # 打印 ['cat', 'dog', 'bat', 'moose']
spam.insert(1, 'chicken')
print(spam) # 打印 ['cat', 'chicken', 'dog', 'bat', 'moose']
方法属于单一数据类型。append() 和 insert() 方法是列表方法,只能对列表值调用,而不能对字符串或整数等其他值调用;否则,Python 会给出 AttributeError 错误消息。
使用 remove() 方法从列表中删除值
spam = ['cat', 'bat', 'rat', 'elephant']
spam.remove('bat')
print(spam) # 打印 ['cat', 'rat', 'elephant']
试图删除列表中不存在的值将导致 ValueError 错误。
spam.remove('chicken') # 报错:ValueError: list.remove(x): x not in list
如果该值在列表中出现多次,则仅删除该值的第一个实例。
spam = ['cat', 'bat', 'rat', 'cat', 'hat', 'cat']
spam.remove('cat')
print(spam) # 打印 ['bat', 'rat', 'cat', 'hat', 'cat']
如果知道要从列表中删除的值的索引,可以使用 del
语句。如果知道要从列表中删除的值,使用 remove()
方法。
使用 sort() 方法对列表中的值进行排序
可以使用 sort() 方法对数值列表或字符串列表进行排序。
spam = [2, 5, 3.14, 1, -7]
spam.sort()
print(spam) # 打印 [-7, 1, 2, 3.14, 5]
spam = ['ants', 'cats', 'dogs', 'badgers', 'elephants']
spam.sort()
print(spam) # 打印 ['ants', 'badgers', 'cats', 'dogs', 'elephants']
可以传递 True 给 reverse 关键字参数,使 sort() 按逆序排序。
spam.sort(reverse=True)
print(spam) # 打印 ['elephants', 'dogs', 'cats', 'badgers', 'ants']
无法对同时包含数字值和字符串值的列表进行排序,因为 Python 不知道如何比较这些值。
spam = [1, 3, 2, 4, 'Alice', 'Bob']
spam.sort() # 报错:TypeError: unorderable types: str() < int()
在 Python2 中运行上面的代码,没有报错,得到结果 spam = [1, 2, 3, 4, ‘Alice’, ‘Bob’]
sort()
对字符串排序使用 “ASCII 字符顺序”,而不是实际的字母顺序。这意味着大写字母在小写字母之前。
spam = ['Alice', 'ants', 'Bob', 'badgers', 'Carol', 'cats']
spam.sort()
print(spam) # 打印 ['Alice', 'Bob', 'Carol', 'ants', 'badgers', 'cats']
如果需要按字母顺序对值进行排序,将关键字参数 key
设置为 str.lower
。
spam = ['a', 'z', 'A', 'Z']
spam.sort(key=str.lower)
print(spam) # 打印 ['a', 'A', 'z', 'Z']
这将导致 sort() 方法将列表中的所有项视为小写,但实际上不更改列表中的值。
使用 reverse() 方法反转列表中的值
>>> spam = ['cat', 'dog', 'moose']
>>> spam.reverse()
>>> spam
['moose', 'dog', 'cat']
4.5 示例程序:魔术 8 球与列表
import random
messages = ['It is certain',
'It is decidedly so',
'Yes definitely',
'Reply hazy try again',
'Ask again later',
'Concentrate and ask again',
'My reply is no',
'Outlook not so good',
'Very doubtful']
print(messages[random.randint(0, len(messages) - 1)])
Python 中缩进规则的例外
在大多数情况下,一行代码的缩进量告诉 Python 它在哪个块中。不过,这条规则也有一些例外。例如,列表实际上可以跨越源代码文件中的几行。这些行的缩进并不重要。Python知道,直到看到结束方括号时,列表才算完成。
大多数人利用 Python 的行为,使他们的列表看起来漂亮易读,就像神奇 8 球程序中的消息列表一样。
也可以在行末使用续行字符 \
,将一条指令写成多行。可以把 \
看成是“这条指令在下一行继续”。\
续行字符之后的一行中,缩进并不重要。
print('Four score and seven ' + \
'years ago...')
4.6 类似列表的类型:字符串和元组
列表并不是唯一表示序列值的数据类型。例如,若将字符串视为单个文本字符的 “列表”,则字符串和列表实际上是相似的。对列表的许多操作,也可以作用于字符串:按下标取值、切片、用于 for 循环、用于 len(),以及用于 in 和 not in 操作符。
name = 'Zophie'
print(name[0]) # 打印 'Z'
print(name[-2]) # 打印 'i'
print(name[0:4]) # 打印 'Zoph'
print(len(name)) # 打印 6
print('Zo' in name) # 打印 True
print('p' not in name) # 打印 False
for i in name: # i 的取值依次为 'Z', 'o', 'p', 'h', 'i', 'e'
print('* * * ' + i + ' * * *')
可变和不可变数据类型
但是列表和字符串在一个重要的方面是不同的。列表值是一种可变的(mutable)数据类型:它可以添加、删除或更改值。然而,字符串是不可变的(immutable):它不能被改变。尝试对字符串中的一个字符重新赋值,会导致 TypeError 错误。
name = 'Zophie a cat'
name[7] = 'the' # 报错:TypeError: 'str' object does not support item assignment
“改变”字符串的正确方法,是使用切片和连接,通过复制旧字符串的部分来构建新字符串。
name = 'Zophie a cat'
newName = name[0:7] + 'the' + name[8:12]
print(name) # 打印 'Zophie a cat'
print(newName) # 打印 'Zophie the cat'
重新赋值并不能修改原来的列表。只是新的列表值覆盖了旧的列表值。
eggs = [1, 2, 3]
print(id(eggs))
eggs = [4, 5, 6]
print(id(eggs))
运行结果可能为:
140643064299336
140643064316040
两者的内存地址并不相同,表示名称为 eggs 的列表是两个不同的列表。
修改 eggs 中原来的列表,让它包含 [4, 5, 6]:
eggs = [1, 2, 3]
print(id(eggs))
del eggs[2]
del eggs[1]
del eggs[0]
eggs.append(4)
eggs.append(5)
eggs.append(6)
print(id(eggs))
运行结果可能为:
140239384989512
140239384989512
两者的内存地址相同,表示名称为 eggs 的列表是同一个。
元组数据类型
元组输入时用圆括号 ()
。
eggs = ('hello', 42, 0.5)
print(eggs[0]) # 打印 'hello'
print(eggs[1:3]) # 打印 (42, 0.5)
print(len(eggs)) # 打印 3
元组像字符串一样,是不可变的。元组的值不能被修改、添加或删除。否则会导致 TypeError 错误。
如果元组中只有一个值,可以通过在圆括号内的值后面加上逗号来表示。否则,Python 会认为您只是在常规括号内键入了一个值。逗号让 Python 知道这是一个元组值。(与其他一些编程语言不同,在 Python 中,在列表或元组的最后一项后面加上逗号是可以的。)
>>> type(('hello',))
<class 'tuple'>
>>> type(('hello'))
<class 'str'>
可以用元组告诉所有读代码的人,不打算改变这个序列的值。
因为元组是不可变的,所以 Python 可以实现一些优化,使得使用元组的代码比使用列表的代码稍微快一些。
使用 list() 和 tuple() 函数来转换类型
在 IDLE 中输入下面内容:
>>> tuple(['cat', 'dog', 5])
('cat', 'dog', 5)
>>> list(('cat', 'dog', 5))
['cat', 'dog', 5]
>>> list('hello')
['h', 'e', 'l', 'l', 'o']
4.7 引用
a = 10
b = a
print(id(a))
print(id(b))
a = 12
print(id(a))
print(b)
上面的代码运行的一种结果为:
10853920
10853920
10853984
10
将 a 赋值给 b 时,a 和 b 指向同一个内存地址。当运行到 a = 12 时,a 指向了另一个内存地址。b 不变。
将一个列表赋值给一个变量时,实际上是将一个列表引用(reference)赋值给该变量。引用是指向一些数据的值,列表引用是指向列表的值。
spam = [0, 1, 2, 3, 4, 5]
cheese = spam
print(id(spam))
print(id(cheese))
cheese[1] = 'Hello!'
print(spam) # 打印 [0, 'Hello!', 2, 3, 4, 5]
print(cheese) # 打印 [0, 'Hello!', 2, 3, 4, 5]
print(id(spam))
print(id(cheese))
上面的代码运行的一种结果为:
139703613665096
139703613665096
[0, 'Hello!', 2, 3, 4, 5]
[0, 'Hello!', 2, 3, 4, 5]
139703613665096
139703613665096
列表变量 spam 和 cheese 指向内存地址相同,且没有改变。两者指向同一个列表,列表本身实际从未复制。
传递引用
def eggs(someParameter):
someParameter.append('Hello')
spam = [1, 2, 3]
eggs(spam)
print(spam) # 打印 [1, 2, 3, 'Hello']
尽管 spam 和 someParameter 包含不同的引用,但它们都指向同一个列表。这就是为什么函数内部的append(‘Hello’) 方法调用即使在函数调用返回之后也会影响列表的原因。
请记住这种行为:忘记 Python 以这种方式处理列表和字典变量,可能会导致令人困惑的 bug。
copy 模块的 copy() 和 deepcopy() 函数
copy.copy()
可以用来复制可变值,比如列表或字典,而不仅仅是复制引用。
import copy
spam = ['A', 'B', 'C', 'D']
cheese = copy.copy(spam)
print(id(spam))
print(id(cheese))
cheese[1] = 42
print(spam) # ['A', 'B', 'C', 'D']
print(cheese) # ['A', 42, 'C', 'D']
上面的代码运行的一种结果是:
140716989992840
140717008873672
['A', 'B', 'C', 'D']
['A', 42, 'C', 'D']
列表变量 spam 和 cheese 指向不同的内存地址,指向不同的列表。
如果需要复制的列表包含列表,那么使用 copy.deepcopy()
函数代替 copy.copy()
。deepcopy()
函数将同时复制它们内部的列表。
import copy
list1 = [1, 2, [3, 4]]
list2 = copy.copy(list1)
list3 = copy.deepcopy(list1)
list1[2][0] = 7
print(list1)
print(list2)
print(list3)
print(id(list1[2]))
print(id(list2[2]))
print(id(list3[2]))
上面的代码运行的结果为:
[1, 2, [7, 4]]
[1, 2, [7, 4]]
[1, 2, [3, 4]]
139733085488008
139733085488008
139733085488072
4.8 康威的生命游戏
# conway.py - Conway's Game of Life
import random, time, copy
WIDTH = 60
HEIGHT = 20
# Create a list of list for the cells:
nextCells = []
for x in range(WIDTH):
column = [] # Create a new column.
for y in range(HEIGHT):
if random.randint(0, 1) == 0:
column.append('#') # Add a living cell.
else:
column.append(' ') # Add a dead cell.
nextCells.append(column) # nextCells is a list of column lists.
while True: # Main program loop.
print('\n\n\n\n\n') # Separate each step with newlines.
currentCells = copy.deepcopy(nextCells)
# Print currentCells on the screen:
for y in range(HEIGHT):
for x in range(WIDTH):
print(currentCells[x][y], end='') # Print the # or space.
print() # Print a newline at the end of the row.
# Calculate the next step's cells based on current step's cells:
for x in range(WIDTH):
for y in range(HEIGHT):
# Get neighboring coordinates:
# `% WIDTH` ensures leftCoord is always between 0 and WIDTH - 1
leftCoord = (x - 1) % WIDTH
rightCoord = (x + 1) % WIDTH
aboveCoord = (y - 1) % HEIGHT
belowCoord = (y + 1) % HEIGHT
# Count number of living neighbors:
numNeighbors = 0
if currentCells[leftCoord][aboveCoord] == '#':
numNeighbors += 1 # Top-left neighbor is alive.
if currentCells[x][aboveCoord] == '#':
numNeighbors += 1 # Top neighbor is alive.
if currentCells[rightCoord][aboveCoord] == '#':
numNeighbors += 1 # Top-right neighbor is alive.
if currentCells[leftCoord][y] == '#':
numNeighbors += 1 # Left neighbor is alive.
if currentCells[rightCoord][y] == '#':
numNeighbors += 1 # Right neighbor is alive.
if currentCells[leftCoord][belowCoord] == '#':
numNeighbors += 1 # Bottom-left neighbor is alive.
if currentCells[x][belowCoord] == '#':
numNeighbors += 1 # Bottom neighbor is alive.
if currentCells[rightCoord][belowCoord] == '#':
numNeighbors += 1 # Bottom-right neighbor is alive.
# Set cell based on Conway's Game of Life rules:
if currentCells[x][y] == '#' and (numNeighbors == 2 or numNeighbors == 3):
# Living cells with 2 or 3 neighbors stay alive:
nextCells[x][y] = '#'
elif currentCells[x][y] == ' ' and numNeighbors == 3:
# Dead cells with 3 neighbors become alive:
nextCells[x][y] = '#'
else:
# Everything else dies or stays dead:
nextCells[x][y] = ' '
time.sleep(1) # Add a 1-second pause to reduce flickering.
4.9 实践项目
逗号代码
假设有一个这样的列表值:
spam = [‘apples’, ‘bananas’, ‘tofu’, ‘cats’]
编写一个函数,该函数接受一个列表值作为参数,并返回一个字符串,该字符串包含所有表项,表项之间由逗号和空格分隔,最后一个表项前插入 and。例如,将前面的 spam 列表传递给函数,将返回 ‘apples, bananas, tofu, and cats’。但是你的函数应该能够处理传递给它的任何列表值。
def commaCode(listExam):
if len(listExam) == 0:
return ''
if len(listExam) == 1:
return str(listExam[0])
result = ''
for i in range(len(listExam) - 2):
result += str(listExam[i]);
result += ', '
result += str(listExam[-2])
result += ' and '
result += str(listExam[-1])
return result
spam = ['apples', 'bananas', 'tofu', 'cats']
print(commaCode(spam))
字符图网格
假设有一个列表的列表,其中内部列表中的每个值都是包含一个字符的字符串,如下所示:
grid = [['.', '.', '.', '.', '.', '.'],
['.', 'O', 'O', '.', '.', '.'],
['O', 'O', 'O', 'O', '.', '.'],
['O', 'O', 'O', 'O', 'O', '.'],
['.', 'O', 'O', 'O', 'O', 'O'],
['O', 'O', 'O', 'O', 'O', '.'],
['O', 'O', 'O', 'O', '.', '.'],
['.', 'O', 'O', '.', '.', '.'],
['.', '.', '.', '.', '.', '.']]
可以认为 grid[x][y] 是一幅“图”在 x、y 坐标处的字符,该图由文本字符组成。原点 (0, 0) 在左上角,向右 x 坐标增加,向下 y 坐标增加。
复制前面的网格值,编写代码用它打印出图像。
..OO.OO..
.OOOOOOO.
.OOOOOOO.
..OOOOO..
...OOO...
....O....
提示:需要使用循环嵌套循环,打印出 grid[0][0],然后 grid[1][0],然后 grid[2][0],以此类推,直到 grid[8][0]。这就完成第一行,所以接下来打印换行。然后程序将打印出 grid[0][1],然后 grid[1][1],然后 grid[2][1],以此类推。程序最后将打印出 grid[8][5]。
另外,如果不想在每次 print() 调用后自动打印换行,请记住将 end 关键字参数传递给 print()。
def picture(grid):
m = len(grid)
n = len(grid[0])
for y in range(n):
for x in range(m):
print(grid[x][y], end = '')
print() # 打印换行
grid = [['.', '.', '.', '.', '.', '.'],
['.', 'O', 'O', '.', '.', '.'],
['O', 'O', 'O', 'O', '.', '.'],
['O', 'O', 'O', 'O', 'O', '.'],
['.', 'O', 'O', 'O', 'O', 'O'],
['O', 'O', 'O', 'O', 'O', '.'],
['O', 'O', 'O', 'O', '.', '.'],
['.', 'O', 'O', '.', '.', '.'],
['.', '.', '.', '.', '.', '.']]
picture(grid)
【python 让繁琐工作自动化】目录
学习网站:https://automatetheboringstuff.com/chapter4/