《python编程从入门到实践》10-11章 文件和异常、测试代码
10 文件和异常

10.1. 读取文件
文本文件可存储大量数据,学习读取文件是数据分析的基础。本章推荐使用 pathlib 模块,这是一种跨平台且更简单的文件处理方法。
读取文件的全部内容
使用 Path 对象的 read_text() 方法,可将文件的全部内容作为一个字符串读取到内存中:
from pathlib import Path
path = Path('pi_digits.txt')
contents = path.read_text()
print(contents)
访问文件中的各行
在分析数据时,通常需要逐行检查文件内容。可以先读取全部内容,再使用 splitlines() 方法将长字符串拆分为行列表:
from pathlib import Path
path = Path('pi_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
for line in lines:
print(line)
使用文件的内容
读取到内存后,就可自由处理数据。例如,将圆周率文件中的各行拼接成一个无空格的连续字符串:
pi_string = ''
for line in lines:
pi_string += line.lstrip() # lstrip() 删除每行左端的空白
print(pi_string)
print(len(pi_string))
⚠️ 注意:Python 读取文本文件时,所有内容都被视为字符串。如果需要将其作为数值使用,必须使用 int() 或 float() 进行转换。
10.2. 写入文件
将数据写入文件,可以在程序结束后依然保留输出结果。
写入一行
使用 write_text() 方法,将单个字符串写入文件。如果文件不存在,会自动创建;如果文件已存在,将覆盖原有内容:
from pathlib import Path
path = Path('programming.txt')
path.write_text("I love programming.")
写入多行
write_text() 只接受单个字符串参数。要写入多行,可以先用换行符 \n 拼接好完整内容,再一次性写入:
from pathlib import Path
contents = "I love programming.\n"
contents += "I love creating new games.\n"
contents += "I also love working with data.\n"
path = Path('programming.txt')
path.write_text(contents)
结果:
I love programming.
I love creating new games.
I also love working with data.
⚠️ 注意:Python 只能将字符串写入文本文件。如果要存储数值,必须先用 str() 转换。
10.3.__file__ 是 Python 的一个特殊变量,表示当前脚本文件的路径。
基本用法
from pathlib import Path
# 获取当前脚本的完整路径
print(__file__) # 输出: d:\开发工具\python编程从入门到实践\10章\10-练习.py
# 获取脚本所在目录
script_dir = Path(__file__).parent
print(script_dir) # 输出: d:\开发工具\python编程从入门到实践\10章
# 拼接文件路径
path = Path(__file__).parent / 'favorite_number.json'
# 结果: d:\开发工具\python编程从入门到实践\10章\favorite_number.json
常用操作
| 代码 | 结果 | 说明 |
|---|---|---|
Path(__file__) |
脚本完整路径 | 包含文件名 |
Path(__file__).parent |
脚本所在目录 | 不包含文件名 |
Path(__file__).name |
脚本文件名 | 如 10-练习.py |
Path(__file__).stem |
文件名(无扩展名) | 如 10-练习 |
Path(__file__).suffix |
扩展名 | 如 .py |
Path(__file__).resolve() |
绝对路径(规范化) | 去除相对符号 |
实际应用
在你的代码中:
from pathlib import Path
import json
# 文件会生成在脚本所在的 10章 目录下
path = Path(__file__).parent / 'favorite_number.json'
def get_stored_favorite_number(number):
numbers = json.dumps(number)
path.write_text(numbers, encoding='utf-8')
print('i had store the number.')
注意事项
⚠️ __file__ 只在脚本运行时有效:
# 在交互式环境(如 Python 控制台)中运行会报错
>>> print(__file__)
NameError: name '__file__' is not defined
总结:__file__ 让文件操作始终以脚本位置为基准,不受运行目录影响。
10.4. 异常
异常是Python在程序执行出错时创建的特殊对象。如果不对异常进行处理,程序将崩溃并显示一段 traceback(回溯)。使用 try-except 代码块可以优雅地处理错误。
处理 ZeroDivisionError
不能将数字除以0,这会引发 ZeroDivisionError:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
这样程序不会崩溃,而是友好地提示错误。
else 代码块
有时候,有些代码只在 try 代码块成功执行后才需要运行,这时应把它们放在 else 代码块中:
try:
answer = 5 / 0
except ZeroDivisionError:
print("You can't divide by zero!")
else:
print(answer) # 只有不出错时才会执行
处理 FileNotFoundError
找不到文件是常见的错误。使用 try-except 可以在文件缺失时给出提示,而不是直接崩溃:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text()
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
静默失败
有时候,你希望在发生异常时什么都不做,程序静静地继续运行。可以在 except 代码块中使用 pass 语句:
try:
contents = path.read_text()
except FileNotFoundError:
pass # 什么都不做,也不报错
else:
# 正常处理文件内容的代码
10.5. 存储数据
很多程序需要保存用户提供的数据(如设置、得分等)。json 模块可以将简单的 Python 数据结构(如列表、字典)转存到文件中,并在下次运行时还原。JSON 是一种通用的数据格式,便于不同编程语言间共享数据。
使用 json.dumps() 和 json.loads()
json.dumps():将 Python 数据结构转换为 JSON 格式的字符串。json.loads():将 JSON 格式的字符串转换回 Python 数据结构。
保存数据(写入)
from pathlib import Path
import json
numbers = [2, 3, 5, 7, 11, 13]
path = Path('numbers.json')
contents = json.dumps(numbers) # 转换为 JSON 字符串
path.write_text(contents) # 写入文件
如果不加 ensure_ascii=False,默认会将非 ASCII 字符(如中文)转义为 Unicode 编码。
当前文件utf-8,如果不加 encoding='utf-8',
numbers = {
"name": "小明",
"age": 20,
"skills": ["Python", "VSCode"]
}
path = Path("numbers.json")
contents = json.dumps(numbers, ensure_ascii=False) # 转换为 JSON 字符串
path.write_text(contents, encoding='utf-8') # 写入文件
读取数据(加载)
from pathlib import Path
import json
path = Path('numbers.json')
contents = path.read_text() # 这里可以传入 encoding='utf-8'
numbers = json.loads(contents) # 还原为 Python 列表
print(numbers)
实战:记住用户名
结合 try-except 和 json,可以写出一个智能的问候程序:如果有存储的名字就欢迎回来,没有就提示输入并保存。
from pathlib import Path
import json
path = Path('username.json')
def get_stored_username():
"""如果存储了用户名,就获取它"""
if path.exists():
contents = path.read_text()
username = json.loads(contents)
return username
else:
return None
def greet_user():
"""问候用户,并指出其名字"""
username = get_stored_username()
if username:
print(f"Welcome back, {username}!")
else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
greet_user()
11.测试代码

11.1 使用 pip 安装 pytest
Python默认不包含pytest,因此需要使用 pip 工具来安装它。pip 让你能够安装和使用其他程序员编写的外部库。
11.1.1 更新 pip
在安装新包之前,最好先更新 pip。在终端中运行以下命令:
python -m pip install --upgrade pip
(注意:如果你的系统使用 python3 命令,请将 python 替换为 python3)
11.1.2 安装 pytest
更新完成后,即可安装 pytest:
python -m pip install --user pytest
11.2 测试函数
要为函数编写测试,需要先编写测试函数,并使用 pytest 提供的断言来核实函数的输出是否符合预期。
11.2.1 单元测试和测试用例
- 单元测试:用于核实函数的某个方面没有问题。
- 测试用例:一组单元测试,一起核实函数在各种情形下的行为都符合要求。
11.2.2 可通过的测试
测试文件的命名必须以 test_ 开头。在测试函数中,使用 assert 语句来断言函数的返回值是否等于预期值。
假设我们有一个格式化姓名的函数 get_formatted_name():
# name_function.py
def get_formatted_name(first, last):
"""生成整洁的姓名"""
full_name = f"{first} {last}"
return full_name.title()
为它编写测试文件:
# test_name_function.py
from name_function import get_formatted_name
def test_first_last_name():
"""能够正确地处理像 Janis Joplin 这样的姓名吗?"""
formatted_name = get_formatted_name('janis', 'joplin')
assert formatted_name == 'Janis Joplin'
11.2.3 运行测试
在终端中,切换到测试文件所在的目录,只需输入 pytest 并回车即可:
pytest
如果测试通过,输出将显示一个点号 . 以及 1 passed。
11.2.4 未通过的测试
如果修改了 get_formatted_name() 使其需要中间名,但没有让中间名变为可选,原来的测试将会失败。此时 pytest 会详细显示断言失败的行、预期值和实际返回值。
11.2.5 在测试未通过时怎么办
测试未通过时不要惊慌,这是正常的。检查修改后的代码,确保新功能正常工作且没有破坏旧功能,调整代码直到测试重新通过。
11.2.6 添加新测试
针对函数的新行为,应在测试文件中添加新的测试函数。例如,为包含中间名的姓名添加一个测试:
def test_first_last_middle_name():
"""能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名吗?"""
formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
assert formatted_name == 'Wolfgang Amadeus Mozart'
再次运行 pytest,输出将显示 2 passed。
11.3 测试类
前半部分测试的是单个函数,下面来编写针对类的测试。如果针对类的测试通过了,你就能确信对类所做的改进没有意外地破坏其原有的行为。
11.3.1 各种断言
在测试中,你可以做出任何可用条件语句表示的断言。常用的断言如下表所示:
| 断言 | 用途 |
|---|---|
assert a == b |
断言两个值相等 |
assert a != b |
断言两个值不等 |
assert a |
断言 a 的布尔求值为 True |
assert not a |
断言 a 的布尔求值为 False |
assert element in list |
断言元素在列表中 |
assert element not in list |
断言元素不在列表中 |
11.3.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}")
11.3.3 测试 AnonymousSurvey 类
测试类的方法与测试函数类似,也是编写测试函数并进行断言。我们可以测试单个答案是否能被正确存储,以及三个答案是否能被正确存储:
# test_survey.py
from survey import AnonymousSurvey
def test_store_single_response():
"""测试单个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
language_survey.store_response('English')
assert 'English' in language_survey.responses
def test_store_three_responses():
"""测试三个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
language_survey.store_response(response)
for response in responses:
assert response in language_survey.responses
11.3.4 使用夹具
上述测试中,每个测试函数都创建了 AnonymousSurvey 的实例。在大型项目中,这种重复代码是个问题。夹具(fixture) 可帮助我们搭建测试环境,创建供多个测试使用的资源。
在 pytest 中,使用 @pytest.fixture 装饰器来创建夹具:
# test_survey.py
import pytest
from survey import AnonymousSurvey
@pytest.fixture
def language_survey():
"""一个可供所有测试函数使用的 AnonymousSurvey 实例"""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
return language_survey
def test_store_single_response(language_survey):
"""测试单个答案会被妥善地存储"""
language_survey.store_response('English')
assert 'English' in language_survey.responses
def test_store_three_responses(language_survey):
"""测试三个答案会被妥善地存储"""
responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
language_survey.store_response(response)
for response in responses:
assert response in language_survey.responses
测试函数接收的参数名与夹具函数名相同时,pytest 就会自动运行夹具,并将夹具返回的资源传递给测试函数。

浙公网安备 33010602011771号