文件与异常
大多数真正干活的程序都会接触文件系统:读取配置、写入结果、加载数据。而当出问题时,Python 会抛出异常(exception),也就是发生了意外情况的信号。这一章会讲两件事:如何把数据读进文件、写出文件,以及如何写出能优雅处理错误而不是直接崩溃的代码。
打开文件
open() 会打开一个文件并返回一个对象,你可以用它来读取或写入内容。你需要告诉它文件路径,以及你想对文件做什么(读取、写入还是追加)。用完文件一定要关闭;with 语句会自动帮你完成这件事。
f = open("data.txt", "r") # "r" = 读取
content = f.read()
f.close()这里的 "r" 就是模式(mode):
| 模式 | 含义 |
|---|---|
"r" | 读取。文件必须存在。默认模式。 |
"w" | 写入。创建文件或覆盖已有文件。 |
"a" | 追加。在末尾添加内容,不会清空原内容。 |
"x" | 创建。如果文件已存在则失败。 |
"r+" | 读写。 |
"b" | 二进制。可加到任意模式后面:"rb"、"wb"。 |
用完文件一定要调用 .close()。忘记调用会让文件一直处于打开状态,还可能丢失那些原本正等待写入的数据。更可靠的做法是使用 with 语句。
open(path, mode) 会给你一个文件对象,模式则说明了你想做什么:"r" 读取,"w" 写入(会把原有内容清空),"a" 在末尾追加。我曾经想用 "a",结果不小心用了 "w",一个文件就这么没了,所以打字之前先在脑子里想清楚要用哪个模式。 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("Hello, world\n")
with open("output.txt", "a") as f:
f.write("Another line\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 遇到自己无法处理的问题时,会抛出一个异常(exception):一个描述出了什么错、错在哪里的错误对象。如果你不处理它,程序就会崩溃并打印出一段 traceback。下表列出了你会遇到的最常见异常。
你会遇到的常见异常:
| 异常 | 发生场景 |
|---|---|
FileNotFoundError | open() 找不到文件 |
ValueError | 函数拿到了类型正确但内容不对的值,例如 int("abc") |
TypeError | 类型完全不对,例如 "hello" + 5 |
KeyError | 字典中不存在这个键 |
IndexError | 列表索引超出范围 |
ZeroDivisionError | 除以零 |
AttributeError | 对象没有这个属性或方法 |
FileNotFoundError、ValueError、KeyError、TypeError。如果不处理它们,程序会停下来并打印一段 traceback,看起来挺吓人,但它其实是一张地图,直接指向出错的地方。 try / except
把可能失败的代码放进 try 代码块。如果发生异常,匹配的 except 代码块会处理它,而不是让程序崩溃。要具体指明你捕获的是哪种异常:用裸的 except: 捕获所有异常会掩盖真正的 bug。
try:
value = int("abc")
except ValueError:
print("这不是一个有效的数字")要具体指明你捕获的是哪种异常。用裸的 except: 捕获所有异常会掩盖 bug:
# 不好,会捕获一切,包括编程失误
try:
result = do_something()
except:
pass
# 好,只捕获你预期到、也真正能处理的错误
try:
result = do_something()
except FileNotFoundError:
print("找不到文件")try 代码块,匹配的 except 会运行起来,而不是让程序崩溃。指明你预期的异常,比如 ValueError,而不是用裸的 except:,后者会捕获一切,把真正的 bug 都藏起来。捕获你真正能处理的,让其余的暴露出来。 捕获多种异常
你可以用不同的 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("文件加载尝试结束") # 总会运行即便你已经在用 with 处理文件,finally 在清理工作上(关闭连接、释放锁)依然最为有用。
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 的字典或列表。json.dump() 则会把字典或列表以 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 |
|---|---|
对象 {} | dict |
数组 [] | list |
字符串 "" | str |
| 数字 | int 或 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 的字典和列表,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)
