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

10 文件和异常

image

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-exceptjson,可以写出一个智能的问候程序:如果有存储的名字就欢迎回来,没有就提示输入并保存。

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.测试代码

image

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 就会自动运行夹具,并将夹具返回的资源传递给测试函数。


posted @ 2026-04-26 17:31  【唐】三三  阅读(56)  评论(0)    收藏  举报