【Python编程 】从入门到实践(入门)

在这里插入图片描述

Python编程 入门

1.变量和数据类型

变量是可以赋给值的标签,也可以说变量指向特定的值。

1.1 变量命名

  • 变量名仅包含字母、数字、下划线,且数字不能打头。
  • 不能将Python关键字和函数名作为变量名。

1.2 变量

message = "Hello Python world!"
print(message)

message = 'Hello Python Crash Course world!'
print(message)

1.3 字符串

字符串就是一系列字符。在Python中,用引号括起的都是字符串,其中的引号可以是单引
号,也可以是双引号,如下所示:

"Hello Python world!"

'Hello Python Crash Course world!'

1.3.1 修改字符串大小写

变量name指向小写的字符串"ada lovelace"。在函数调用print()中,方法title()出现在这个变量的后面。方法是Python可对数据执行的操作。
在name.title()中,name后面的句点(.)让Python对变量name执行方法title()指定的操作。每个方法后面都跟着一对圆括号,这是因为方法通常需要额外的信息来完成其工作。这种信息是在圆括号内提供的。函数title()不需要额外的信息,因此它后面的圆括号是空的。

  • 方法title()以首字母大写的方式显示每个单词,即将每个单词的首字母都改为大写。
name = "ada lovelace"
print(name.title())
  • 将字符串改为全部大写或全部小写
name = "ada lovelace"
print(name.upper())  # 全部大写
print(name.lower())  # 全部小写

1.3.2 字符串中使用变量

  • 这种字符串名为f字符串。f是format(设置格式)的简写,因为Python通过把花括号内的
    变量替换为其值来设置字符串的格式。
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
print(full_name)

要在字符串中插入变量的值,可在前引号前加上字母f,再将要插入的变量放在花括号内。这样,当Python显示字符串时,将把每个变量都替换为其值。

1.3.3 使用制表符或换行符来添加空白

空白泛指任何非打印字符,如空格、制表符和换行符。

  • 字符串中添加制表符,可使用字符组合\t
print("Python")
Python
print("\tPython")
	Python
  • 在字符串中添加换行符,可使用字符组合\n
print("Languages:\nPython\nC\nJavaScript")
Languages:
Python
C
JavaScript

1.3.4 删除空白

Python能够找出字符串开头和末尾多余的空白。

  • 要确保字符串末尾没有空白,可使用方法rstrip()。
  • 要确保字符串开头没有空白,可使用方法lstrip()。
favorite_language = 'python '  # 末尾空格
print(favorite_language)
favorite_language = favorite_language.rstrip()
print(favorite_language)

还可以剔除字符串开头的空白,或者同时剔除字符串两边的空白。为此,可分别使用方
法lstrip()和strip()。

1.4 数

1.4.1 整数

在Python中,可对整数执行加(+)减(-)乘(*)除(/)运算。
使用两个乘号表示乘方运算。
可以使用圆括号来修改运算次序。

1.4.2 浮点数

Python将所有带小数点的数称为浮点数。
无论是哪种运算,只要有操作数是浮点数,Python默认得到的总是浮点数,即便结果原本
为整数也是如此。

1.4.3 数中的下划线

书写很大的数时,可使用下划线将其中的数字分组,使其更清晰易读,打印这种使用下划线定义的数时,Python不会打印其中的下划线。

universe_age = 14_000_000_000
print(universe_age)

1.4.4 同时给多个变量赋值

x, y, z = 0, 0, 0

1.4.5 常量

Python没有内置的常量类型,但可以使用全大写来指出应将某个变量视为常量,其值应始终不变。

MAX_CONNECTIONS = 5000
  • 在代码中,要指出应将特定的变量视为常量,可将其字母全部大写。

1.5 注释

在Python中,注释用井号(#)标识。井号后面的内容都会被Python解释器忽略。

favorite_language = 'python'  # 末尾空格
print(favorite_language)

2. 列表

列表由一系列按特定顺序排列的元素组成。其中的元素之间可以没有任何关系,列表通常包含多个元素。

2.1 列表是什么

在Python中,用方括号([ ])表示列表,并用逗号分隔其中的元素。

bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles)

2.2 访问列表元素

列表是有序集合,因此要访问列表的任意元素,只需将该元素的位置(索引)告诉Python即可。要访问列表元素,可指出列表的名称,再指出元素的索引,并将后者放在方括号内。

bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[0])

这里的索引还可以倒序,最后一个元素的索引为-1,倒数第二个元素的索引为-2,以此类推。

bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[-2])

2.3 修改、添加和删除列表元素

2.3.1 修改列表元素

  • 可以修改任意列表元素的值。
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)

motorcycles[0] = 'ducati'   # 修改第一个列表元素
print(motorcycles)

2.3.2 添加列表元素

01. 在列表末尾添加元素
  • 使用方法append()在列表末尾添加元素
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)

motorcycles.append('ducati')
print(motorcycles)
02. 在列表中添加元素
  • 使用方法insert()可在列表的任何位置添加新元素。需要指定新元素的索引和值。
motorcycles = ['honda', 'yamaha', 'suzuki']

motorcycles.insert(0, 'ducati')
print(motorcycles)

2.3.3 删除列表元素

01. del语句删除
  • 如果知道要删除元素在列表中的位置,可使用del语句删除列表中任意位置的元素。
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)

del motorcycles[1]
print(motorcycles)
02. 使用方法pop()删除

方法pop()删除列表末尾的元素,并让你能够接着使用它。术语弹出(pop)源自这样的类比:列表就像一个栈,而删除列表末尾的元素相当于弹出栈顶元素。

  • 方法pop()默认删除列表末尾的元素,也可以删除列表中任意位置的元素,只需在圆括号中指定要删除元素的索引即可。
  • 方法pop()删除的列表元素,可以继续使用它。
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)

popped_motorcycle = motorcycles.pop()
print(motorcycles)
print(popped_motorcycle)
03. 根据值删除列表元素
  • 如果只知道要删除元素的值,可使用方法remove()。但若列表中有多个重复的值,仅删除第一个符合要求的元素。
  • 使用remove()从列表中删除元素时,也可接着使用它的值。
motorcycles = ['honda', 'yamaha', 'suzuki', 'ducati']
print(motorcycles)

too_expensive = 'ducati'
motorcycles.remove(too_expensive)
print(motorcycles)
print(f"\nA {too_expensive.title()} is too expensive for me.")

2.4 组织列表

2.4.1 使用方法sort()对列表永久排序

cars = ['bmw', 'audi', 'toyota', 'subaru']
cars.sort()
print(cars)

2.4.2 使用函数sorted()对列表临时排序

cars = ['bmw', 'audi', 'toyota', 'subaru']
print("Here is the original list:")
print(cars)

print("\nHere is the sorted list:")
print(sorted(cars))

print("\nHere is the original list again:")
print(cars)

2.4.3 使用方法reverse()使列表倒序

cars = ['bmw', 'audi', 'toyota', 'subaru']
print(cars)

cars.reverse()
print(cars)

2.4.4 使用方法len()确定列表长度

cars = ['bmw', 'audi', 'toyota', 'subaru']
len(cars)

3. 列表操作

3.1 遍历列表(for循环)

需要对列表中的每个元素都执行相同的操作时,可使用Python中的for循环。

magicians = ['alice', 'david', 'carolina']
for magician in magicians:		# for循环
	print(magician)

在for循环中,想包含多少行代码都可以。实际上,你会发现使用for循环对每个元素执行众多不同的操作很有用。在函数中要注意缩进,Python中对缩进有严格的要求。

3.2 创建数值列表

列表非常适合用于存储数字集合,而Python提供了很多工具,可帮助你高效地处理数字列表。

3.2.1 使用函数range()

函数range()能够轻松地生成一系列数。

for value in range(1, 5):
	print(value)

**在这个示例中,range()只打印数1~4。**这是编程语言中常见的差一行为的结果。函数range()让Python从指定的第一个值开始数,并在到达你指定的第二个值时停止。因为它在第二个值处停止,所以输出不包含该值(这里为5)。
调用函数range()时,也可只指定一个参数,这样它将从0开始。例如,range(6)返回数0~5

3.2.2 使用range()创建数字列表

要创建数字列表,可使用函数list()将range()的结果直接转换为列表。如果将range()作为list()的参数,输出将是一个数字列表。

numbers = list(range(1, 6))
print(numbers)

**使用函数range()时,还可指定步长。**为此,可给这个函数指定第三个参数,Python将根据这个步长来生成数。步长也可为负数,但相应的范围也需从大到小排列。

even_numbers = list(range(2, 11, 2))		# 打印1~10的偶数
print(even_numbers)
even_numbers = list(range(10, 2, -2))		# 打印1~10的偶数
print(even_numbers)

使用函数range()几乎能够创建任何需要的数集,示例如下。

squares = []		# 创建一个名为squares的空列表
for value in range(1, 11):		# 使用函数range()让Python遍历1~10的值
	square = value ** 2		# 计算当前值的平方
	squares.append(square)		# 将新计算得到的平方值附加到列表squares末尾
print(squares)

3.2.3 对数字列表执行简单的统计计算

有几个专门用于处理数字列表的Python函数。例如,你可以轻松地找出数字列表的最大值、最小值和总和:

digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

min(digits)	# 最小值
max(digits)	# 最大值
sum(digits)	# 求和

3.3 使用列表的一部分

3.3.1 切片

要创建切片,可指定要使用的第一个元素和最后一个元素的索引。与函数range()一样,Python在到达第二个索引之前的元素后停止。要输出列表中的前三个元素,需要指定索引0和3,这将返回索引为0、1和2的元素。

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[0:3])

示例代码打印该列表的一个切片,其中包含五个元素。输出也是一个列表,其中包含前三元素。
你可以生成列表的任意子集。例如,如果要提取列表的第二、第三和第四个元素,可将起始索引指定为1,并将终止索引指定为4:

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[1:4])

如果没有指定第一个索引,Python将自动从列表开头开始:

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[:4])

要让切片终止于列表末尾,也可使用类似的语法。例如,如果要提取从第三个元素到列表末尾的所有元素,可将起始索引指定为2,并省略终止索引:

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[2:])

负数索引返回离列表末尾相应距离的元素,因此你可以输出列表末尾的任意切片。例如,如果要输出最后三个元素,可使用切片players[-3:]:

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[-3:])

可在表示切片的方括号内指定第三个值。这个值告诉Python在指定范围内每隔多少元素提取一个。

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[-3::2])

3.3.2 遍历切片

players = ['charles', 'martina', 'michael', 'florence', 'eli']
print("Here are the first three players on my team:")
for player in players[:3]:
	print(player.title())

3.3.3 复制列表

要复制列表,可创建一个包含整个列表的切片,方法是同时省略起始索引和终止索引([:])。这让Python创建一个始于第一个元素、终止于最后一个元素的切片,即整个列表的副本。

my_foods = ['pizza', 'falafel', 'carrot cake']
friend_foods = my_foods[:]

print("My favorite foods are:")
print(my_foods)

print("\nMy friend's favorite foods are:")
print(friend_foods)

3.4 元组

列表非常适合用于存储在程序运行期间可能变化的数据集。列表是可以修改的,这对处理网站的用户列表或游戏中的角色列表至关重要。
然而,有时候你需要创建一系列不可修改的元素,元组可以满足这种需求。Python将不能修改的值称为不可变的,而不可变的列表被称为元组。

3.4.1 定义元祖

元组看起来很像列表,但使用圆括号而非中括号来标识。定义元组后,就可使用索引来访问其元素,就像访问列表元素一样。

dimensions = (200, 50)
print(dimensions[0])
print(dimensions[1])

元组是由逗号标识的,圆括号只是让元组看起来更整洁、更清晰。如果你要定义只包含一个元素的元组,必须在这个元素后面加上逗号:

my_t = (3,)

3.4.2 遍历元组中的所有值

dimensions = (200, 50)
for dimension in dimensions:
	print(dimension)

3.4.3 修改元组变量

虽然不能修改元组的元素,但可以给存储元组的变量赋值。因此,如果要修改可重新定义整个元组

dimensions = (200, 50)
print("Original dimensions:")
for dimension in dimensions:
	print(dimension)
	
dimensions = (400, 100)
print("\nModified dimensions:")
for dimension in dimensions:
	print(dimension)

4. if语句

4.1 示例

cars = ['audi', 'bmw', 'subaru', 'toyota']
for car in cars:
	if car == 'bmw':
		print(car.upper())
	else:
		print(car.title())

4.2 条件测试

每条if语句的核心都是一个值为True或False的表达式,这种表达式称为条件测试
Python根据条件测试的值为True还是False来决定是否执行if语句中的代码。如果条件测试的值为True,Python就执行紧跟在if语句后面的代码;如果为False,Python就忽略这些代码。

4.2.1 检查是否相等

car = 'bmw'

car == 'bmw'

4.2.2 检查是否相等时忽略大小写

car = 'Audi'

car == 'audi'

4.2.3 检查是否不相等

要判断两个值是否不等,可结合使用惊叹号和等号(!=),其中的惊叹号表示不,其他很多编程语言中也是如此。

requested_topping = 'mushrooms'

if requested_topping != 'anchovies':
	print("Hold the anchovies!")

4.2.4 数值比较

answer = 17
if answer != 42:
	print("That is not the correct answer. Please try again!")

条件语句中可包含各种数学比较,如小于、小于等于、大于、大于等于:

age = 19

age < 21
age <= 21
age > 21
age >= 21

4.2.5 检查多个条件

你可能想同时检查多个条件。例如,有时候需要在两个条件都为True时才执行相应的操作,而有时候只要求一个条件为True。在这些情况下,关键字and和or可助你一臂之力。

01.使用and检查多个条件

要检查是否两个条件都为True,可使用关键字and将两个条件测试合而为一。如果每个测试都通过了,整个表达式就为True;如果至少一个测试没有通过,整个表达式就为False。

age_0 = 22
age_1 = 18

age_0 >= 21 and age_1 >= 21
age_1 = 22
age_0 >= 21 and age_1 >= 21
02.使用or检查多个条件

关键字or也能够让你检查多个条件,但只要至少一个条件满足,就能通过整个测试。仅当两个测试都没有通过时,使用or的表达式才为False。

age_0 = 22
age_1 = 18

age_0 >= 21 or age_1 >= 21
age_0 = 18
age_0 >= 21 or age_1 >= 21

4.2.6 检查特定值是否包含在列表中

要判断特定的值是否已包含在列表中,可使用关键字in。

requested_toppings = ['mushrooms', 'onions', 'pineapple']

'mushrooms' in requested_toppings
'pepperoni' in requested_toppings

4.2.7 检查特定值是否不包含在列表中

要判断特定的值是否已包含在列表中,可使用关键字not in。

banned_users = ['andrew', 'carolina', 'david']
user = 'marie'
if user not in banned_users:
	print(f"{user.title()}, you can post a response if you wish.")

4.2.8 布尔表达式

随着你对编程的了解越来越深入,将遇到术语布尔表达式,它不过是条件测试的别名。与条件表达式一样,布尔表达式的结果要么为True,要么为False。

game_active = True
can_edit = False

4.3 if语句的种类

4.3.1 简单if语句

最简单的if语句只有一个测试和一个操作:

age = 19
if age >= 18:
	print("You are old enough to vote!")

第一行可包含任何条件测试,而在紧跟在测试后面的缩进代码块中,可执行任何操作。如果条件测试的结果为True,Python就会执行紧跟在if语句后面的代码,否则Python将忽略这些代码。

4.3.2 if-else语句

我们经常需要在条件测试通过时执行一个操作,在没有通过时执行另一个操作。在这种情况下,可使用Python提供的if-else语句。if-else语句块类似于简单的if语句,但其中的else语句让你能够指定条件测试未通过时要执行的操作。

age = 17
if age >= 18:
	print("You are old enough to vote!")
	print("Have you registered to vote yet?")
else:
	print("Sorry, you are too young to vote.")
	print("Please register to vote as soon as you turn 18!")

4.3.3 if-elif-else结构

我们经常需要检查超过两个的情形,为此可使用Python提供的if-elif-else结构。
Python只执行if-elif-else结构中的一个代码块。它依次检查每个条件测试,直到遇到通过了的条件测试。测试通过后,Python将执行紧跟在它后面的代码,并跳过余下的测试。

age = 12
if age < 4:
	print("Your admission cost is $0.")
elif age < 18:
	print("Your admission cost is $25.")
else:
	print("Your admission cost is $40.")

可根据需要使用任意数量的elif代码块。

age = 12
if age < 4:
	price = 0
elif age < 18:
	price = 25
elif age < 65:
	price = 40
else:
	price = 20
	
print(f"Your admission cost is ${price}.")

Python并不要求if-elif结构后面必须有else代码块。在有些情况下,else代码块很有用;而在其他一些情况下,使用一条elif语句来处理特定的情形更清晰:

age = 12
if age < 4:
	price = 0
elif age < 18:
	price = 25
elif age < 65:
	price = 40
elif:
	price = 20
	
print(f"Your admission cost is ${price}.")

4.4 使用if语句处理列表

4.4.1 检查特殊元素

requested_toppings = ['mushrooms', 'green peppers', 'extra cheese']
for requested_topping in requested_toppings:
	if requested_topping == 'green peppers':
		print("Sorry, we are out of green peppers right now.")
	else:
		print(f"Adding {requested_topping}.")
		print("\nFinished making your pizza!")

4.4.2 确定列表不为空

到目前为止,我们对于处理的每个列表都做了一个简单的假设——假设它们都至少包含一个元素。因为马上就要让用户来提供存储在列表中的信息,所以不能再假设循环运行时列表不是空的。有鉴于此,在运行for循环前确定列表是否为空很重要。

requested_toppings = []
if requested_toppings:
	for requested_topping in requested_toppings:
		print(f"Adding {requested_topping}.")
	print("\nFinished making your pizza!")
else:
	print("Are you sure you want a plain pizza?")

4.4.3 使用多个列表

available_toppings = ['mushrooms', 'olives', 'green peppers','pepperoni','pineapple','extracheese']
requested_toppings = ['mushrooms', 'french fries', 'extra cheese']
for requested_topping in requested_toppings:
	if requested_topping in available_toppings:
		print(f"Adding {requested_topping}.")
	else:
		print(f"Sorry, we don't have {requested_topping}.")
print("\nFinished making your pizza!")

5. 字典

字典可存储的信息量几乎不受限制。有存储字典的列表、存储列表的字典和存储字典的字典。

5.1 一个简单的字典

alien_0 = {'color': 'green', 'points': 5}

print(alien_0['color'])
print(alien_0['points'])

5.2 使用字典

在Python中,字典是一系列键值对。每个键都与一个值相关联,你可使用键来访问相关联的值。与键相关联的值可以是数、字符串、列表乃至字典。事实上,可将任何Python对象用作字典中的值。
在Python中,字典用放在花括号({})中的一系列键值对表示。

alien_0 = {'color': 'green', 'points': 5}

键值对是两个相关联的值。指定键时,Python将返回与之相关联的值。键和值之间用冒号分隔,而键值对之间用逗号分隔。在字典中,想存储多少个键值对都可以。

5.2.1 访问字典中的值

要获取与键相关联的值,可依次指定字典名和放在方括号内的键,如下所示:

alien_0 = {'color': 'green'}
print(alien_0['color'])

5.2.2 添加键值对

字典是一种动态结构,可随时在其中添加键值对。要添加键值对,可依次指定字典名、用方括号括起的键和相关联的值。

alien_0 = {'color': 'green', 'points': 5}
print(alien_0)

alien_0['x_position'] = 0	# 添加键值对
alien_0['y_position'] = 25
print(alien_0)

5.2.3 先创建一个空字典

在空字典中添加键值对有时候可提供便利,而有时候必须这样做。为此,可先使用一对空花括号定义一个字典,再分行添加各个键值对。

alien_0 = {}

alien_0['color'] = 'green'
alien_0['points'] = 5

print(alien_0)

5.2.4 修改字典中的值

要修改字典中的值,可依次指定字典名、用方括号括起的键,以及与该键相关联的新值。

alien_0 = {'color': 'green'}
print(f"The alien is {alien_0['color']}.")

alien_0['color'] = 'yellow'
print(f"The alien is now {alien_0['color']}.")

5.2.5 删除键值对

对于字典中不再需要的信息,可使用del语句将相应的键值对彻底删除。使用del语句时,必须指定字典名和要删除的键。

alien_0 = {'color': 'green', 'points': 5}
print(alien_0)

del alien_0['points']
print(alien_0)

5.2.6 由类似对象组成的字典

在前面的示例中,字典存储的是一个对象的多种信息,但你也可以使用字典来存储众多对象的同一种信息。

favorite_languages = {
	'jen': 'python',
	'sarah': 'c',
	'edward': 'ruby',
	'phil': 'python',
	}

我们将一个较大的字典放在了多行中。每个键都是一个被调查者的名字,而每个值都是被调查者喜欢的语言。确定需要使用多行来定义字典时,要在输入左花括号后按回车键。在下一行缩进四个空格,指定第一个键值对,并在它后面加上一个逗号。此后再按回车键时,文本编辑器将自动缩进后续键值对,且缩进量与第一个键值对相同。
定义好字典后,在最后一个键值对的下一行添加一个右花括号,并缩进四个空格,使其与字典中的键对齐。一种不错的做法是,在最后一个键值对后面也加上逗号,为以后在下一行添加键值对做好准备。
注意 对于较长的列表和字典,大多数编辑器提供了以类似方式设置格式的功能。
对于较长的字典,还有其他一些可行的格式设置方式,因此在你的编辑器或其他源代码中,你可能会看到稍微不同的格式设置方式。

5.2.7 使用get()来访问值

使用放在方括号内的键从字典中获取感兴趣的值时,可能会引发问题:如果指定的键不存在就会出错。就字典而言,可使用方法get()在指定的键不存在时返回一个默认值,从而避免这样的错误。
方法get()的第一个参数用于指定键,是必不可少的;第二个参数为指定的键不存在时要返回的值,是可选的:

alien_0 = {'color': 'green', 'speed': 'slow'}
point_value = alien_0.get('points', 'No point value assigned.')
print(point_value)

如果字典中有键’points’,将获得与之相关联的值;如果没有,将获得指定的默认值。
虽然这里没有键’points’,但将获得一条清晰的消息,不会引发错误
如果指定的键有可能不存在,应考虑使用方法get(),而不要使用方括号表示法。
注意 调用get()时,如果没有指定第二个参数且指定的键不存在,Python将返回值None。这个特殊值表示没有相应的值。None并非错误,而是一个表示所需值不存在的特殊值。

5.3 遍历字典

一个Python字典可能只包含几个键值对,也可能包含数百万个键值对。鉴于字典可能包含
大量数据,Python支持对字典进行遍历。字典可用于以各种方式存储信息,因此有多种遍
历方式:可遍历字典的所有键值对,也可仅遍历键或值。

5.3.1 遍历所有键值对

探索各种遍历方法前,先来看一个新字典,它用于存储有关网站用户的信息。下面的字典存储一名用户的用户名、名和姓:

user_0 = {
	'username': 'efermi',
	'first': 'enrico',
	'last': 'fermi',
	}

如果要获悉该用户字典中的所有信息,该如何办呢?可使用for循环来遍历这个字典:

user_0 = {
	'username': 'efermi',
	'first': 'enrico',
	'last': 'fermi',
	}
for key, value in user_0.items():
	print(f"\nKey: {key}")
	print(f"Value: {value}")

5.3.2 遍历字典中的所有键

在不需要使用字典中的值时,方法keys()很有用。遍历字典时,会默认遍历所有的键。

favorite_languages = {
	'jen': 'python',
	'sarah': 'c',
	'edward': 'ruby',
	'phil': 'python',
}
for name in favorite_languages.keys():
	print(name.title())

显式地使用方法keys()可让代码更容易理解,你可以选择这样做,但是也可以省略它。
在这种循环中,可使用当前键来访问与之相关联的值。
方法keys()并非只能用于遍历:实际上,它返回一个列表,其中包含字典中的所有键。

5.3.3 按特定顺序遍历字典中的所有键

从Python 3.7起,遍历字典时将按插入的顺序返回其中的元素。不过在有些情况下,可能要按与此不同的顺序遍历字典。
要以特定顺序返回元素,一种办法是在for循环中对返回的键进行排序。为此,可使用函数sorted()来获得按特定顺序排列的键列表的副本:

favorite_languages = {
	'jen': 'python',
	'sarah': 'c',
	'edward': 'ruby',
	'phil': 'python',
	}
for name in sorted(favorite_languages.keys()):
	print(f"{name.title()}, thank you for taking the poll.")

这条for语句类似于其他for语句,不同之处是对方法dictionary.keys()的结果调用了函数sorted()。这让Python列出字典中的所有键,并在遍历前对这个列表进行排序。

5.3.4 遍历字典中的所有值

如果主要对字典包含的值感兴趣,可使用方法values()来返回一个值列表,不包含任何键。

favorite_languages = {
	'jen': 'python',
	'sarah': 'c',
	'edward': 'ruby',
	'phil': 'python',
	}
	
print("The following languages have been mentioned:")
for language in favorite_languages.values():
	print(language.title())

这种做法提取字典中所有的值,而没有考虑是否重复。涉及的值很少时,这也许不是问题,但如果被调查者很多,最终的列表可能包含大量重复项。为剔除重复项,可使用集合(set)。集合中的每个元素都必须是独一无二的:

favorite_languages = {
	'jen': 'python',
	'sarah': 'c',
	'edward': 'ruby',
	'phil': 'python',
	}
	
print("The following languages have been mentioned:")
for language in set(favorite_languages.values()):
	print(language.title())
  • 通过对包含重复元素的列表调用set(),可让Python找出列表中独一无二的元素,并使用这些元素来创建一个集合。
    集合和字典很容易混淆,因为它们都是用一对花括号定义的。当花括号内没有键值对时,定义的很可能是集合。不同于列表和字典,集合不会以特定的顺序存储元素。

5.4 嵌套

有时候,需要将一系列字典存储在列表中,或将列表作为值存储在字典中,这称为嵌套。你可以在列表中嵌套字典、在字典中嵌套列表甚至在字典中嵌套字典。

5.4.1 字典列表

alien_0 = {'color': 'green', 'points': 5}	# 字典类型
alien_1 = {'color': 'yellow', 'points': 10}
alien_2 = {'color': 'red', 'points': 15}

aliens = [alien_0, alien_1, alien_2]	# 列表类型
for alien in aliens:
	print(alien)

更符合现实的情形是,元素不止三个,且每个元素都是使用代码自动生成的。在下面的示例中,使用range()生成了30个元素:

# 创建一个用于存储元素的空列表
aliens = []

# 创建30个元素
for alien_number in range(30):
	new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
	aliens.append(new_alien)

# 显示前5个元素
for alien in aliens[:5]:
	print(alien)

# 显示创建了多少个元素
print(f"Total number of aliens: {len(aliens)}")

我们还可以修改字典中的值。

# 创建一个用于存储元素的空列表
aliens = []

# 创建30个元素
for alien_number in range(30):
	new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
	aliens.append(new_alien)

for alien in aliens[:3]:
if alien['color'] == 'green':
	alien['color'] = 'yellow'
	alien['speed'] = 'medium'
	alien['points'] = 10

# 显示前5个元素
for alien in aliens[:5]:
	print(alien)
	
for alien in aliens[0:3]:
	if alien['color'] == 'green':
		alien['color'] = 'yellow'
		alien['speed'] = 'medium'
		alien['points'] = 10
	elif alien['color'] == 'yellow':
		alien['color'] = 'red'
		alien['speed'] = 'fast'
		alien['points'] = 15
		
# 显示前5个元素
for alien in aliens[:5]:
	print(alien)

经常需要在列表中包含大量的字典,而其中每个字典都包含特定对象的众多信息。例如,你可能需要为网站的每个用户创建一个字典(就像6.3.1节的user.py中那样),并将这些字典存储在一个名为users的列表中。在这个列表中,所有字典的结构都相同,因此你可以遍历这个列表,并以相同的方式处理其中的每个字典。

5.4.2 在字典中存储列表

有时候,需要将列表存储在字典中,而不是将字典存储在列表中。

# 存储所点比萨的信息。
pizza = {
	'crust': 'thick',
	'toppings': ['mushrooms', 'extra cheese'],
	}
# 概述所点的比萨。
print(f"You ordered a {pizza['crust']}-crust pizza ""with the following toppings:")
for topping in pizza['toppings']:
	print("\"f+topping)

每当需要在字典中将一个键关联到多个值时,都可以在字典中嵌套一个列表。
在遍历该字典的for循环中,我们需要再使用一个for循环来遍历与被调查者相关联的语言列表:

favorite_languages = {
	'jen': ['python', 'ruby'],
	'sarah': ['c'],
	'edward': ['ruby', 'go'],
	'phil': ['python', 'haskell'],
	}
	
for name, languages in favorite_languages.items():
	print(f"\n{name.title()}'s favorite languages are:")
	for language in languages:
		print(f"\t{language.title()}")

为进一步改进这个程序,可在遍历字典的for循环开头添加一条if语句,通过查看len(languages)的值来确定当前的被调查者喜欢的语言是否有多种。如果他喜欢的语言有多种,就像以前一样显示输出;如果只有一种,就相应修改输出的措辞,如显示Sarah’s favorite language is C。
注意: 列表和字典的嵌套层级不应太多。如果嵌套层级比前面的示例多得多,很可能有更简单的解决方案。

5.4.3 在字典中存储字典

可在字典中嵌套字典,但这样做时,代码可能很快复杂起来。例如,如果有多个网站用户,每个都有独特的用户名,可在字典中将用户名作为键,然后将每位用户的信息存储在一个字典中,并将该字典作为与用户名相关联的值。在下面的程序中,存储了每位用户的三项信息:名、姓和居住地。为访问这些信息,我们遍历所有的用户名,并访问与每个用户名相关联的信息字典:

users = {
	'aeinstein': {
		'first': 'albert',
		'last': 'einstein',
		'location': 'princeton',
		},
	'mcurie': {
		'first': 'marie',
		'last': 'curie',
		'location': 'paris',
		},
	}
	
for username, user_info in users.items():
	print(f"\nUsername: {username}")
	full_name = f"{user_info['first']} {user_info['last']}"
	location = user_info['location']
	
	print(f"\tFull name: {full_name.title()}")
	print(f"\tLocation: {location.title()}")

请注意,表示每位用户的字典都具有相同的结构。虽然Python并没有这样的要求,但这使得嵌套的字典处理起来更容易。倘若表示每位用户的字典都包含不同的键,for循环内部的代码将更复杂。

6. 用户输入和while循环

6.1 函数input()的工作原理

函数input()让程序暂停运行,等待用户输入一些文本。获取用户输入后,Python将其赋给一个变量,以方便使用。

message = input("Tell me something, and I will repeat it back to you: ")
print(message)

函数input()接受一个参数——要向用户显示的提示(prompt)或说明,让用户知道该如何做。

有时候,提示可能超过一行。例如,你可能需要指出获取特定输入的原因。在这种情况下,可将提示赋给一个变量,再将该变量传递给函数input()。这样,即便提示超过一行,input()语句也会非常清晰。

prompt = "If you tell us who you are, we can personalize the messages you see."
prompt += "\nWhat is your first name? "

name = input(prompt)
print(f"\nHello, {name}!")

本例演示了一种创建多行字符串的方式。第一行将消息的前半部分赋给变量prompt中。在第二行中,运算符+=在前面赋给变量prompt的字符串末尾附加一个字符串。

6.1.1 使用int()来获取数值输入

使用函数input()时,Python将用户输入解读为字符串。

age = input("How old are you? ")

试图将输入用于数值比较时,Python会引发错误,因为它无法将字符串和整数进行比较。
为解决这个问题,可使用函数int(),它让Python将输入视为数值。函数int()将数的字符串表示转换为数值表示,如下所示:

age = input("How old are you? ")

 age = int(age)
 age >= 18

将数值输入用于计算和比较前,务必将其转换为数值表示。

6.1.2 求模运算符

处理数值信息时,求模运算符(%)是个很有用的工具,它将两个数相除并返回余数:

 4 % 3

求模运算符不会指出一个数是另一个数的多少倍,只指出余数是多少。

如果一个数可被另一个数整除,余数就为0,因此求模运算将返回0。可利用这一点来判断一个数是奇数还是偶数。
偶数都能被2整除,因此如果对一个数和2执行求模运算的结果为0,即number % 2 ==0,那么这个数就是偶数;否则就是奇数。

6.2 while循环简介

for循环用于针对集合中的每个元素都执行一个代码块,而while循环则不断运行,直到指定的条件不满足为止。

6.2.1 使用while循环

可使用while循环来数数。例如,下面的while循环从1数到5:

current_number = 1
while current_number <= 5:
	print(current_number)
	current_number += 1

6.2.2 让用户选择何时退出

可以使用while循环让程序在用户愿意时不断运行,如下面的程序parrot.py所示。我们在其中定义了一个退出值,只要用户输入的不是这个值,程序就将接着运行:

prompt = "\nTell me something, and I will repeat it back to you:"
prompt += "\nEnter 'quit' to end the program. "
message = ""
while message != 'quit':
	message = input(prompt)
	if message != 'quit':
		print(message)

6.2.3 使用break退出循环

要立即退出while循环,不再运行循环中余下的代码,也不管条件测试的结果如何,可使用break语句。break语句用于控制程序流程,可用来控制哪些代码行将执行、哪些代码行不执行,从而让程序按你的要求执行你要执行的代码。

prompt = "\nPlease enter the name of a city you have visited:"
prompt += "\n(Enter 'quit' when you are finished.) "
while True:
	city = input(prompt)
	if city == 'quit':
		break
	else:
		print(f"I'd love to go to {city.title()}!")

6.2.4 在循环中使用continue

要返回循环开头,并根据条件测试结果决定是否继续执行循环,可使用continue语句,它不像break语句那样不再执行余下的代码并退出整个循环,仅是跳过当前循环,进入下一次循环。

例如,来看一个从1数到10但只打印其中奇数的循环:

current_number = 0
while current_number < 10:
	current_number += 1
	if current_number % 2 == 0:
		continue
	print(current_number)

6.3 使用while循环处理列表和字典

到目前为止,我们每次都只处理了一项用户信息:获取用户的输入,再将输入打印出来或做出应答;循环再次运行时,获悉另一个输入值并做出响应。然而,要记录大量的用户和信息,需要在while循环中使用列表和字典。
for循环是一种遍历列表的有效方式,但不应在for循环中修改列表,否则将导致Python难以跟踪其中的元素。要在遍历列表的同时对其进行修改,可使用while循环。通过将while循环同列表和字典结合起来使用,可收集、存储并组织大量输入,供以后查看和显示。

6.3.1 在列表之间移动元素

假设有一个列表包含新注册但还未验证的网站用户。验证这些用户后,如何将他们移到另一个已验证用户列表中呢?一种办法是使用一个while循环,在验证用户的同时将其从未验证用户列表中提取出来,再将其加入另一个已验证用户列表中。代码可能类似于下面这样:

# 首先,创建一个待验证用户列表和一个用于存储已验证用户的空列表
unconfirmed_users = ['alice', 'brian', 'candace']
confirmed_users = []

# 验证每个用户,直到没有未验证用户为止
# 将每个经过验证的用户都移到已验证用户列表中
while unconfirmed_users:
	current_user = unconfirmed_users.pop()
	print(f"Verifying user: {current_user.title()}")
	confirmed_users.append(current_user)
	
# 显示所有已验证的用户
print("\nThe following users have been confirmed:")
for confirmed_user in confirmed_users:
print(confirmed_user.title())

6.3.2 删除为特定值的所有列表元素

在第3章中,我们使用函数remove()来删除列表中的特定值。这之所以可行,是因为要删除的值只在列表中出现一次。如果要删除列表中所有为特定值的元素,该怎么办呢?
假设你有一个宠物列表,其中包含多个值为’cat’的元素。要删除所有这些元素,可不断运行一个while循环,直到列表中不再包含值’cat’,如下所示:

pets = ['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']
print(pets)

while 'cat' in pets:
	pets.remove('cat')
	
print(pets)

6.3.3 使用用户输入来填充字典

可使用while循环提示用户输入任意多的信息。下面创建一个调查程序,其中的循环每次执行时都提示输入被调查者的名字和回答。我们将收集的数据存储在一个字典中,以便将回答同被调查者关联起来:

responses = {}
# 设置一个标志,指出调查是否继续
polling_active = True

while polling_active:
	# 提示输入被调查者的名字和回答
	name = input("\nWhat is your name? ")
	response = input("Which mountain would you like to climb someday? ")
	# 将回答存储在字典中
	responses[name] = response
	# 看看是否还有人要参与调查
	repeat = input("Would you like to let another person respond? (yes/ no) ")
	if repeat == 'no':
		polling_active = False
		
# 调查结束,显示结果
print("\n--- Poll Results ---")
for name, response in responses.items():
	print(f"{name} would like to climb {response}.")

7. 函数

函数是带名字的代码块,用于完成具体的工作。要执行函数定义的特定任务,可调用该函数。需要在程序中多次执行同一项任务时,无须反复编写完成该任务的代码,只需要调用执行该任务的函数,让Python运行其中的代码即可。

7.1 定义函数

下面是一个打印问候语的简单函数,名为greet_user():

def greet_user():	# 函数定义
	"""显示简单的问候语"""
	print("Hello!")
	
greet_user()	# 调用

7.1.1 向函数传递信息

只需稍作修改,就可让函数greet_user()不仅向用户显示Hello!,还将用户的名字作为抬头。为此,可在函数定义def greet_user()的括号内添加username。通过在这里添加username,可让函数接受你给username指定的任何值。现在,这个函数要求你调用它时给username指定一个值。

def greet_user(username):
	"""显示简单的问候语"""
	print(f"Hello, {username.title()}!")
	
greet_user('jesse')

7.1.2 实参和形参

前面定义函数greet_user()时,要求给变量username指定一个值。调用这个函数并提供这种信息(人名)时,它将打印相应的问候语。

在函数greet_user()的定义中,变量username是一个形参(parameter),即函数完成工作所需的信息。在代码greet_user(‘jesse’)中,值’jesse’是一个实参(argument),即调用函数时传递给函数的信息。调用函数时,将要让函数使用的信息放在圆括号内。在greet_user(‘jesse’)中,将实参’jesse’传递给了函数greet_user(),这个值被赋给了形参username。

7.2 传递实参

函数定义中可能包含多个形参,因此函数调用中也可能包含多个实参。向函数传递实参的方式很多:可使用位置实参,这要求实参的顺序与形参的顺序相同;也可使用关键字实参,其中每个实参都由变量名和值组成;还可使用列表和字典。下面依次介绍这些方
式。

7.2.1 位置实参

调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于实参的顺序。这种关联方式称为位置实参。

def describe_pet(animal_type, pet_name):
	"""显示宠物的信息"""
	print(f"\nI have a {animal_type}.")
	print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet('hamster', 'harry')
01. 多次调用函数

可以根据需要调用函数任意次。要再描述一个宠物,只需再次调用describe_pet()即可:

def describe_pet(animal_type, pet_name):
	"""显示宠物的信息"""
	print(f"\nI have a {animal_type}.")
	print(f"My {animal_type}'s name is {pet_name.title()}.")
	
describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')

第二次调用describe_pet()函数时,向它传递了实参’dog’和’willie’。与第一次调用时一样,Python将实参’dog’关联到形animal_type,并将实参’willie’关联到形参pet_name。与前面一样,这个函数完成了任务,但打印的是一条名为Willie的小狗的信息。

多次调用函数是一种效率极高的工作方式。只需在函数中编写一次描述宠物的代码,然后每当需要描述新宠物时,都调用该函数并向它提供新宠物的信息。即便描述宠物的代码增加到了10行,依然只需使用一行调用函数的代码,就可描述一个新宠物。

在函数中,可根据需要使用任意数量的位置实参,Python将按顺序将函数调用中的实参关联到函数定义中相应的形参。

02. 位置实参的顺序很重要

使用位置实参来调用函数时,如果实参的顺序不正确,结果可能出乎意料:

def describe_pet(animal_type, pet_name):
	"""显示宠物的信息"""
	print(f"\nI have a {animal_type}.")
	print(f"My {animal_type}'s name is {pet_name.title()}.")
	
describe_pet('harry', 'hamster')

在这个函数调用中,先指定名字,再指定动物类型。由于实参’harry’在前,这个值将赋给形参animal_type。在传递参数时,确认函数调用中实参的顺序与函数定义中形参的顺序一致。

7.2.2 关键字实参

关键字实参是传递给函数的名称值对。因为直接在实参中将名称和值关联起来,所以向函数传递实参时不会混淆(不会得到名为Hamster的harry这样的结果)。关键字实参让你无须考虑函数调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。

def describe_pet(animal_type, pet_name):
	"""显示宠物的信息"""
	print(f"\nI have a {animal_type}.")
	print(f"My {animal_type}'s name is {pet_name.title()}.")
	
describe_pet(animal_type='hamster', pet_name='harry')

关键字实参的顺序无关紧要,因为Python知道各个值该赋给哪个形参。
注意:使用关键字实参时,务必准确指定函数定义中的形参名。

7.2.3 默认值

编写函数时,可给每个形参指定默认值。在调用函数中给形参提供了实参时,Python将使用指定的实值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。

def describe_pet(pet_name, animal_type='dog'):		# animal_type为默认值
	"""显示宠物的信息"""
	print(f"\nI have a {animal_type}.")
	print(f"My {animal_type}'s name is {pet_name.title()}.")
	
describe_pet(pet_name='willie')

请注意,在这个函数的定义中,修改了形参的排列顺序。因为给animal_type指定了默认值,无须通过实参来指定动物类型,所以在函数调用中只包含一个实参——宠物的名字。然而,Python依然将这个实参视为位置实参,因此如果函数调用中只包含宠物的名字,这个实参将关联到函数定义中的第一个形参。这就是需要将pet_name放在形参列表开头的原因。

如果显式地给animal_type提供了实参,Python将忽略这个形参的默认值。

def describe_pet(pet_name, animal_type='dog'):		# animal_type为默认值
	"""显示宠物的信息"""
	print(f"\nI have a {animal_type}.")
	print(f"My {animal_type}'s name is {pet_name.title()}.")
	
describe_pet(pet_name='harry', animal_type='hamster')

注意:使用默认值时,必须先在形参列表中列出没有默认值的形参,再列出有默认值的实参。这让Python依然能够正确地解读位置实参。

7.2.4 等效的函数调用

鉴于可混合使用位置实参、关键字实参和默认值,通常有多种等效的函数调用方式。请看下面对函数describe_pet()的定义,其中给一个形参提供了默认值:

def describe_pet(pet_name, animal_type='dog'):

基于这种定义,在任何情况下都必须给pet_name提供实参。指定该实参时可采用位置方式,也可采用关键字方式。如果要描述的动物不是小狗,还必须在函数调用中给animal_type提供实参。同样,指定该实参时可以采用位置方式,也可采用关键字方式。

7.2.5 避免实参错误

等你开始使用函数后,如果遇到实参不匹配错误,不要大惊小怪。你提供的实参多于或少于函数完成工作所需的信息时,将出现实参不匹配错误。

7.3 返回值

函数并非总是直接显示输出,它还可以处理一些数据,并返回一个或一组值。函数返回的值称为返回值。在函数中,可使用return语句将值返回到调用函数的代码行。返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。

7.3.1 返回简单值

下面来看一个函数,它接受名和姓并返回整洁的姓名:

def get_formatted_name(first_name, last_name):
	"""返回整洁的姓名"""
	full_name = f"{first_name} {last_name}"
	return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')
print(musician)

7.3.2 让实参变成可选的

有时候,需要让实参变成可选的,这样使用函数的人就能只在必要时提供额外的信息。可使用默认值来让实参变成可选的。

例如,假设要扩展函数get_formatted_name(),使其同时处理中间名。为此,可将其修改成类似于下面这样:

def get_formatted_name(first_name, middle_name, last_name):
	"""返回整洁的姓名"""
	full_name = f"{first_name} {middle_name} {last_name}"
	return full_name.title()
	
musician = get_formatted_name('john', 'lee', 'hooker')
print(musician)

可选值让函数能够处理各种不同的情形,同时确保函数调用尽可能简单。

7.3.3 返回字典

函数可返回任何类型的值,包括列表和字典等较复杂的数据结构。例如,下面的函数接受姓名的组成部分,并返回一个表示人的字典:

def build_person(first_name, last_name):
	"""返回一个字典,其中包含有关一个人的信息"""
	person = {'first': first_name, 'last': last_name}
	return person
	
musician = build_person('jimi', 'hendrix')
print(musician)

7.3.4 结合使用函数和while循环

可将函数同本书前面介绍的任何Python结构结合起来使用。例如,下面将结合使用函数get_formatted_name()和while循环,以更正式的方式问候用户。下面尝试使用名和姓跟用户打招呼:

def get_formatted_name(first_name, last_name):
	"""返回整洁的姓名"""
	full_name = f"{first_name} {last_name}"
	return full_name.title()
	
# 这是一个无限循环
while True:
	print("\nPlease tell me your name:")
	f_name = input("First name: ")
	l_name = input("Last name: ")
	formatted_name = get_formatted_name(f_name, l_name)
	print(f"\nHello, {formatted_name}!")

7.4 传递列表

你经常会发现,向函数传递列表很有用,其中包含的可能是名字、数或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问其内容。下面使用函数来提高处理列表的效率。

假设有一个用户列表,我们要问候其中的每位用户。下面的示例将包含名字的列表传递给一个名为greet_users()的函数,这个函数问候列表中的每个人:

def greet_users(names):
	"""向列表中的每位用户发出简单的问候"""
	for name in names:
		msg = f"Hello, {name.title()}!"
		print(msg)
		
usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)

7.4.1 在函数中修改列表

将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量数据。

来看一家为用户提交的设计制作3D打印模型的公司。需要打印的设计存储在一个列表中,打印后将移到另一个列表中。下面是在不使用函数的情况下模拟这个过程的代码:

 def print_models(unprinted_designs, completed_models):
	""" 模拟打印每个设计,直到没有未打印的设计为止,打印每个设计后,都将其移到列表completed_models中"""
	while unprinted_designs:
		current_design = unprinted_designs.pop()
		print(f"Printing model: {current_design}")
		completed_models.append(current_design)
		
def show_completed_models(completed_models):
	"""显示打印好的所有模型"""
	print("\nThe following models have been printed:")
	for completed_model in completed_models:
		print(completed_model)
		
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)

7.4.2 禁止函数修改列表

有时候,需要禁止函数修改列表。例如,假设像前一个示例那样,你有一个未打印的设计列表,并编写了一个函数将这些设计移到打印好的模型列表中。你可能会做出这样的决定:即便打印好了所有设计,也要保留原来的未打印的设计列表,以供备案。但由于你将所有的设计都移出了unprinted_designs,这个列表变成了空的,原来的列表没有了。

为解决这个问题,可向函数传递列表的副本而非原件。这样,函数所做的任何修改都只影响副本,而原件丝毫不受影响。要将列表的副本传递给函数,可以像下面这样做:

function_name(list_name_[:])

切片表示法[:]创建列表的副本。

7.5 传递任意数量的实参

有时候,预先不知道函数需要接受多少个实参,好在Python允许函数从调用语句中收集任意数量的实参。

例如,来看一个制作比萨的函数,它需要接受很多配料,但无法预先确定顾客要多少种配料。下面的函数只有一个形参*toppings,但不管调用语句提供了多少实参,这个形参会将它们统统收入囊中:

def make_pizza(*toppings):
	"""打印顾客点的所有配料"""
	print(toppings)
	
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

7.5.1 结合使用位置实参和任意数量实参

如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。

例如,如果前面的函数还需要一个表示比萨尺寸的形参,必须将其放在形参*toppings的前面:

def make_pizza(size, *toppings):
	"""概述要制作的比萨"""
	print(f"\nMaking a {size}-inch pizza with the following toppings:")
	for topping in toppings:
		print(f"- {topping}")
		
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

注意:你经常会看到通用形参名*args,它也收集任意数量的位置实参。

7.5.2 使用任意数量的关键字实参

有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信息。在这种情况下,可将函数编写成能够接受任意数量的键值对——调用语句提供了多少就接受多少。一个这样的示例是创建用户简介:你知道将收到有关用户的信息,但不确定会是什么样的信息。

在下面的示例中,函数build_profile()接受名和姓,还接受任意数量的关键字实参:

def build_profile(first, last, **user_info):
	"""创建一个字典,其中包含我们知道的有关用户的一切"""
	user_info['first_name'] = first
	user_info['last_name'] = last
	return user_info
	
user_profile = build_profile('albert', 'einstein',location='princeton',field='physics')
print(user_profile)

7.6 将函数存储在模块中

使用函数的优点之一是可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在称为模块的独立文件中,再将模块导入到主程序中。import语句允许在当前运行的程序文件中使用模块中的代码。

通过将函数存储在独立的文件中,可隐藏程序代码的细节,将重点放在程序的高层逻辑上。这还能让你在众多不同的程序中重用函数。将函数存储在独立文件中后,可与其他程序员共享这些文件而不是整个程序。知道如何导入函数还能让你使用其他程序员编写的函数库。

7.6.1 导入整个模块

要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件,包含要导入到程序中的代码。下面来创建一个包含函数make_pizza()的模块。为此,将文件pizza.py中除函数make_pizza()之外的其他代码删除:

def make_pizza(size, *toppings):
	"""概述要制作的比萨"""
	print(f"\nMaking a {size}-inch pizza with the following toppings:")
	for topping in toppings:
		print(f"- {topping}")

接下来,在pizza.py所在的目录中创建一个名为making_pizzas.py的文件。这个文件导入刚创建的模块,再调用make_pizza()两次:

import pizza

pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

7.6.2 导入特定的函数

还可以导入模块中的特定函数,这种导入方法的语法如下:

from module_name import function_name

通过用逗号分隔函数名,可根据需要从模块中导入任意数量的函数:

from module_name import function_0, function_1, function_2

使用这种语法时,调用函数时无须使用句点。由于在import语句中显式地导入了函数make_pizza(),调用时只需指定其名称即可。

7.6.3 使用as给函数指定别名

如果要导入函数的名称可能与程序中现有的名称冲突,或者函数的名称太长,可指定简短而独一无二的别名:函数的另一个名称,类似于外号。要给函数取这种特殊外号,需要在导入它时指定。

下面给函数make_pizza()指定了别名mp()。这是在import语句中使用make_pizza asmp实现的,关键字as将函数重命名为指定的别名:

from pizza import make_pizza as mp

mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')

7.6.4 使用as给模块指定别名

还可以给模块指定别名。通过给模块指定简短的别名(如给模块pizza指定别名p),让你能够更轻松地调用模块中的函数。相比于pizza.make_pizza(),p.make_pizza()更为简洁:

import pizza as p

p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

7.6.5 导入模块中的所有函数

使用星号(*)运算符可让Python导入模块中的所有函数:

from pizza import *

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

import语句中的星号让Python将模块pizza中的每个函数都复制到这个程序文件中。由于导入了每个函数,可通过名称来调用每个函数,而无须使用句点表示法。然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法。这是因为如果模块中有函数的名称与当前项目中使用的名称相同,可能导致意想不到的结果:Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。

最佳的做法是,要么只导入需要使用的函数,要么导入整个模块并使用句点表示法。这让代码更清晰,更容易阅读和理解。

8. 类

面向对象编程是最有效的软件编写方法之一。在面向对象编程中,你编写表示现实世界中的事物和情景的类,并基于这些类来创建对象。

编写类时,你定义一大类对象都有的通用行为。基于类创建对象时,每个对象都自动具备这种通用行为,然后可根据需要赋予每个对象独特的个性。

根据类来创建对象称为实例化,这让你能够使用类的实例。

8.1 创建和使用类

使用类几乎可以模拟任何东西。下面来编写一个表示小狗的简单类Dog,它表示的不是特定的小狗,而是任何小狗。对于大多数宠物狗,我们都知道些什么呢?它们都有名字和年龄。我们还知道,大多数小狗还会蹲下和打滚。由于大多数小狗都具备上述两项信息(名字和年龄)和两种行为(蹲下和打滚),我们的Dog类将包含它们。这个类让Python知道如何创建表示小狗的对象。编写这个类后,我们将使用它来创建表示特定小狗的实例。

8.1.1 创建Dog类

根据Dog类创建的每个实例都将存储名字和年龄,我们赋予了每条小狗蹲下(sit())和打滚(roll_over())的能力:

class Dog:
	"""一次模拟小狗的简单尝试"""
	def __init__(self, name, age):
		"""初始化属性name和age"""
		self.name = name
		self.age = age
		
	 def sit(self):
		"""模拟小狗收到命令时蹲下"""
		print(f"{self.name} is now sitting.")
		
	def roll_over(self):
		"""模拟小狗收到命令时打滚"""
		print(f"{self.name} rolled over!")

方法__init__()
类中的函数称为方法。你在前面学到的有关函数的一切都适用于方法,就目前而言,唯一重要的差别是调用方法的方式。

我们将方法__init__()定义成包含三个形参:self、name和age。在这个方法的定义中,形参self必不可少,而且必须位于其他形参的前面。为何必须在方法定义中包含形参self呢?因为Python调用这个方法来创建Dog实例时,将自动传入实参self。每个与实例相关联的方法调用都自动传递实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。创建Dog实例时,Python将调用Dog类的方法__init__()。我们将通过实参向Dog()传递名字和年龄,self会自动传递,因此不需要传递它。每当根据Dog类创建实例时,都只需给最后两个形参(name和age)提供值。

8.1.2 根据类创建实例

可将类视为有关如何创建实例的说明。Dog类是一系列说明,让Python知道如何创建表示特定小狗的实例。
下面来创建一个表示特定小狗的实例:

class Dog:
	def __init__(self, name, age):
		"""初始化属性name和age"""
		self.name = name
		self.age = age
		
	 def sit(self):
		"""模拟小狗收到命令时蹲下"""
		print(f"{self.name} is now sitting.")
		
	def roll_over(self):
		"""模拟小狗收到命令时打滚"""
		print(f"{self.name} rolled over!")
		
		
my_dog = Dog('Willie', 6)

print(f"My dog's name is {my_dog.name}.")
print(f"My dog is {my_dog.age} years old.")
01. 访问属性

要访问实例的属性,可使用句点表示法。编写如下代码来访问my_dog的属性name的值:

my_dog.name

句点表示法在Python中很常用,这种语法演示了Python如何获悉属性的值。
Python先找到实例my_dog,再查找与该实例相关联的属性name。在Dog类中引用这个属性时,使用的是self.name。

02. 调用方法

根据Dog类创建实例后,就能使用句点表示法来调用Dog类中定义的任何方法了。下面来让小狗蹲下和打滚:

class Dog:
	def __init__(self, name, age):
		"""初始化属性name和age"""
		self.name = name
		self.age = age
		
	 def sit(self):
		"""模拟小狗收到命令时蹲下"""
		print(f"{self.name} is now sitting.")
		
	def roll_over(self):
		"""模拟小狗收到命令时打滚"""
		print(f"{self.name} rolled over!")
		
		
my_dog = Dog('Willie', 6)
my_dog.sit()
my_dog.roll_over()

要调用方法,可指定实例的名称(这里是my_dog)和要调用的方法,并用句点分隔。遇到代码my_dog.sit()时,Python在类Dog中查找方法sit()并运行其代码。Python以同样的方式解读代码my_dog.roll_over()。

03. 创建多个实例

可按需求根据类创建任意数量的实例。下面再创建一个名为your_dog的小狗实例:

class Dog:
	def __init__(self, name, age):
		"""初始化属性name和age"""
		self.name = name
		self.age = age
		
	 def sit(self):
		"""模拟小狗收到命令时蹲下"""
		print(f"{self.name} is now sitting.")
		
	def roll_over(self):
		"""模拟小狗收到命令时打滚"""
		print(f"{self.name} rolled over!")
		
		
my_dog = Dog('Willie', 6)
your_dog = Dog('Lucy', 3)

print(f"My dog's name is {my_dog.name}.")
print(f"My dog is {my_dog.age} years old.")
my_dog.sit()

print(f"\nYour dog's name is {your_dog.name}.")
print(f"Your dog is {your_dog.age} years old.")
your_dog.sit()

即使给第二条小狗指定同样的名字和年龄,Python依然会根据Dog类创建另一个实例。你可按需求根据一个类创建任意数量的实例,条件是将每个实例都存储在不同的变量中,或者占用列表或字典的不同位置。

8.2 使用类和实例

可使用类来模拟现实世界中的很多情景。类编写好后,你的大部分时间将花在根据类创建的实例上。你需要执行的一个重要任务是修改实例的属性。可以直接修改实例的属性,也可以编写方法以特定的方式进行修改。

8.2.1 Car类

下面来编写一个表示汽车的类。它存储了有关汽车的信息,还有一个汇总这些信息的方法:

class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性"""
		self.make = make
		self.model = model
		self.year = year
		
	def get_descriptive_name(self):
		"""返回整洁的描述性信息"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()
		
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())

定义了方法__init__()。与前面的Dog类中一样,这个方法的第一个形参为self。该方法还包含另外三个形参:make、model和year。方法__init__()接受这些形参的值,并将它们赋给根据这个类创建的实例的属性。创建新的Car实例时,需要指定其制造商、型号和生产年份。
定义了一个名为get_descriptive_name()的方法。它使用属性year、make和model创建一个对汽车进行描述的字符串,让我们无须分别打印每个属性的值。为在这个方法中访问属性的值,使用了self.make、self.model和self.year。

8.2.2 给属性指定默认值

创建实例时,有些属性无须通过形参来定义,可在方法__init__()中为其指定默认值。

下面来添加一个名为odometer_reading的属性,其初始值总是为0。我们还添加了一个名read_odometer()的方法,用于读取汽车的里程表:

class Car:
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性。"""
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0
		
	def get_descriptive_name(self):
		"""返回整洁的描述性信息"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		"""打印一条指出汽车里程的消息"""
		print(f"This car has {self.odometer_reading} miles on it.")


my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()

8.2.3 修改属性的值

我们能以三种方式修改属性的值:直接通过实例进行修改,通过方法进行设置,以及通过方法进行递增(增加特定的值)。下面依次介绍这些方式。

01. 直接修改属性的值

要修改属性的值,最简单的方式是通过实例直接访问它。下面的代码直接将里程表读数设置为23:

class Car:
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性。"""
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0
		
	def get_descriptive_name(self):
		"""返回整洁的描述性信息"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		"""打印一条指出汽车里程的消息"""
		print(f"This car has {self.odometer_reading} miles on it.")


my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23
my_new_car.read_odometer()

有时候需要像这样直接访问属性,但其他时候需要编写对属性进行更新的方法。

02. 通过方法修改属性的值

如果有方法能替你更新属性,将大有裨益。这样就无须直接访问属性,而可将值传递给方法,由它在内部进行更新。
下面的示例演示了一个名为update_odometer()的方法:

class Car:
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性。"""
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0
		
	def get_descriptive_name(self):
		"""返回整洁的描述性信息"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		"""打印一条指出汽车里程的消息"""
		print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		""" 将里程表读数设置为指定的值,禁止将里程表读数往回调"""
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")
		
		
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())

my_new_car.update_odometer(23)
my_new_car.read_odometer()
03. 通过方法对属性的值进行递增

有时候需要将属性值递增特定的量,而不是将其设置为全新的值。假设我们购买了一辆二手车,且从购买到登记期间增加了100英里的里程。

下面的方法让我们能够传递这个增量,并相应地增大里程表读数:

class Car:
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性。"""
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0
		
	def get_descriptive_name(self):
		"""返回整洁的描述性信息"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		"""打印一条指出汽车里程的消息"""
		print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		""" 将里程表读数设置为指定的值,禁止将里程表读数往回调"""
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

	def increment_odometer(self, miles):
		"""将里程表读数增加指定的量"""
		self.odometer_reading += miles
		
		
my_used_car = Car('subaru', 'outback', 2015)
print(my_used_car.get_descriptive_name())

my_used_car.update_odometer(23_500)
my_used_car.read_odometer()

my_used_car.increment_odometer(100)
my_used_car.read_odometer()

8.3 继承

编写类时,并非总是要从空白开始。如果要编写的类是另一个现成类的特殊版本,可使用继承。一个类继承另一个类时,将自动获得另一个类的所有属性和方法。原有的类称为父类,而新类称为子类。子类继承了父类的所有属性和方法,同时还可以定义自己的属性和方法。

8.3.1 子类的方法__init__()

在既有类的基础上编写新类时,通常要调用父类的方法__init__()。这将初始化在父类__init__()方法中定义的所有属性,从而让子类包含这些属性。

例如,下面来模拟电动汽车。电动汽车是一种特殊的汽车,因此可在前面创建的Car类的基础上创建新类ElectricCar。这样就只需为电动汽车特有的属性和行为编写代码。

下面来创建ElectricCar类的一个简单版本,它具备Car类的所有功能:

class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0

	def get_descriptive_name(self):
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

	def increment_odometer(self, miles):
		self.odometer_reading += miles
		
class ElectricCar(Car):
	"""电动汽车的独特之处"""
	def __init__(self, make, model, year):
		"""初始化父类的属性"""
		super().__init__(make, model, year)		# 继承父类
		
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())

super()是一个特殊函数,让你能够调用父类的方法。这行代码让Python调用Car类的方法__init__(),让ElectricCar实例包含这个方法中定义的所有属性。父类也称为超类(superclass),名称super由此而来。
除方法__init___()外,电动汽车没有其他特有的属性和方法。

8.3.2 给子类定义属性和方法

让一个类继承另一个类后,就可以添加区分子类和父类所需的新属性和新方法了。
下面来添加一个电动汽车特有的属性(电瓶),以及一个描述该属性的方法。我们将存储电瓶容量,并编写一个打印电瓶描述的方法:

class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0

	def get_descriptive_name(self):
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

	def increment_odometer(self, miles):
		self.odometer_reading += miles
		
class ElectricCar(Car):
	"""电动汽车的独特之处"""
	def __init__(self, make, model, year):
		""" 初始化父类的属性,再初始化电动汽车特有的属性"""
		super().__init__(make, model, year)
		self.battery_size = 75
		
	def describe_battery(self):
		"""打印一条描述电瓶容量的消息"""
		print(f"This car has a {self.battery_size}-kWh battery.")
		
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()

对于ElectricCar类的特殊程度没有任何限制。模拟电动汽车时,可根据所需的准确程度添加任意数量的属性和方法。如果一个属性或方法是任何汽车都有的,而不是电动汽车特有的,就应将其加入到Car类而非ElectricCar类中。这样,使用Car类的人将获得相应的功能,而ElectricCar类只包含处理电动汽车特有属性和行为的代码。

8.3.3 重写父类的方法

对于父类的方法,只要它不符合子类模拟的实物的行为,都可以进行重写。为此,可在子类中定义一个与要重写的父类方法同名的方法。这样,Python将不会考虑这个父类方法,而只关注你在子类中定义的相应方法。

假设Car类有一个名为fill_gas_tank()的方法,它对全电动汽车来说毫无意义,因此你可能想重写它。下面演示了一种重写方式:

class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0

	def get_descriptive_name(self):
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

	def increment_odometer(self, miles):
		self.odometer_reading += miles
		
class ElectricCar(Car):
	"""电动汽车的独特之处"""
	def __init__(self, make, model, year):
		""" 初始化父类的属性,再初始化电动汽车特有的属性"""
		super().__init__(make, model, year)
		self.battery_size = 75
		
	def describe_battery(self):
		"""打印一条描述电瓶容量的消息"""
		print(f"This car has a {self.battery_size}-kWh battery.")
		
	def fill_gas_tank(self):
		"""电动汽车没有油箱"""
		print("This car doesn't need a gas tank!")

现在,如果有人对电动汽车调用方法fill_gas_tank(),Python将忽略Car类中的方法fill_gas_tank(),转而运行上述代码。使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。

8.3.4 将实例用作属性

使用代码模拟实物时,你可能会发现自己给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分提取出来,作为一个独立的类。可以将大型类拆分成多个协同工作的小类。

例如,不断给ElectricCar类添加细节时,我们可能发现其中包含很多专门针对汽车电瓶的属性和方法。在这种情况下,可将这些属性和方法提取出来,放到一个名为Battery的类中,并将一个Battery实例作为ElectricCar类的属性:

class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0

	def get_descriptive_name(self):
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

	def increment_odometer(self, miles):
		self.odometer_reading += miles
		
class Battery:
	"""一次模拟电动汽车电瓶的简单尝试"""
	def __init__(self, battery_size=75):
		"""初始化电瓶的属性。"""
		self.battery_size = battery_size
	
	def describe_battery(self):
		"""打印一条描述电瓶容量的消息"""
		print(f"This car has a {self.battery_size}-kWh battery.")
		
	def get_range(self):
		"""打印一条消息,指出电瓶的续航里程"""
		if self.battery_size == 75:
			range = 260
		elif self.battery_size == 100:
			range = 315
		print(f"This car can go about {range} miles on a full charge.")
		
class ElectricCar(Car):
	"""电动汽车的独特之处"""
	def __init__(self, make, model, year):
		""" 初始化父类的属性,再初始化电动汽车特有的属性"""
		super().__init__(make, model, year)
		self.battery = Battery()
		
my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()

8.4 导入类

随着不断给类添加功能,文件可能变得很长,即便妥善地使用了继承亦如此。为遵循Python的总体理念,应让文件尽可能整洁。Python在这方面提供了帮助,允许将类存储在模块中,然后在主程序中导入所需的模块。

8.4.1 导入单个类

下面来创建一个只包含Car类的模块。这让我们面临一个微妙的命名问题:这里已经有一个名为car.py的文件,但这个模块也应命名为car.py,因为它包含表示汽车的代码。我们将这样解决这个命名问题:将Car类存储在一个名为car.py的模块中,该模块将覆盖前面使用的文件car.py。从现在开始,使用该模块的程序都必须使用更具体的文件名,如my_car.py。下面是模块car.py,其中只包含Car类的代码:

"""一个可用于表示汽车的类"""
class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性"""
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0
	
	def get_descriptive_name(self):
		"""返回整洁的描述性名称"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		"""打印一条消息,指出汽车的里程"""
	print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		""" 将里程表读数设置为指定的值,拒绝将里程表往回调"""
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

		def increment_odometer(self, miles):
			"""将里程表读数增加指定的量"""
			self.odometer_reading += miles

下面来创建另一个文件my_car.py,在其中导入Car类并创建其实例:

from car import Car

my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23
my_new_car.read_odometer()

import语句让Python打开模块car并导入其中的Car类。这样,我们就可以使用Car类,就像它是在这个文件中定义的一样。

导入类是一种有效的编程方式。如果这个程序包含整个Class类,它该有多长啊!通过将这个类移到一个模块中并导入该模块,依然可以使用其所有功能,但主程序文件变得整洁而易于阅读了。这还让你能够将大部分逻辑存储在独立的文件中。确定类像你希望的那样工作后,就可以不管这些文件,而专注于主程序的高级逻辑了。

8.4.2 在一个模块中存储多个类

虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。Battery类和ElectricCar类都可帮助模拟汽车,下面将它们都加入模块car.py中:

"""一组用于表示燃油汽车和电动汽车的类。"""
class Car:
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		"""初始化描述汽车的属性"""
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 0
	
	def get_descriptive_name(self):
		"""返回整洁的描述性名称"""
		long_name = f"{self.year} {self.make} {self.model}"
		return long_name.title()

	def read_odometer(self):
		"""打印一条消息,指出汽车的里程"""
	print(f"This car has {self.odometer_reading} miles on it.")

	def update_odometer(self, mileage):
		""" 将里程表读数设置为指定的值,拒绝将里程表往回调"""
		if mileage >= self.odometer_reading:
			self.odometer_reading = mileage
		else:
			print("You can't roll back an odometer!")

		def increment_odometer(self, miles):
			"""将里程表读数增加指定的量"""
			self.odometer_reading += miles
			
class Battery:
	"""一次模拟电动汽车电瓶的简单尝试"""
	def __init__(self, battery_size=75):
		"""初始化电瓶的属性"""
		self.battery_size = battery_size
	
	def describe_battery(self):
		"""打印一条描述电瓶容量的消息"""
		print(f"This car has a {self.battery_size}-kWh battery.")
	
	def get_range(self):
		"""打印一条描述电瓶续航里程的消息"""
		if self.battery_size == 75:
			range = 260
		elif self.battery_size == 100:
			range = 315
		print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):
	"""模拟电动汽车的独特之处"""
	def __init__(self, make, model, year):
		""" 初始化父类的属性,再初始化电动汽车特有的属性"""
		super().__init__(make, model, year)
		self.battery = Battery()

现在,可以新建一个名为my_electric_car.py的文件,导入ElectricCar类,并创建一辆电动汽车了:

from car import ElectricCar
my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()

8.4.3 从一个模块中导入多个类

可根据需要在程序文件中导入任意数量的类。如果要在同一个程序中创建普通汽车和电动汽车,就需要将Car类和ElectricCar类都导入:

from car import Car, ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())

8.4.4 导入整个模块

还可以导入整个模块,再使用句点表示法访问需要的类。这种导入方式很简单,代码也易于阅读。因为创建类实例的代码都包含模块名,所以不会与当前文件使用的任何名称发生冲突。

下面的代码导入整个car模块,并创建一辆普通汽车和一辆电动汽车:

import car

my_beetle = car.Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())

my_tesla = car.ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())

8.4.5 导入模块中的所有类

要导入模块中的每个类,可使用下面的语法:

from module_name import *

不推荐使用这种导入方式,原因有二。第一,如果只看文件开头的import语句,就能清楚地知道程序使用了哪些类。然而这种导入方式没有明确地指出使用了模块中的哪些类。第二,这种方式还可能引发名称方面的迷惑。如果不小心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误。

需要从一个模块中导入很多类时,最好导入整个模块,并使用module_name.ClassName语法来访问类。这样做时,虽然文件开头并没有列出用到的所有类,但你清楚地知道在程序的哪些地方使用了导入的模块。这也避免了导入模块中的每个类可能引发的名称冲突。

8.4.6 在一个模块中导入另一个模块

有时候,需要将类分散到多个模块中,以免模块太大或在同一个模块中存储不相关的类。将类存储在多个模块中时,你可能会发现一个模块中的类依赖于另一个模块中的类。在这种情况下,可在前一个模块中导入必要的类。

下面将Car类存储在一个模块中,并将ElectricCar类和Battery类存储在另一个模块中。将第二个模块命名为electric_car.py(这将覆盖前面创建的文件electric_car.py),并将Battery类和ElectricCar类复制到这个模块中:

"""一组可用于表示电动汽车的类。"""from car import Car
class Battery:
	"""一次模拟电动汽车电瓶的简单尝试"""
	def __init__(self, battery_size=75):
		"""初始化电瓶的属性"""
		self.battery_size = battery_size
	
	def describe_battery(self):
		"""打印一条描述电瓶容量的消息"""
		print(f"This car has a {self.battery_size}-kWh battery.")
	
	def get_range(self):
		"""打印一条描述电瓶续航里程的消息"""
		if self.battery_size == 75:
			range = 260
		elif self.battery_size == 100:
			range = 315
		print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):
	"""模拟电动汽车的独特之处"""
	def __init__(self, make, model, year):
		""" 初始化父类的属性,再初始化电动汽车特有的属性"""
		super().__init__(make, model, year)
		self.battery = Battery()

ElectricCar类需要访问其父类Car。

8.4.7 使用别名

使用模块来组织项目代码时,可使用别名。导入类时,也可为其指定别名。

例如,要在程序中创建大量电动汽车实例,需要反复输入ElectricCar,非常烦琐。为避免这种烦恼,可在import语句中给ElectricCar指定一个别名:

from electric_car import ElectricCar as EC

现在每当需要创建电动汽车实例时,都可使用这个别名:

my_tesla = EC('tesla', 'roadster', 2019)

8.4.8 自定义工作流程

在组织大型项目的代码方面,Python提供了很多选项。熟悉所有这些选项很重要,这样你才能确定哪种项目组织方式是最佳的,并能理解别人开发的项目。

一开始应让代码结构尽可能简单。先尽可能在一个文件中完成所有的工作,确定一切都能正确运行后,再将类移到独立的模块中。如果你喜欢模块和文件的交互方式,可在项目开始时就尝试将类存储到模块中。先找出让你能够编写出可行代码的方式,再尝试改进代码。

8.5 Python标准库

Python标准库是一组模块,我们安装的Python都包含它。你现在对函数和类的工作原理已有大致的了解,可以开始使用其他程序员编写好的模块了。可以使用标准库中的任何函数和类,只需在程序开头包含一条简单的import语句即可。

下面来看看模块random,它在你模拟很多现实情况时很有用。
在这个模块中,一个有趣的函数是randint()。它将两个整数作为参数,并随机返回一个位于这两个整数之间(含)的整数。下面演示了如何生成一个位于1和6之间的随机整数:

from random import randint
randint(1, 6)	# 生成一个位于1和6之间的随机整数

在模块random中,另一个有用的函数是choice()。它将一个列表或元组作为参数,并随
机返回其中的一个元素:

from random import choice
players = ['charles', 'martina', 'michael', 'florence', 'eli']
first_up = choice(players)

8.6 类编码风格

你必须熟悉有些与类相关的编码风格问题,在编写的程序较复杂时尤其如此。类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。

对于每个类,都应紧跟在类定义后面包含一个文档字符串。这种文档字符串简要地描述类的功能,并遵循编写函数的文档字符串时采用的格式约定。每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。

可使用空行来组织代码,但不要滥用。在类中,可使用一个空行来分隔方法;而在模块中,可使用两个空行来分隔类。

需要同时导入标准库中的模块和你编写的模块时,先编写导入标准库模块的import语句,再添加一个空行,然后编写导入你自己编写的模块的import语句。在包含多条import语句的程序中,这种做法让人更容易明白程序使用的各个模块都来自何处。

9. 文件和异常

学习处理文件和保存数据可让你的程序使用起来更容易:用户将能够选择输入什么样的数据,以及在什么时候输入;用户使用程序做一些工作后,可将程序关闭,以后再接着往下做。学习处理异常可帮助应对文件不存在的情形,以及处理其他可能导致程序崩溃的问题。这让程序在面对错误的数据时更健壮,不管这些错误数据源自无意的错误,还是源自破坏程序的恶意企图。在本节学习的技能可提高程序的适用性、可用性和稳定性。

9.1 从文件中读取数据

文本文件可存储的数据量多得难以置信:天气数据、交通数据、社会经济数据、文学作品等。每当需要分析或修改存储在文件中的信息时,读取文件都很有用,对数据分析应用程序来说尤其如此。

例如,可以编写一个这样的程序:读取一个文本文件的内容,重新设置这些数据的格式并将其写入文件,让浏览器能够显示这些内容。

要使用文本文件中的信息,首先需要将信息读取到内存中。为此,可以一次性读取文件的全部内容,也可以以每次一行的方式逐步读取。

9.1.1 读取整个文件

要读取文件,需要一个包含几行文本的文件。下面首先创建一个文件(txt文本文件),文件名为pi_digits.txt,它包含精确到小数点后30位的圆周率值,且在小数点后每10位处换行:

3.1415926535
  8979323846
  2643383279

下面的程序打开并读取这个文件,再将其内容显示到屏幕上:

with open('pi_digits.txt') as file_object:
	contents = file_object.read()
	
print(contents)

函数open()接受一个参数:要打开的文件的名称。Python在当前执行的文件所在的目录中查找指定的文件。函数open()返回一个表示文件的对象。

关键字with在不再需要访问文件后将其关闭。在这个程序中,注意到我们调用了open(),但没有调用close()。也可以调用open()和close()来打开和关闭文件,但这样做时,如果程序存在bug导致方法close()未执行,文件将不会关闭。

未妥善关闭文件可能导致数据丢失或受损。但通过使用前面所示的结构,可让Python去确定:你只管打开文件,并在需要时使用它,Python自会在合适的时候自动将其关闭。

相比于原始文件,该输出唯一不同的地方是末尾多了一个空行。为何会多出这个空行呢?
因为read()到达文件末尾时返回一个空字符串,而将这个空字符串显示出来时就是一个空行。要删除多出来的空行,可在函数调用print()中使用rstrip():

with open('pi_digits.txt') as file_object:
	contents = file_object.read()
	
print(contents.rstrip())

9.1.2 文件路径

将类似于pi_digits.txt的简单文件名传递给函数open()时,Python将在当前执行的文件(即.py程序文件)所在的目录中查找。

根据你组织文件的方式,有时可能要打开不在程序文件所属目录中的文件。例如,你可能将程序文件存储在了文件夹python_work中,而该文件夹中有一个名为text_files的文件夹用于存储程序文件操作的文本文件。虽然文件夹text_files包含在文件夹python_work中,但仅向open()传递位于前者中的文件名称也不可行,因为Python只在文件夹python_work中查找,而不会在其子文件夹text_files中查找。要让Python打开不与程序文件位于同一个目录中的文件,需要提供文件路径,让Python到系统的特定位置去查找。
由于文件夹text_files位于文件夹python_work中,可以使用相对文件路径来打开其中的文件。相对文件路径让Python到指定的位置去查找,而该位置是相对于当前运行的程序所在目录的。例如,可这样编写代码:

with open('text_files/filename.txt') as file_object:

这行代码让Python到文件夹python_work下的文件夹text_files中去查找指定的.txt文件。

注意 显示文件路径时,Windows系统使用反斜杠(\)而不是斜杠(/),但在代码中依然可以使用斜杠。

还可以将文件在计算机中的准确位置告诉Python,这样就不用关心当前运行的程序存储在什么地方了。这称为绝对文件路径。在相对路径行不通时,可使用绝对路径。

例如,如果text_files并不在文件夹python_work中,而在文件夹other_files中,则向open()传递路
径’text_files/filename.txt’行不通,因为Python只在文件夹python_work中查找该位置。为明确指出希望Python到哪里去查找,需要提供完整的路径。

绝对路径通常比相对路径长,因此将其赋给一个变量,再将该变量传递给open()会有所帮助:

file_path = '/home/ehmatthes/other_files/text_files/_filename_.txt'
with open(file_path) as file_object:

通过使用绝对路径,可读取系统中任何地方的文件。就目前而言,最简单的做法是,要么将数据文件存储在程序文件所在的目录,要么将其存储在程序文件所在目录下的一个文件夹(如text_files)中。

注意 如果在文件路径中直接使用反斜杠,将引发错误,因为反斜杠用于对字符串中的字符进行转义。例如,对于路径"C:\path\to\file.txt",其中的\t将被解读为制表符。如果一定要使用反斜杠,可对路径中的每个反斜杠都进行转义,如"C:\path\to\file.txt"。

9.1.3 逐行读取

读取文件时,常常需要检查其中的每一行:可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。例如,你可能要遍历一个包含天气数据的文件,并使用天气描述中包含sunny字样的行。在新闻报道中,你可能会查找包含标签的行,并按特定的格式设置它。

要以每次一行的方式检查文件,可对文件对象使用for循环:

filename = 'pi_digits.txt'

with open(filename) as file_object:
for line in file_object:
	print(line)

使用关键字with,让Python负责妥善地打开和关闭文件。

打印每一行时,发现空白行更多了:
为何会出现这些空白行呢?因为在这个文件中,每行的末尾都有一个看不见的换行符,而函数调用print()也会加上一个换行符,因此每行末尾都有两个换行符:一个来自文件,另一个来自函数调用print()。要消除这些多余的空白行,可在函数调用print()中使用rstrip():

filename = 'pi_digits.txt'

with open(filename) as file_object:
for line in file_object:
	print(line.rstrip())

9.1.4 创建一个包含文件各行内容的列表

使用关键字with时,open()返回的文件对象只在with代码块内可用。如果要在with代码块外访问文件的内容,可在with代码块内将文件的各行存储在一个列表中,并在with代码块外使用该列表:可以立即处理文件的各个部分,也可以推迟到程序后面再处理。

filename = 'pi_digits.txt'

with open(filename) as file_object:
	lines = file_object.readlines()

for line in lines:
	print(line.rstrip())

方法readlines()从文件中读取每一行,并将其存储在一个列表中。

9.1.5 使用文件的内容

将文件读取到内存中后,就能以任何方式使用这些数据了。下面以简单的方式使用圆周率的值。首先,创建一个字符串,它包含文件中存储的所有数字,且没有任何空格:

filename = 'pi_digits.txt'

with open(filename) as file_object:
	lines = file_object.readlines()
	
pi_string = ''
for line in lines:
	pi_string += line.strip()
	
print(pi_string)
print(len(pi_string))

注意 读取文本文件时,Python将其中的所有文本都解读为字符串。如果读取的是数,并要将其作为数值使用,就必须使用函数int()将其转换为整数或使用函数float()将其转换为浮点数。

9.1.6 包含一百万位的大型文件

前面分析的都是一个只有三行的文本文件,但这些代码示例也可处理大得多的文件。如果我们有一个文本文件,其中包含精确到小数点后1 000 000位而不是30位的圆周率值,也可创建一个包含所有这些数字的字符串。为此,无须对前面的程序做任何修改,只要将这个文件传递给它即可。

filename = 'pi_million_digits.txt'

with open(filename) as file_object:
	lines = file_object.readlines()
	
pi_string = ''
for line in lines:
	pi_string += line.strip()
	
print(f"{pi_string[:52]}...")	# 仅显示后50位
print(len(pi_string))

对于可处理的数据量,Python没有任何限制。只要系统的内存足够多,想处理多少数据都可以。

9.1.7 圆周率值中包含你的生日吗

我一直想知道自己的生日是否包含在圆周率值中。下面来扩展刚才编写的程序,以确定某个人的生日是否包含在圆周率值的前1 000 000位中。为此,可将生日表示为一个由数字组成的字符串,再检查这个字符串是否包含在pi_string中:

filename = 'pi_million_digits.txt'

with open(filename) as file_object:
	lines = file_object.readlines()
	
pi_string = ''
for line in lines:
	pi_string += line.strip()
	
birthday = input("Enter your birthday, in the form mmddyy: ")
if birthday in pi_string:
	print("Your birthday appears in the first million digits of pi!")
else:
	print("Your birthday does not appear in the first million digits of pi.")

9.2 写入文件

保存数据的最简单的方式之一是将其写入文件中。通过将输出写入文件,即便关闭包含程序输出的终端窗口,这些输出也依然存在:可以在程序结束运行后查看这些输出,可以与别人分享输出文件,还可以编写程序来将这些输出读取到内存中并进行处理。

9.2.1 写入空文件

要将文本写入文件,你在调用open()时需要提供另一个实参,告诉Python你要写入打开的文件。为明白其中的工作原理,我们来将一条简单的消息存储到文件中,而不是将其打印到屏幕上:

filename = 'programming.txt'

with open(filename, 'w') as file_object:
	file_object.write("I love programming.")

打开文件时,可指定读取模式(‘r’)、写入模式(‘w’)、附加模式(‘a’)或读写模式(‘r+’)。如果省略了模式实参,Python将以默认的只读模式打开文件。

如果要写入的文件不存在,函数open()将自动创建它。然而,以写入模式(‘w’)打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件的内容。

注意 Python只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数str()将其转换为字符串格式。

9.2.2 写入多行

函数write()不会在写入的文本末尾添加换行符,因此如果写入多行时没有指定换行符,文件看起来可能不是你希望的那样:

filename = 'programming.txt'

with open(filename, 'w') as file_object:
	file_object.write("I love programming.")
	file_object.write("I love creating new games.")

如果你打开programming.txt,将发现两行内容挤在一起:
要让每个字符串都单独占一行,需要在方法调用write()中包含换行符:

filename = 'programming.txt'

with open(filename, 'w') as file_object:
	file_object.write("I love programming.\n")
	file_object.write("I love creating new games.\n")

像显示到终端的输出一样,还可以使用空格、制表符和空行来设置这些输出的格式。

9.2.3 附加到文件

如果要给文件添加内容,而不是覆盖原有的内容,可以以附加模式打开文件。以附加模式打开文件时,Python不会在返回文件对象前清空文件的内容,而是将写入文件的行添加到文件末尾。如果指定的文件不存在,Python将为你创建一个空文件。

filename = 'programming.txt'

with open(filename, 'a') as file_object:
	file_object.write("I also love finding meaning in large datasets.\n")
	file_object.write("I love creating apps that can run in a browser.\n")

打开文件时指定了实参’a’,以便将内容附加到文件末尾,而不是覆盖文件原来的内容。

9.3 异常

Python使用称为异常的特殊对象来管理程序执行期间发生的错误。每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行;如果未对异常进行处理,程序将停止并显示traceback,其中包含有关异常的报告。

异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用try-except代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。

9.3.1 处理ZeroDivisionError异常

下面来看一种导致Python引发异常的简单错误。你可能知道,不能用数除以0,但还是让Python这样做:

print(5/0)

显然,Python无法这样做,因此你将看到一个traceback:

Traceback (most recent call last):
File "division_calculator.py", line 1, in <module>
print(5/0)
ZeroDivisionError: division by zero

ZeroDivisionError是个异常对象。Python无法按你的要求做时,就会创建这种对象。在这种情况下,Python将停止运行程序,并指出引发了哪种异常,而我们可根据这些信息对程序进行修改。下面来告诉Python,发生这种错误时怎么办。

9.3.2 使用try-except代码块

当你认为可能会发生错误时,可编写一个try-except代码块来处理可能引发的异常。你让Python尝试运行一些代码,并告诉它如果这些代码引发了指定的异常该怎么办。处理ZeroDivisionError异常的try-except代码块类似于下面这样:

try:
	print(5/0)
except ZeroDivisionError:
	print("You can't divide by zero!")

将导致错误的代码行print(5/0)放在一个try代码块中。如果try代码块中的代码运行起来没有问题,Python将跳过except代码块;如果try代码块中的代码导致了错误,Python将查找与之匹配的except代码块并运行其中的代码。

如果try-except代码块后面还有其他代码,程序将接着运行,因为已经告诉了Python如何处理这种错误。

9.3.3 使用异常避免崩溃

发生错误时,如果程序还有工作尚未完成,妥善地处理错误就尤其重要。这种情况经常会出现在要求用户提供输入的程序中;如果程序能够妥善地处理无效输入,就能再提示用户提供有效输入,而不至于崩溃。

下面来创建一个只执行除法运算的简单计算器:

print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")

while True:
	first_number = input("\nFirst number: ")
	if first_number == 'q':
		break
	second_number = input("Second number: ")
	if second_number == 'q':
		break
	answer = int(first_number) / int(second_number)
	print(answer)

程序崩溃可不好,但让用户看到traceback也不是个好主意。不懂技术的用户会被搞糊涂,怀有恶意的用户还会通过traceback获悉你不想他知道的信息。例如,他将知道你的程序文件的名称,还将看到部分不能正确运行的代码。有时候,训练有素的攻击者可根据这些信息判断出可对你的代码发起什么样的攻击。

9.3.4 else代码块

通过将可能引发错误的代码放在try-except代码块中,可提高程序抵御错误的能力。错误是执行除法运算的代码行导致的,因此需要将它放到try-except代码块中。

这个示例还包含一个else代码块。依赖try代码块成功执行的代码都应放到else代码块中:

print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")

while True:
	first_number = input("\nFirst number: ")
	if first_number == 'q':
		break
	second_number = input("Second number: ")
	if second_number == 'q':
		break
	answer = int(first_number) / int(second_number)
	print(answer)
	if second_number == 'q':
		break
	try:
		answer = int(first_number) / int(second_number)
	except ZeroDivisionError:
		print("You can't divide by 0!")
	else:
		print(answer)

如果try代码块因除零错误而失败,就打印一条友好的消息,告诉用户如何避免这种错误。程序继续运行,用户根本看不到traceback。

try-except-else代码块的工作原理大致如下。Python尝试执行try代码块中的代码,只有可能引发异常的代码才需要放在try语句中。有时候,有一些仅在try代码块成功执行时才需要运行的代码,这些代码应放在else代码块中。except代码块告诉Python,如果尝试运行try代码块中的代码时引发了指定的异常该怎么办。

通过预测可能发生错误的代码,可编写健壮的程序。它们即便面临无效数据或缺少资源,也能继续运行,从而抵御无意的用户错误和恶意的攻击。

9.3.5 处理FileNotFoundError异常

使用文件时,一种常见的问题是找不到文件:查找的文件可能在其他地方,文件名可能不正确,或者这个文件根本就不存在。对于所有这些情形,都可使用try-except代码块以直观的方式处理。

我们来尝试读取一个不存在的文件。下面的程序尝试读取文件alice.txt的内容,但该文件没有存储在alice.py所在的目录中:

filename = 'alice.txt'

with open(filename, encoding='utf-8') as f:
	contents = f.read()

相比于前面的文件打开方式,这里有两个不同之处。一是使用变量f来表示文件对象,这是一种常见的做法。二是给参数encoding指定了值,在系统的默认编码与要读取文件使用的编码不一致时,必须这样做。

Python无法读取不存在的文件,因此它引发一个异常:

Traceback (most recent call last):
	File "alice.py", line 3, in <module>
		with open(filename, encoding='utf-8') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'

上述traceback的最后一行报告了FileNotFoundError异常,这是Python找不到要打开的文件时创建的异常。在本例中,这个错误是函数open()导致的。因此,要处理这个错误,必须将try语句放在包含open()的代码行之前:

filename = 'alice.txt'

try:
	with open(filename, encoding='utf-8') as f:
		contents = f.read()
except FileNotFoundError:
	print(f"Sorry, the file {filename} does not exist.")

在本例中,try代码块引发了FileNotFoundError异常,因此Python找到与该错误匹配的except代码块,并运行其中的代码。最终的结果是显示一条友好的错误消息,而不是traceback。

9.3.6 分析文本

你可以分析包含整本书的文本文件。很多经典文学作品都是简单以文本文件的形式提供的,因为它们不受版权限制。本节使用的文本来自古登堡计划,该计划提供了一系列不受版权限制的文学作品。如果你要在编程项目中使用文学文本,这是一个很不错的资源。

下面来提取童话《爱丽丝漫游奇境记》(Alice in Wonderland)的文本,并尝试计算它包含多少个单词。我们将使用方法split(),它能根据一个字符串创建一个单词列表。下面是对只包含童话名"Alice in Wonderland"的字符串调用方法split()的结果:

title = "Alice in Wonderland"
title.split()

**方法split()以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。**结果是一个包含字符串中所有单词的列表,虽然有些单词可能包含标点。为计算《爱丽丝漫游奇境记》包含多少个单词,我们将对整篇小说调用split(),再计算得到的列表包含多少个元素,从而确定整篇童话大致包含多少个单词:

filename = 'alice.txt'

try:
	with open(filename, encoding='utf-8') as f:
		contents = f.read()
except FileNotFoundError:
	print(f"Sorry, the file {filename} does not exist.")
else:
	# 计算该文件大致包含多少个单词
	words = contents.split()
	num_words = len(words)
	print(f"The file {filename} has about {num_words} words.")

9.3.7 使用多个文件

下面多分析几本书。这此之前,先将这个程序的大部分代码移到一个名为count_words()的函数中。这样,对多本书进行分析时将更容易:

def count_words(filename):
	"""计算一个文件大致包含多少个单词"""
	try:
		with open(filename, encoding='utf-8') as f:
			contents = f.read()
	except FileNotFoundError:
			print(f"Sorry, the file {filename} does not exist.")
	else:
		words = contents.split()
		num_words = len(words)
		print(f"The file {filename} has about {num_words} words.")
		
filename = 'alice.txt'
count_words(filename)

现在可以编写一个简单的循环,计算要分析的任何文本包含多少个单词了。为此,将要分析的文件的名称存储在一个列表中,然后对列表中的每个文件调用count_words()。我们将尝试计算《爱丽丝漫游奇境记》《悉达多》(Siddhartha)、《白鲸》(Moby Dick)和《小妇人》(Little Women)分别包含多少个单词。

def count_words(filename):
	"""计算一个文件大致包含多少个单词"""
	try:
		with open(filename, encoding='utf-8') as f:
			contents = f.read()
	except FileNotFoundError:
			print(f"Sorry, the file {filename} does not exist.")
	else:
		words = contents.split()
		num_words = len(words)
		print(f"The file {filename} has about {num_words} words.")
		
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)

使用try-except代码块提供了两个重要的优点:避免用户看到traceback,以及让程序继续分析能够找到的其他文件。如果不捕获因找不到siddhartha.txt而引发的FileNotFoundError异常,用户将看到完整的traceback,而程序将在尝试分析《悉达多》后停止运行。它根本不会分析《白鲸》和《小妇人》。

9.3.8 静默失败

在前一个示例中,我们告诉用户有一个文件找不到。但并非每次捕获到异常都需要告诉用户,有时候你希望程序在发生异常时保持静默,就像什么都没有发生一样继续运行。要让程序静默失败,可像通常那样编写try代码块,但在except代码块中明确地告诉Python什么都不要做。

Python有一个pass语句,可用于让Python在代码块中什么都不要做:

def count_words(filename):
	"""计算一个文件大致包含多少个单词"""
	try:
		with open(filename, encoding='utf-8') as f:
			contents = f.read()
	except FileNotFoundError:
			pass
	else:
		words = contents.split()
		num_words = len(words)
		print(f"The file {filename} has about {num_words} words.")
		
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)

现在,出现FileNotFoundError异常时,将执行except代码块中的代码,但什么都不会发生。这种
错误发生时,不会出现traceback,也没有任何输出。用户将看到存在的每个文件包含多少个单词,但没有任何迹象表明有一个文件未找到。

pass语句还充当了占位符,提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。例如,在这个程序中,我们可能决定将找不到的文件的名称写入文件missing_files.txt中。用户看不到这个文件,但我们可以读取它,进而处理所有找不到文件的问题。

9.3.9 决定报告哪些错误

该在什么情况下向用户报告错误?又该在什么情况下静默失败呢?如果用户知道要分析哪些文件,他们可能希望在有文件却没有分析时出现一条消息来告知原因。如果用户只想看到结果,并不知道要分析哪些文件,可能就无须在有些文件不存在时告知他们。向用户显示他不想看到的信息可能会降低程序的可用性。Python的错误处理结构让你能够细致地控制与用户分享错误信息的程度,要分享多少信息由你决定。

编写得很好且经过详尽测试的代码不容易出现内部错误,如语法或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络链接,就有可能出现异常。凭借经验可判断该在程序的什么地方包含异常处理块,以及出现错误时该向用户提供多少相关的信息。

9.4 存储数据

很多程序都要求用户输入某种信息,如让用户存储游戏首选项或提供要可视化的数据。不管关注点是什么,程序都把用户提供的信息存储在列表和字典等数据结构中。用户关闭程序时,几乎总是要保存他们提供的信息。

一种简单的方式是使用模块json来存储数据。模块json让你能够将简单的Python数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。你还可以使用json在Python程序之间分享数据。更重要的是,JSON数据格式并非Python专用的,这让你能够将以JSON格式存储的数据与使用其他编程语言的人分享。这是一种轻便而有用的格式,也易于学习。

注意 JSON(JavaScript Object Notation)格式最初是为JavaScript开发的,但随后成了一种常见格式,被包括Python在内的众多语言采用。

9.4.1 使用json.dump()和json.load()

我们来编写一个存储一组数的简短程序,再编写一个将这些数读取到内存中的程序。第一个程序将使用json.dump()来存储这组数,而第二个程序将使用json.load()。

函数json.dump()接受两个实参:要存储的数据,以及可用于存储数据的文件对象。下面演示了如何使用json.dump()来存储数字列表:

import json

numbers = [2, 3, 5, 7, 11, 13]

filename = 'numbers.json'	# 指定了要将该数字列表存储到哪个文件中,通常使用文件扩展名.json来指出文件存储的数据为JSON格式。
with open(filename, 'w') as f:
	json.dump(numbers, f)

下面再编写一个程序,使用json.load()将列表读取到内存中:

import json

filename = 'numbers.json'	# 确保读取的是前面写入的文件
with open(filename) as f:	# 这次以读取方式打开该文件,因为Python只需要读取它
	numbers = json.load(f)	# 使用函数json.load()加载存储在numbers.json中的信息,并将其赋给变量numbers。
	
print(numbers)

这是一种在程序之间共享数据的简单方式。

9.4.2 保存和读取用户生成的数据

使用json保存用户生成的数据大有裨益,因为如果不以某种方式存储,用户的信息会在程序停止运行时丢失。

下面来看一个这样的例子:提示用户首次运行程序时输入自己的名字,并在再次运行程序时记住他。

import json

username = input("What is your name? ")

filename = 'username.json'
with open(filename, 'w') as f:
	json.dump(username, f)
	print(f"We'll remember you when you come back, {username}!")

现在再编写一个程序,向已存储了名字的用户发出问候:

import json

filename = 'username.json'

with open(filename) as f:
	username = json.load(f)
	print(f"Welcome back, {username}!")

需要将这两个程序合并到一个程序(remember_me.py)中。这个程序运行时,将尝试从文件username.json中获取用户名。因此,首先编写一个尝试恢复用户名的try代码块。如果这个文件不存在,就在except代码块中提示用户输入用户名,并将其存储到username.json中,以便程序再次运行时能够获取:

import json
# 如果以前存储了用户名,就加载它
# 否则,提示用户输入用户名并存储它

filename = 'username.json'

try:
	with open(filename) as f:
		username = json.load(f)
except FileNotFoundError:
	username = input("What is your name? ")
	with open(filename, 'w') as f:
		json.dump(username, f)
		print(f"We'll remember you when you come back, {username}!")
else:
	print(f"Welcome back, {username}!")

无论执行的是except还是else代码块,都将显示用户名和合适的问候语。

9.4.3 重构

你经常会遇到这样的情况:代码能够正确地运行,但通过将其划分为一系列完成具体工作的函数,还可以改进。这样的过程称为重构。重构让代码更清晰、更易于理解、更容易扩展。

要重构remember_me.py,可将其大部分逻辑放到一个或多个函数中。remember_me.py的重点是问候用户,因此将其所有代码都放到一个名为greet_user()的函数中:

import json

def greet_user():
	"""问候用户,并指出其名字"""
	filename = 'username.json'
	try:
		with open(filename) as f:
			username = json.load(f)
	except FileNotFoundError:
		username = input("What is your name? ")
		with open(filename, 'w') as f:
			json.dump(username, f)
			print(f"We'll remember you when you come back, {username}!")
	else:
		print(f"Welcome back, {username}!")
		
greet_user()

下面来重构greet_user(),减少其任务。为此,首先将获取已存储用户名的代码移到另一个函数中:

import json

def get_stored_username():
"""如果存储了用户名,就获取它"""
	filename = 'username.json'
	try:
		with open(filename) as f:
			username = json.load(f)
	except FileNotFoundError:
		return None
	else:
		return username
		
def greet_user():
	"""问候用户,并指出其名字"""
	username = get_stored_username()
	if username:
		print(f"Welcome back, {username}!")
	else:
		username = input("What is your name? ")
		filename = 'username.json'
		with open(filename, 'w') as f:
			json.dump(username, f)
			print(f"We'll remember you when you come back, {username}!")
		
greet_user()

还需要重构greet_user()中的另一个代码块,将没有存储用户名时提示用户输入的代码放在一个独立的函数中:

import json

def get_stored_username():
"""如果存储了用户名,就获取它"""
	filename = 'username.json'
	try:
		with open(filename) as f:
			username = json.load(f)
	except FileNotFoundError:
		return None
	else:
		return username

def get_new_username():
	"""提示用户输入用户名"""
	username = input("What is your name? ")
	filename = 'username.json'
	with open(filename, 'w') as f:
		json.dump(username, f)
	return username		
	
def greet_user():
	"""问候用户,并指出其名字"""
	username = get_stored_username()
	if username:
		print(f"Welcome back, {username}!")
	else:
		username = get_new_username()
		print(f"We'll remember you when you come back, {username}!")
		
greet_user()

在remember_me.py的这个最终版本中,每个函数都执行单一而清晰的任务。我们调用greet_user(),它打印一条合适的消息:要么欢迎老用户回来,要么问候新用户。为此,它首先调用get_stored_username(),该函数只负责获取已存储的用户名(如果存储了的话)。最后在必要时调用get_new_username(),该函数只负责获取并存储新用户的用户名。要编写出清晰而易于维护和扩展的代码,这种划分必不可少。

10. 测试代码

编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。在程序中添加新代码时,也可以对其进行测试,确认不会破坏程序既有的行为。程序员都会犯错,因此每个程序员都必须经常测试其代码,在用户发现问题前找出它们。这里将介绍如何使用Python模块unittest中的工具来测试代码,编写测试用例。

10.1 测试函数

要学习测试,必须有要测试的代码。下面是一个简单的函数name_function.py,它接受名和姓并返回整洁的姓名:

def get_formatted_name(first, last):
	"""生成整洁的姓名"""
	full_name = f"{first} {last}"
	return full_name.title()

函数get_formatted_name()将名和姓合并成姓名:在名和姓之间加上一个空格并将其首字母大写,再返回结果。为核实get_formatted_name()像期望的那样工作,我们来编写一个使用该函数的程序。

from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
	first = input("\nPlease give me a first name: ")
	if first == 'q':
		break
	last = input("Please give me a last name: ")
	if last == 'q':
		break
	formatted_name = get_formatted_name(first, last)
	print(f"\tNeatly formatted name: {formatted_name}.")

这个程序从name_function.py中导入get_formatted_name()。用户可输入一系列名和姓,并看到格式整洁的姓名。

现在假设要修改get_formatted_name(),使其还能够处理中间名。这样做时,要确保不破坏这个函数处理只含有名和姓的姓名的方式。为此,可在每次修改get_formatted_name()后都进行测试:运行程序names.py,并输入像Janis Joplin这样的姓名。不过这太烦琐了。

所幸Python提供了一种自动测试函数输出的高效方式。倘若对get_formatted_name()进行自动测试,就能始终确信当提供测试过的姓名时,该函数都能正确工作。

10.1.1 单元测试和测试用例

Python标准库中的模块unittest提供了代码测试工具。单元测试用于核实函数的某个方面没有问题。测试用例是一组单元测试,它们一道核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖的测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要进行全覆盖测试可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。

10.1.2 可通过的测试

你需要一段时间才能习惯创建测试用例的语法,但创建测试用例之后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest和要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。

下面的测试用例只包含一个方法,它检查函数get_formatted_name()在给定名和姓时能
否正确工作:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
	"""测试name_function.py"""
	def test_first_last_name(self):
	"""能够正确地处理像Janis Joplin这样的姓名吗?"""
		formatted_name = get_formatted_name('janis', 'joplin')
		self.assertEqual(formatted_name, 'Janis Joplin')	# 将formatted_name的值与字符串'Janis Joplin'比较
		
if __name__ == '__main__':
	unittest.main()

创建了一个名为NamesTestCase的类,用于包含一系列针对get_formatted_name()的单元测试。这个类可以随意命名,但最好让它看起来与要测试的函数相关并包含Test字样。这个类必须继承unittest.TestCase类,这样Python才知道如何运行你编写的测试。

NamesTestCase只包含一个方法,用于测试get_formatted_name()的一个方面。将该方法命名为test_first_last_name(),因为要核实的是只有名和姓的姓名能否被正确格式化。运行test_name_function.py时,所有以test_打头的方法都将自动运行。在这个方法中,调用了要测试的函数。在本例中,使用实参’janis’和’joplin’调用get_formatted_name(),并将结果赋给变量formatted_name。

这里使用了unittest类最有用的功能之一:断言方法。断言方法核实得到的结果是否与期望的结果一致。在这里,我们知道get_formatted_name()应返回名和姓首字母大写且之间有一个空格的姓名,因此期望formatted_name的值为Janis Joplin。为检查是否确实如此,我们调用unittest的方法assertEqual(),并向它传递formatted_name和’Janis Joplin’。

我们将直接运行这个文件,但需要指出的是,很多测试框架都会先导入测试文件再运行。导入文件时,解释器将在导入的同时执行它。if代码块检查特殊变量__name__,这个变量是在程序执行时设置的。如果这个文件作为主程序执行,变量__name__将被设置为’main’。在这里,调用unittest.main()来运行测试用例。如果这个文件被测试框架导入,变量__name__的值将不是’main’,因此不会调用unittest.main()。

10.1.3 未通过的测试

测试未通过时结果是什么样的呢?我们来修改get_formatted_name(),使其能够处理中间名,但同时故意让该函数无法正确处理像Janis Joplin这样只有名和姓的姓名。

下面是函数get_formatted_name()的新版本,name_function.py要求通过一个实参指定中间名。

def get_formatted_name(first, middle, last):
	"""生成整洁的姓名"""
	full_name = f"{first} {middle} {last}"
	return full_name.title()

这个版本应该能够正确处理包含中间名的姓名,但对其进行测试时,我们发现它不再能正确处理只有名和姓的姓名。返回信息里面包含很多信息,因为测试未通过时,需要让你知道的事情可能有很多。

10.1.4 测试未通过时怎么办

测试未通过时怎么办呢?如果你检查的条件没错,测试通过意味着函数的行为是对的,而测试未通过意味着编写的新代码有错。因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚刚对函数所做的修改,找出导致函数行为不符合预期的修改。

在本例中,get_formatted_name()以前只需要名和姓两个实参,但现在要求提供名、中间名和姓。新增的中间名参数是必不可少的,这导致get_formatted_name()的行为不符合预期。就这里而言,最佳的选择是让中间名变为可选的。这样做后,使用类似于Janis Joplin的姓名进行测试时,测试就又能通过了,而且也可以接受中间名。

下面来修改get_formatted_name(),将中间名设置为可选的,然后再次运行这个测试用例。如果
通过了,就接着确认该函数能够妥善地处理中间名。

要将中间名设置为可选的,可在函数定义中将形参middle移到形参列表末尾,并将其默认值指定为一个空字符串。还需要添加一个if测试,以便根据是否提供了中间名相应地创建姓名:

def get_formatted_name(first, last, middle=''):
	"""生成整洁的姓名"""
	if middle:
		full_name = f"{first} {middle} {last}"
	else:
		full_name = f"{first} {last}"
	return full_name.title()

在get_formatted_name()的这个新版本中,中间名是可选的。如果向该函数传递了中间名,姓名将包含名、中间名和姓,否则姓名将只包含名和姓。现在,对于两种不同的姓名,这个函数都应该能够正确地处理。

现在测试用例通过了。这意味着这个函数又能正确处理像Janis Joplin这样的姓名了,而且我们无须手工测试这个函数。这个函数之所以很容易修复,是因为未通过的测试让我们得知新代码破坏了函数原来的行为。

10.1.5 添加新测试

确定get_formatted_name()又能正确处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,在NamesTestCase类中再添加一个方法:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
	"""测试name_function.py"""
	def test_first_last_name(self):
		"""能够正确地处理像Janis Joplin这样的姓名吗?"""
		formatted_name = get_formatted_name('janis', 'joplin')
		self.assertEqual(formatted_name, 'Janis Joplin')	# 将formatted_name的值与字符串'Janis Joplin'比较
		
	def test_first_last_middle_name(self):
		"""能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
		formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
		self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')
	
if __name__ == '__main__':
	unittest.main()

将该方法命名为test_first_last_middle_name()。方法名必须以test_打头,这样它才会在我们运行test_name_function.py时自动运行。这个方法名清楚地指出了它测试的是get_formatted_name()的哪个行为。这样,如果该测试未通过,我们就能马上知道受影响的是哪种类型的姓名。可以在TestCase类中使用很长的方法名,而且这些方法名必须是描述性的,这样你才能看懂测试未通过时的输出。这些方法由Python自动调用,你根本不用编写调用它们的代码。

10.2 测试类

前半部分编写了针对单个函数的测试,下面来编写针对类的测试。很多程序中都会用到类,因此证明你的类能够正确工作大有裨益。如果针对类的测试通过了,就能确信对类所做的改进没有意外地破坏其原有的行为。

10.2.1 各种断言方法

Python在unittest.TestCase类中提供了很多断言方法。前面说过,断言方法检查你认为应该满足的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认,可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python将引发异常。

表10-1描述了6个常用的断言方法。使用这些方法可核实返回的值等于或不等于预期的值,返回的值为True或False,以及返回的值在列表中或不在列表中。只能在继承unittest.TestCase的类中使用这些方法,随后来看看如何在测试类时使用其中之一。
在这里插入图片描述

10.2.2 一个要测试的类

类的测试与函数的测试相似,你所做的大部分工作是测试类中方法的行为。不过还是存在一些不同之处。

下面编写一个要测试的类survey.py。来看一个帮助管理匿名调查的类:

class AnonymousSurvey:
	"""收集匿名调查问卷的答案"""
	def __init__(self, question):
		"""存储一个问题,并为存储答案做准备"""
		self.question = question
		self.responses = []		# 创建了一个空列表,用于存储答案
		
 	def show_question(self):
		"""显示调查问卷"""
		print(self.question)	# 打印调查问题
	
	def store_response(self, new_response):
		"""存储单份调查答卷"""
		self.responses.append(new_response)		# 在答案列表中添加新答案
	
	def show_results(self):
		"""显示收集到的所有答卷"""
		print("Survey results:")
		for response in self.responses:
			print(f"- {response}")

为证明AnonymousSurvey类能够正确工作,编写一个使用它的程序language_survey.py:

from survey import AnonymousSurvey

# 定义一个问题,并创建一个调查
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# 显示问题并存储答案
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
	response = input("Language: ")
	if response == 'q':
		break
	my_survey.store_response(response)
	
# 显示调查结果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

这个程序定义了一个问题(“What language did you first learn to speak?”),并使用该问题创建了一个AnonymousSurvey对象。接下来,这个程序调用show_question()来显示问题,并提示用户输入答案。在收到每个答案的同时将其存储起来。用户输入所有答案(输入q要求退出)后,调用show_results()来打印调查结果。

AnonymousSurvey类可用于进行简单的匿名调查。假设我们将它放在了模块survey中,并想进行改进:让每位用户都可输入多个答案;编写一个方法,只列出不同的答案并指出每个答案出现了多少次;再编写一个类,用于管理非匿名调查。

进行上述修改存在风险,可能影响AnonymousSurvey类的当前行为。例如,允许每位用户输入多个答案时,可能会不小心修改处理单个答案的方式。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。

10.2.3 测试AnonymousSurvey类

下面来编写一个测试,对AnonymousSurvey类的行为的一个方面进行验证:如果用户面对调查问题只提供一个答案,这个答案也能被妥善地存储。为此,我们将在这个答案被存储后,使用方法assertIn() 来核实它确实在答案列表中test_survey.py:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
	"""针对AnonymousSurvey类的测试"""
	def test_store_single_response(self):
		"""测试单个答案会被妥善地存储"""
		question = "What language did you first learn to speak?"
		my_survey = AnonymousSurvey(question)
		my_survey.store_response('English')
		self.assertIn('English', my_survey.responses)
		
if __name__ == '__main__':
unittest.main()

首先导入模块unittest和要测试的类AnonymousSurvey。将测试用例命名为TestAnonymousSurvey,它也继承了unittest.TestCase。第一个测试方法验证:调查问题的单个答案被存储后,会包含在调查结果列表中。对于这个方法,一个不错的描述性名称是test_store_single_response()。如果这个测试未通过,我们就能通过输出中的方法名得知,在存储单个调查答案方面存在问题。要测试类的行为,需要创建其实例。使用问题"What language did you first learn to speak?"创建一个名为my_survey的实例,然后使用方法store_response()存储单个答案English。接下来,检查English是否包含在列表my_survey.responses中,以核实这个答案是否被妥善地存储了。

下面来核实当用户提供三个答案时,它们也将被妥善地存储。为此,在TestAnonymousSurvey中再添加一个方法:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
	"""针对AnonymousSurvey类的测试"""
	def test_store_single_response(self):
		"""测试单个答案会被妥善地存储"""
		question = "What language did you first learn to speak?"
		my_survey = AnonymousSurvey(question)
		my_survey.store_response('English')
		self.assertIn('English', my_survey.responses)
	
	def test_store_three_responses(self):
		"""测试三个答案会被妥善地存储"""
		question = "What language did you first learn to speak?"
		my_survey = AnonymousSurvey(question)
		responses = ['English', 'Spanish', 'Mandarin']
		for response in responses:
			my_survey.store_response(response)
		for response in responses:
			self.assertIn(response, my_survey.responses)
	
if __name__ == '__main__':
unittest.main()

10.2.4 方法setUp()

在前面的test_survey.py中,我们在每个测试方法中都创建了一个AnonymousSurvey实例,并在每个方法中都创建了答案。unittest.TestCase类包含的方法setUp()让我们只需创建这些对象一次,就能在每个测试方法中使用。如果在TestCase类中包含了方法setUp(),Python将先运行它,再运行各个以test_打头的方法。这样,在编写的每个测试方法中,都可使用在方法setUp()中创建的对象。

下面使用setUp()来创建一个调查对象和一组答案,供方法test_store_single_response()和test_store_three_responses()使用:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
	"""针对AnonymousSurvey类的测试"""
	def setUp(self):
		""" 创建一个调查对象和一组答案,供使用的测试方法使用"""
		question = "What language did you first learn to speak?"
		self.my_survey = AnonymousSurvey(question)
		self.responses = ['English', 'Spanish', 'Mandarin']
	
	def test_store_single_response(self):
		"""测试单个答案会被妥善地存储"""
		self.my_survey.store_response(self.responses[0])
		self.assertIn(self.responses[0], self.my_survey.responses)
	
	def test_store_three_responses(self):
		"""测试三个答案会被妥善地存储"""
		for response in self.responses:
			self.my_survey.store_response(response)
		for response in self.responses:
			self.assertIn(response, self.my_survey.responses)

if __name__ == '__main__':
unittest.main()

方法setUp()做了两件事情:创建一个调查对象,以及创建一个答案列表。存储这两样东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用。这让两个测试方法都更简单,因为它们都不用创建调查对象和答案了。方法test_store_single_response()核实self.responses中的第一个答案self.responses[0]被妥善地存储,而方法test_store_three_response()核实self.responses中的全部三个答案都被妥善地存储。

再次运行test_survey.py时,这两个测试也都通过了。如果要扩展AnonymousSurvey,使其允许每位用户输入多个答案,这些测试将很有用。修改代码以接受多个答案后,可运行这些测试,确认存储单个答案或一系列答案的行为未受影响。

测试自己编写的类时,方法setUp()让测试方法编写起来更容易:可在setUp()方法中创建一系列实例并设置其属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。

注意 运行测试用例时,每完成一个单元测试,Python都打印一个字符:测试通过时打印一个句点,测试引发错误时打印一个E,而测试导致断言失败时则打印一个F。这就是你运行测试用例时,在输出的第一行中看到的句点和字符数量各不相同的原
因。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值