文件与异常
大多数真正有用的程序都会接触文件系统:读取配置、写入结果、加载数据。当出现问题时,Python 会抛出异常(exception),这是表示发生意外情况的信号。本章涵盖两方面内容:数据如何在文件中读取和写入,以及如何编写优雅处理错误而不是崩溃的代码。
打开文件
open() 打开一个文件并返回一个可供读取或写入的对象。你需要告诉它路径以及你要做什么(读、写或追加)。使用完毕后一定要关闭文件;with 语句会自动完成这一步。
f = open("data.txt", "r") # "r" = 读取
content = f.read()
f.close()"r" 是模式:
| 模式 | 含义 |
|---|---|
"r" | 读取。文件必须存在。默认模式。 |
"w" | 写入。创建或覆盖文件。 |
"a" | 追加。在末尾添加,不清除已有内容。 |
"x" | 创建。如果文件已存在则失败。 |
"r+" | 读写。 |
"b" | 二进制。可加到任何模式上:"rb"、"wb"。 |
完成后务必调用 .close()。忘记关闭会让文件保持锁定状态,并可能导致数据损坏。处理这件事最可靠的方法就是 with 语句。
with 语句
with open(...) 会为你管理文件,在缩进块结束时自动关闭它,即使发生错误也是如此。请始终使用 with open(...) 而不是手动 open()/close()。它更安全,也是标准做法。
with open("data.txt", "r") as f:
content = f.read()
# 这里 f 已被关闭,保证关闭with 做了什么?
with 是 Python 的上下文管理器语法。它为你调用设置和清理代码;在这种情况下,即可靠地打开和关闭文件。你不需要了解它的内部实现,只要配合 open() 使用即可。
读取文件
读取的三种方法。.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)是读取大文件最高效、最惯用的方式。
写入文件
"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 = ["Line one", "Line two", "Line three"]
with open("output.txt", "w") as f:
f.write("\n".join(lines) + "\n")异常
当 Python 遇到无法处理的问题时,它会抛出异常:一个描述出错原因及位置的错误。如果不处理它,程序会崩溃并打印追溯(traceback)。下表展示了你最常遇到的异常。
你会遇到的常见异常:
| 异常 | 何时发生 |
|---|---|
FileNotFoundError | open() 找不到文件 |
ValueError | 函数得到了正确类型但内容错误的值,例如 int("abc") |
TypeError | 完全错误的类型,例如 "hello" + 5 |
KeyError | 字典键不存在 |
IndexError | 列表索引越界 |
ZeroDivisionError | 除以零 |
AttributeError | 对象没有该属性或方法 |
try / except
把可能失败的代码包在 try 块中。如果发生异常,匹配的 except 块会处理它,而不是导致崩溃。捕获异常要具体明确:用裸 except: 捕获一切会掩盖真正的 bug。
try:
value = int("abc")
except ValueError:
print("That's not a valid number")捕获异常时要具体。用裸 except: 捕获所有异常会掩盖 bug:
# 不好,会捕获包括程序员错误在内的所有异常
try:
result = do_something()
except:
pass
# 好,只捕获你预期且确实能处理的异常
try:
result = do_something()
except FileNotFoundError:
print("File not found")捕获多个异常
你可以在独立的 except 块中处理不同类型的错误,或者使用元组在一个块中同时捕获多个类型。as e 部分让你能访问错误消息。
try:
data = int(user_input)
result = 100 / data
except ValueError:
print("Not a number")
except ZeroDivisionError:
print("Can't divide by zero")或用元组一次捕获多个:
except (ValueError, ZeroDivisionError) as e:
print(f"Input error: {e}")as e 把异常对象绑定到一个名字上,以便检查消息。
else 和 finally
else 仅在没有发生异常时运行。finally 总是运行,无论是否发生异常。finally 适用于无论如何都必须执行的清理工作。
try:
with open("data.txt") as f:
content = f.read()
except FileNotFoundError:
print("File not found, using defaults")
content = ""
else:
print("File loaded successfully")
finally:
print("Done attempting to load file") # 总是运行finally 即使在你已经为文件使用 with 时,也最适合用于清理(关闭连接、释放锁)。
raise
你可以用 raise 自己抛出异常。这就是你如何让函数明确地向调用者发出问题信号,而不是悄悄返回错误值。
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b这让你的函数对它期望什么变得明确,并向调用者清晰地发出问题信号。
自定义异常类
对于较大的程序,你可以通过继承 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"Cannot withdraw {amount}, balance is {self.balance}"
)
self.balance -= amounttry:
account.withdraw(1000)
except InsufficientFundsError as e:
print(f"Transaction declined: {e}")JSON
JSON 是几乎所有东西都使用的格式:API、配置文件、数据导出。Python 的 json 模块直接处理它。json.load() 从文件中读取 JSON 并转为 Python 字典或列表。json.dump() 将 Python 字典或列表作为 JSON 写入文件。
从文件读取 JSON:
import json
with open("config.json", "r") as f:
config = json.load(f) # 将 JSON 解析为 Python 字典/列表
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 或 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,在下次运行时读回,如果还没有存档文件就退回到默认值:
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("Game saved.")
def load_game() -> dict:
try:
with open(SAVE_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
print("No save file found, starting fresh.")
return {"name": "Player", "score": 0, "level": 1}
state = load_game()
state["score"] += 50
save_game(state)
