文件和异常

大多数做实际工作的程序都会接触文件系统:读取配置、写入结果、加载数据。当出现问题时,Python 会抛出异常,这是一个信号,表示发生了意外情况。本章介绍两者:如何将数据读入和写出文件,以及如何编写能够优雅地处理错误而不是直接崩溃的代码。
打开文件
open() 打开一个文件并返回一个对象,你可以从中读取或写入。你告诉它路径和你想对文件做什么(读、写或追加)。完成后总要关闭文件;with 语句会自动做这件事。
f = open("data.txt", "r") # "r" = 读取
content = f.read()
f.close()"r" 是模式:
| 模式 | 含义 |
|---|---|
"r" | 读取。文件必须存在。默认模式。 |
"w" | 写入。创建或覆盖文件。 |
"a" | 追加。不删除内容地添加到末尾。 |
"x" | 创建。如果文件已存在则失败。 |
"r+" | 读取和写入。 |
"b" | 二进制。添加到任何模式:"rb"、"wb"。 |
完成后总要调用 .close()。忘记它会让文件保持打开状态,可能会丢失还在等待写入的数据。处理这个问题的可靠方法是 with 语句。
open(path, mode) 给你一个文件对象,模式说明你想做什么:"r" 读取,"w" 写入(会清除之前的内容),"a" 追加到末尾。我曾经因为误用 "w" 而丢失过一个文件,我本来想用 "a",所以在输入模式前先想象一下。 with 语句
with open(...) 为你管理文件,当缩进块结束时自动关闭它,即使发生了错误。总是使用 with open(...) 而不是手动 open()/close()。 这样更安全,也是标准做法。
with open("data.txt", "r") as f:
content = f.read()
# f 在这里被关闭,保证with 是 Python 的上下文管理器语法:它为你运行设置和清理代码,这里打开文件并可靠地关闭它。你不需要知道它内部如何工作就能把它用于 open()。
with open(...) as f 打开文件,让你在缩进块内使用 f,然后在块结束时为你关闭它,即使出了问题。每次都改用它而不是自己调用 .close(),这是一件少了一件要忘掉的事。 读取文件
三种读取方法。.read() 将整个文件作为一个字符串加载。.readline() 读取一行。直接迭代文件对象逐行读取,这是处理大文件最有效的方法,因为它不会一次性将所有内容加载到内存中。
with open("data.txt", "r") as f:
content = f.read() # 整个文件作为一个字符串
with open("data.txt", "r") as f:
first_line = f.readline() # 一次一行
with open("data.txt", "r") as f:
lines = f.readlines() # 行的列表,每行以 "\n" 结尾对于大文件,逐行读取比一次性加载所有内容更高效:
with open("big_file.txt", "r") as f:
for line in f: # 直接迭代文件,内存高效
print(line.strip()) # strip() 移除尾部换行符直接迭代文件对象(for line in f)是读取大文件最高效的方式。
.read() 给你整个文件作为一个字符串,.readline() 给你单一行。当文件可能很大时,用 for line in f 循环,它一次读一行,这样你从不把全部加载到内存。每行的 .strip() 清除尾部换行符。 写入文件
"w" 模式如果文件存在则完全覆盖它。"a" 模式添加到末尾。.write() 不会自动添加换行符;在每行末尾显式包含 "\n"。要一次写入多行,用 "\n".join() 连接它们。
with open("output.txt", "w") as f:
f.write("你好,世界\n")
with open("output.txt", "a") as f:
f.write("另一行\n")"w" 覆盖整个文件如果它存在。"a" 添加到末尾。
f.write() 不会自动添加换行符,所以显式包含 "\n"。要一次写入多行:
lines = ["第一行", "第二行", "第三行"]
with open("output.txt", "w") as f:
f.write("\n".join(lines) + "\n")"w" 创建或覆盖文件,"a" 添加到末尾而不清除它。.write() 不为你添加换行符,所以自己在每行末尾加上 "\n"。对于一批行,"\n".join(lines) 先把它们缝在一起。 异常
当 Python 遇到它无法处理的问题时,它会抛出一个异常:一个错误,描述什么出了问题以及在哪里。如果你不处理它,你的程序会崩溃并打印一个追踪。下表显示了你将遇到的最常见的异常。
你会遇到的常见异常:
| 异常 | 何时发生 |
|---|---|
FileNotFoundError | open() 找不到文件 |
ValueError | 函数得到类型正确但内容错误的值,例如 int("abc") |
TypeError | 完全错误的类型,例如 "hello" + 5 |
KeyError | 字典键不存在 |
IndexError | 列表索引超出范围 |
ZeroDivisionError | 除以零 |
AttributeError | 对象没有那个属性或方法 |
try / except
在可能失败的代码周围包裹 try 块。如果发生异常,匹配的 except 块处理它而不是崩溃。对你捕获的异常类型要具体:用裸 except: 捕获所有东西会隐藏真实的错误。
try:
value = int("abc")
except ValueError:
print("那不是一个有效的数字")对你捕获的异常类型要具体。用裸 except: 捕获所有异常会隐藏错误:
# 不好,捕获包括程序员错误的所有东西
try:
result = do_something()
except:
pass
# 好,只捕获你预期的和实际能处理的
try:
result = do_something()
except FileNotFoundError:
print("文件未找到")try 块中,匹配的 except 会运行而不是程序崩溃。命名异常你期望的,如 ValueError,而不是裸 except:,它捕获所有东西并从你那里隐藏真实的错误。捕获你实际能处理的,让其他的显示。 捕获多个异常
你可以在分开的 except 块中处理不同的错误类型,或用一个元组在一个块中捕获几个类型。as e 部分给你访问错误消息的权限。
try:
data = int(user_input)
result = 100 / data
except ValueError:
print("不是一个数字")
except ZeroDivisionError:
print("不能除以零")或在一个元组中捕获多个:
except (ValueError, ZeroDivisionError) as e:
print(f"输入错误:{e}")as e 绑定异常对象到一个名字所以你能检查消息。
except 块,Python 运行第一个匹配的。要以相同的方式处理几个,在元组中列出它们:except (ValueError, ZeroDivisionError)。`as e` 部分给你错误对象所以你能打印它的消息。 else 和 finally
else 仅在没有异常发生时运行。finally 总是运行,不管是否有异常。finally 对于必须无论如何都发生的清理很有用。
try:
with open("data.txt") as f:
content = f.read()
except FileNotFoundError:
print("文件未找到,使用默认值")
content = ""
else:
print("文件成功加载")
finally:
print("完成尝试加载文件") # 总是运行finally 最有用于清理(关闭连接,释放锁)即使你已经在为文件使用 with。
else 仅在 try 没有抛出时运行,成功路径其余部分的整洁地方。finally 每次都运行,异常或否,这使它成为必须无论如何都发生的清理的地方。对于文件 with 已经覆盖清理,所以你会比预期更少地使用 finally。 raise
你可以用 raise 自己抛出异常。这就是你如何让你的函数清楚地向调用者报告问题而不是静默地返回错误值。
def divide(a, b):
if b == 0:
raise ValueError("不能除以零")
return a / b这使你的函数显式它们期望什么并清楚地向调用者报告问题。
raise ExceptionType("message") 让你自己的代码大声标记一个问题而不是继续用一个坏值。这就是一个函数说"我不能用这个工作"所以调用者处理它。清楚和早期打败一个坏答案在稍后显示。 自定义异常类
对于更大的程序,你能通过继承 Exception 定义你自己的异常类型。这让调用者分开地捕获你的特定错误从其他类型的错误。
class InsufficientFundsError(Exception):
pass
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(
f"无法取款 {amount},余额是 {self.balance}"
)
self.balance -= amounttry:
account.withdraw(1000)
except InsufficientFundsError as e:
print(f"交易被拒绝:{e}")Exception 做你自己的错误类型,一个空类体是足够开始的。它给调用者一个名字来捕获那是你的,分开于内置错误,所以你程序的问题独立站立。 JSON
JSON 是每个人都说的格式:API、配置文件、数据导出。Python 的 json 模块直接处理它。json.load() 从文件读 JSON 到一个 Python dictionary 或 list。json.dump() 把一个 dictionary 或 list 写回到一个文件作为 JSON。
从文件读 JSON:
import json
with open("config.json", "r") as f:
config = json.load(f) # 解析 JSON 到一个 Python dict/list
print(config["setting"])写 JSON 到文件:
import json
data = {"name": "王五", "score": 87, "active": True}
with open("output.json", "w") as f:
json.dump(data, f, indent=2) # indent= 使它人类可读JSON 到 Python 类型映射:
| JSON | Python |
|---|---|
object {} | dict |
array [] | list |
string "" | str |
| number | int or float |
true / false | True / False |
null | None |
在不接触文件的情况下在 JSON 字符串和 Python 对象之间转换:
import json
# 字符串到 Python
data = json.loads('{"name": "王五", "score": 87}')
# Python 到字符串
text = json.dumps({"name": "王五", "score": 87}, indent=2)json.load() 从一个文件对象读。json.loads()(带"s")从一个字符串读。
json.load(f) 把文件中的 JSON 变成 Python dicts 和 lists,json.dump(data, f) 写它们回出来。`s` 版本,json.loads 和 json.dumps,对一个字符串做相同的而不是一个文件。当你想要输出可读时添加 indent=2。 实际中
一个小游戏的保存/加载模式:写状态到 JSON,在下一次运行时加载它,如果还没有保存文件存在则回退到默认值:
import json
SAVE_FILE = "save_game.json"
def save_game(player_data: dict) -> None:
with open(SAVE_FILE, "w") as f:
json.dump(player_data, f, indent=2)
print("游戏已保存。")
def load_game() -> dict:
try:
with open(SAVE_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
print("未找到保存文件,开始新的。")
return {"name": "玩家", "score": 0, "level": 1}
state = load_game()
state["score"] += 50
save_game(state)
