Skip to content
This page has been auto-translated and may contain errors.View in English

文件与异常

大多数真正有用的程序都会接触文件系统:读取配置、写入结果、加载数据。当出现问题时,Python 会抛出异常(exception),这是表示发生意外情况的信号。本章涵盖两方面内容:数据如何在文件中读取和写入,以及如何编写优雅处理错误而不是崩溃的代码。

文件 I/O 和异常处理是让程序变得健壮的两大机制。open() 提供访问文件系统的能力;with 确保即使发生错误也会进行清理。try/except 捕获特定的异常类型,让你能够优雅地恢复而不是崩溃。它们共同构成了任何无人值守运行脚本的基础。

Python 中的文件操作通过操作系统的文件描述符抽象进行。open() 返回一个文件对象,其类型取决于模式和缓冲设置。上下文管理器通过 __enter____exit__ 保证资源清理。异常处理使用结构化展开:异常传播时,Python 沿调用栈寻找匹配的 except 子句,并在退出时无条件执行 finally 块。

打开文件

open() 打开一个文件并返回一个可供读取或写入的对象。你需要告诉它路径以及你要做什么(读、写或追加)。使用完毕后一定要关闭文件;with 语句会自动完成这一步。

open(path, mode) 返回一个文件对象。模式字符串控制访问方式:"r" 用于读取,"w" 用于写入(会先截断),"a" 用于追加。加上 "b" 即为二进制模式。文本模式的默认编码是系统区域设置;为了可移植性,请显式指定 encoding="utf-8"

open() 调用操作系统分配文件描述符,并返回一个带缓冲的 I/O 包装器。文本模式会用 TextIOWrapper 包装,处理编码和通用换行符。二进制模式返回 BufferedReaderBufferedWriterencoding 参数对可移植性至关重要:默认值(locale.getpreferredencoding())因平台而异。对于文本文件,除非有特定理由,否则始终指定 encoding="utf-8"

python
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 是 Python 的上下文管理器语法。它在开始时调用 __enter__,在结束时调用 __exit__,即使抛出异常也是如此。对于文件,__exit__ 会关闭文件描述符。这样无需为每次文件访问都包一层 try/finally,就能保证清理工作完成。

with expr as name 调用 expr.__enter__() 并把结果绑定到 name。退出时(正常或异常),调用 expr.__exit__(exc_type, exc_val, tb)。如果 __exit__ 返回真值,异常会被抑制。文件对象实现了这一协议:__exit__ 调用 self.close()。上下文管理器可以叠加:with open(a) as f, open(b) as g: 是处理多个文件的惯用写法。

python
with open("data.txt", "r") as f:
    content = f.read()

# 这里 f 已被关闭,保证关闭

with 做了什么?

with 是 Python 的上下文管理器语法。它为你调用设置和清理代码;在这种情况下,即可靠地打开和关闭文件。你不需要了解它的内部实现,只要配合 open() 使用即可。

读取文件

读取的三种方法。.read() 把整个文件作为一个字符串加载。.readline() 读取一行。直接在文件对象上迭代会逐行读取,这是处理大文件最高效的方式,因为它不会一次性把所有内容加载到内存中。

.read() 把整个文件加载到内存。.readline() 读取一行,包括换行符。.readlines() 返回所有行的列表。直接迭代文件对象是处理大文件最节省内存的方式:它每次从缓冲区读取一行,而不持有完整内容。

.read() 读到 EOF,以字符串形式返回内容(在二进制模式下则为字节)。.readline() 读到 \n 或 EOF。迭代文件对象会调用 __iter__,后者反复调用 readline()😮(1) 内存,逐行处理。.readlines() 等价于 list(file),但会预先生成所有行。对于非常大的文件,迭代器模式优于 .read()

python
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" 结尾

对于大文件,逐行读取比一次加载所有内容更高效:

python
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() 拼接。

.write(s) 写入一个字符串并返回写入的字符数。它不会添加换行符。.writelines(iterable) 写入可迭代对象中的每个字符串,不添加分隔符。"w" 在打开时会截断;"a" 会定位到 EOF。为了可移植性,使用 "\n"os.linesep,而不是硬编码平台特定的行尾。

.write() 写入缓冲区;数据可能要等到调用 .flush().close() 时才会到达磁盘。"w" 模式在打开时调用 truncate(0),销毁之前的所有内容。"a" 模式在每次写入前定位到 EOF,这在大多数操作系统上对并发追加是安全的。f.writelines() 对每项调用 f.write():不会有额外的内存分配,但也不会添加分隔符。

python
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"。要一次写入多行:

python
lines = ["Line one", "Line two", "Line three"]

with open("output.txt", "w") as f:
    f.write("\n".join(lines) + "\n")

异常

当 Python 遇到无法处理的问题时,它会抛出异常:一个描述出错原因及位置的错误。如果不处理它,程序会崩溃并打印追溯(traceback)。下表展示了你最常遇到的异常。

异常是继承自 BaseException 的对象。所有面向用户的异常都继承自 Exception。当被抛出时,Python 沿调用栈展开,寻找匹配的 except 子句。追溯展示了从异常发生点到入口点的完整调用链。

异常对象携带类型、消息以及一个 __traceback__ 属性指向追溯对象。raise 创建一个新异常;不带参数的 raise 会重新抛出当前异常并保留原始追溯。异常链(raise B from A)关联两个异常,并在追溯中显示。BaseException 是根类;KeyboardInterruptSystemExit 直接继承自它,而不是 Exception,这就是为什么裸 except Exception 不会捕获它们。

你会遇到的常见异常:

异常何时发生
FileNotFoundErroropen() 找不到文件
ValueError函数得到了正确类型但内容错误的值,例如 int("abc")
TypeError完全错误的类型,例如 "hello" + 5
KeyError字典键不存在
IndexError列表索引越界
ZeroDivisionError除以零
AttributeError对象没有该属性或方法

try / except

把可能失败的代码包在 try 块中。如果发生异常,匹配的 except 块会处理它,而不是导致崩溃。捕获异常要具体明确:用裸 except: 捕获一切会掩盖真正的 bug。

try/except 拦截特定的异常类型。指定类型可以避免悄悄吞掉无关的错误。可以用 as e 把异常绑定到一个名字,以便检查消息。多个 except 子句处理不同的异常类型;Python 匹配第一个兼容的子句。

except ExceptionType as eisinstance(raised_exception, ExceptionType) 为真时匹配。这意味着捕获 Exception 也会捕获所有子类。Python 匹配第一个类型匹配的 except 子句;之后的子句会被跳过。except (A, B) as e 同时捕获两种类型。裸 except: 会捕获一切,包括 KeyboardInterruptSystemExit,这几乎从来不合理。

python
try:
    value = int("abc")
except ValueError:
    print("That's not a valid number")

捕获异常时要具体。用裸 except: 捕获所有异常会掩盖 bug:

python
# 不好,会捕获包括程序员错误在内的所有异常
try:
    result = do_something()
except:
    pass

# 好,只捕获你预期且确实能处理的异常
try:
    result = do_something()
except FileNotFoundError:
    print("File not found")

捕获多个异常

你可以在独立的 except 块中处理不同类型的错误,或者使用元组在一个块中同时捕获多个类型。as e 部分让你能访问错误消息。

多个 except 子句从上到下求值;第一个匹配的获胜。用元组捕获多个类型可以捕获其中任意一个。使用 as e 绑定异常实例,可以访问消息、类型和追溯。先捕获最具体的类型,再捕获更通用的类型,可以避免遮蔽。

except (A, B) as e 是同时捕获两种类型的语法糖。as e 绑定会在 except 块退出后被清除(以防止与追溯产生引用循环)。要带额外上下文重新抛出异常,使用 raise ValueError("context") from e,它会设置 __cause__ 并在追溯中显示两个异常。

python
try:
    data = int(user_input)
    result = 100 / data
except ValueError:
    print("Not a number")
except ZeroDivisionError:
    print("Can't divide by zero")

或用元组一次捕获多个:

python
except (ValueError, ZeroDivisionError) as e:
    print(f"Input error: {e}")

as e 把异常对象绑定到一个名字上,以便检查消息。

else 和 finally

else 仅在没有发生异常时运行。finally 总是运行,无论是否发生异常。finally 适用于无论如何都必须执行的清理工作。

else 把"正常路径"代码与 try 主体分开,清楚地表明 else 中的代码仅在未抛出异常时运行。finally 是清理保证:即使抛出、捕获或重新抛出了异常,它也会运行。即使遇到 returnbreak,它仍会运行。

elsetry 块未抛出异常完成时运行。finally 无条件运行,包括在 returnbreakcontinue 或未处理的异常之后。如果 finally 和调用代码都有 return 语句,finallyreturn 优先。当无法使用 with 时,带文件句柄或数据库连接的 finally 是次要的安全网。

python
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 自己抛出异常。这就是你如何让函数明确地向调用者发出问题信号,而不是悄悄返回错误值。

raise ExceptionType("message") 创建并抛出一个异常。在 except 块内,不带参数的 raise 会重新抛出当前异常。从你的函数中抛出异常,可以让其错误条件变得明确,这样调用者就可以有针对性地处理它们。

raise expr 求值 expr 得到异常实例并设置 __traceback__。单独的 raise 会重新抛出当前异常而不修改追溯。raise B from A 设置 B.__cause__ = A(显式链);raise B from None 抑制上下文显示。在捕获并记录之后,raise 是在保留原始追溯的同时进行传播的干净方式。

python
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

这让你的函数对它期望什么变得明确,并向调用者清晰地发出问题信号。

自定义异常类

对于较大的程序,你可以通过继承 Exception 定义自己的异常类型。这让调用者可以将你的特定错误与其他类型的错误分别捕获。

自定义异常构建了一个层级结构,调用者可以在合适的具体程度上捕获。继承 Exception 通常就足够了。对于异常族,创建一个基础异常类并为特定错误模式继承它;调用者随后可以捕获基类来处理所有变体。

自定义异常应继承 Exception(而不是 BaseException)。添加带有领域特定字段的 __init__ 让调用者能够以编程方式检查异常,而不只是读取消息字符串。异常层级让调用者选择具体程度:except PaymentError 捕获所有与支付相关的错误;except InsufficientFundsError 只捕获那个特定情况。

python
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 -= amount
python
try:
    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.load()json.dump() 作用于文件对象。json.loads()json.dumps() 作用于字符串。dump/dumps 中的 indent= 参数使输出对人友好。无效 JSON 会抛出 json.JSONDecodeError(ValueError 的子类)。

json.load() 是一个流式解析器:它会逐步从文件对象读取。json.dumps() 返回一个字符串;json.dump() 写入支持 .write() 的文件类对象。dump/dumpsdefault= 参数处理无法 JSON 序列化的类型;它接收无法序列化的对象,应返回可序列化的内容。load/loads 中的 object_hook 拦截每个解析得到的对象字典,支持自定义反序列化。

从文件读取 JSON:

python
import json

with open("config.json", "r") as f:
    config = json.load(f)    # 将 JSON 解析为 Python 字典/列表

print(config["setting"])

将 JSON 写入文件:

python
import json

data = {"name": "小明", "score": 87, "active": True}

with open("output.json", "w") as f:
    json.dump(data, f, indent=2)    # indent= 使其对人友好

JSON 到 Python 类型的映射:

JSONPython
object {}dict
array []list
string ""str
numberintfloat
true / falseTrue / False
nullNone

要在不接触文件的情况下在 JSON 字符串和 Python 对象之间转换:

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,在下次运行时读回,如果还没有存档文件就退回到默认值:

python
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)

加载配置文件并保存结果,对每种失败模式进行专门的异常处理:

python
import json

def load_config(path: str) -> dict:
    try:
        with open(path, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        raise FileNotFoundError(f"Config file not found: {path}")
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON in {path}: {e}")

def save_results(results: list[dict], path: str) -> None:
    with open(path, "w") as f:
        json.dump(results, f, indent=2)
    print(f"Saved {len(results)} result(s) to {path}")

config  = load_config("experiment.json")
results = [{"epoch": 1, "loss": 0.82}, {"epoch": 2, "loss": 0.61}]
save_results(results, "results.json")

一个结构化的日志写入器,将带时间戳的条目追加到文件中,并设有一个顶层处理器来捕获和记录意外失败:

python
import json
from datetime import datetime

LOG_FILE = "run.log"

def log(level: str, message: str) -> None:
    ts    = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    entry = f"[{ts}] [{level.upper():7}] {message}\n"
    with open(LOG_FILE, "a") as f:
        f.write(entry)

def process(config_path: str) -> None:
    log("info", f"Starting job, config: {config_path}")
    try:
        with open(config_path) as f:
            config = json.load(f)
        log("info", f"Loaded config: {config}")
    except FileNotFoundError:
        log("error", f"Config not found: {config_path}")
        raise
    except json.JSONDecodeError as e:
        log("error", f"Bad JSON in config: {e}")
        raise

try:
    process("config.json")
except Exception as e:
    log("critical", f"Job failed: {e}")

记录日志后重新抛出可以为调用者保留原始追溯。顶层的 except Exception 捕获任何漏过的异常,将其作为严重级别记录,并让进程干净地退出。