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

文件和异常

docs.scrimba.com

大多数做实际工作的程序都会接触文件系统:读取配置、写入结果、加载数据。当出现问题时,Python 会抛出异常,这是一个信号,表示发生了意外情况。本章介绍两者:如何将数据读入和写出文件,以及如何编写能够优雅地处理错误而不是直接崩溃的代码。

文件 I/O 和异常处理是让程序可靠运行的两个机制。open() 让你可以访问文件系统;with 确保即使出错也能清理文件。try/except 捕获特定的异常类型,这样你可以恢复而不是崩溃。它们共同构成了任何无人值守脚本的基础。

文件和异常是脚本与凌乱现实世界相接的地方:路径不存在、磁盘满了、输入无法解析。open() 返回一个文件对象,with 保证即使代码内部抛出异常也会关闭它。异常是 Python 表示失败的方式:当异常被抛出时,它会沿着导致它的函数调用栈向上传播,第一个匹配的 except 会处理它。这一章的核心纪律是:捕获你实际上能从中恢复的特定失败,让其余的错误大声浮出来,而不是吞掉一切并发布一个在无声中失败的脚本。

打开文件

open() 打开一个文件并返回一个对象,你可以从中读取或写入。你告诉它路径和你想对文件做什么(读、写或追加)。完成后总要关闭文件;with 语句会自动做这件事。

open(path, mode) 返回一个文件对象。模式字符串控制访问方式:"r" 读取,"w" 写入(先清空文件),"a" 追加到末尾。文件不是文本时加上 "b" 表示二进制模式。有一点从一开始就值得设置:对文本文件传递 encoding="utf-8"。不设置的话 Python 会使用机器的默认值,所以一个在你笔记本电脑上读得很好的文件在别人的电脑上可能会出现乱码。

open() 向操作系统请求一个文件句柄并返回一个包装在文件对象中的句柄。模式选择访问方式("r""w""a")以及你是获取文本还是原始字节("b")。会坑人的参数是 encoding:它决定了磁盘上的字节如何映射到字符。不设置的话 Python 会回退到机器的区域设置默认值,这在不同平台上有所不同,所以一个在一台机器上写的文件在另一台机器上可能解码错误。对文本总要传递 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 语句。

Juno打开文件open(path, mode) 给你一个文件对象,模式说明你想做什么:"r" 读取,"w" 写入(会清除之前的内容),"a" 追加到末尾。我曾经因为误用 "w" 而丢失过一个文件,我本来想用 "a",所以在输入模式前先想象一下。
Juno打开文件open(path, mode) 返回一个文件对象:"r" 读取,"w" 覆盖,"a" 追加,加上 "b" 表示二进制。对文本文件传递 encoding="utf-8" 以确保它在每台机器上都能读取相同的内容,让 with 处理关闭操作。
Juno打开文件open() 给你一个操作系统句柄上的文件对象;模式设置访问方式和文本与字节的选择。容易跨平台出问题的设置是 encoding:不设置它会继承机器的区域设置默认值,所以要养成为文本设置 encoding="utf-8" 的习惯。当 with 可以为你关闭文件时,不要依赖手动调用 .close()

with 语句

with open(...) 为你管理文件,当缩进块结束时自动关闭它,即使发生了错误。总是使用 with open(...) 而不是手动 open()/close() 这样更安全,也是标准做法。

with 是 Python 的上下文管理器语法:一个小协议,其中一个对象在块开始时获得"设置"调用,在块结束时获得"清理"调用。对于文件,设置返回打开的文件,清理关闭它。优势是即使块内抛出异常,清理也会运行,所以你得到 try/finally 的保障而不用在每个文件周围写一个。

上下文管理器(任何可以放在 with 后面的对象)定义两部分行为:进入时做什么,离开时做什么。with open(...) as f 运行进入步骤,将文件绑定到 f,并注册离开步骤,Python 在块结束时运行它,无论是正常结束还是因为异常被抛出。对于文件,离开步骤是 close()。两个实际的注意事项。你可以在一个语句中打开多个文件,with open(a) as f, open(b) as g:,两个都会按顺序关闭。并且离开步骤可以选择吞掉异常,这是像 contextlib.suppress(FileNotFoundError) 这样的助手的工作原理,所以要知道 with 块可以在上下文管理器这样编写时静默吸收错误。

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

# f 在这里被关闭,保证

with 是 Python 的上下文管理器语法:它为你运行设置和清理代码,这里打开文件并可靠地关闭它。你不需要知道它内部如何工作就能把它用于 open()

Junowith 语句with open(...) as f 打开文件,让你在缩进块内使用 f,然后在块结束时为你关闭它,即使出了问题。每次都改用它而不是自己调用 .close(),这是一件少了一件要忘掉的事。
Junowith 语句with 在块打开时运行设置,在块关闭时运行清理,并且即使发生异常清理也会触发。对于文件,那个清理就是 close(),所以你得到 try/finally 的安全性而不用写一个。总是优先于手动 open()/close()
Junowith 语句 上下文管理器在离开时关闭文件,无论块是结束还是抛出,所以 with 代替了手写的 try/finally 来清理。一次打开多个用一个 with。记住离开步骤可以压制异常,这正是 contextlib.suppress 的用途,当 with 块吞掉错误时也值得认出来。

读取文件

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

.read() 将整个文件加载到内存中作为一个字符串。.readline() 读取单一行,包含换行符。.readlines() 返回所有行的列表。对于任何可能很大的内容,直接用 for line in f 迭代文件对象:它每次拉一行,从不一次性保持整个文件,这样无论文件多大内存使用都保持平坦。

.read() 将整个文件返回为一个字符串,所以它的内存成本随文件大小而变化:对于配置文件没问题,对于多千兆字节的日志则是个问题。迭代文件对象,for line in f,每步读一行并在下一步前丢掉它,所以无论长度如何内存都保持平坦。这是默认应该用于未知大小文件的模式。.readlines() 看起来方便但一次性建立每行的列表,它与 .read() 有相同的内存占用,所以对循环来说没有任何优势。阻止经典内存不足错误的规则:首先尝试 for line in f 循环,只在你知道文件很小时才调用 .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)是读取大文件最高效的方式。

Juno读取文件.read() 给你整个文件作为一个字符串,.readline() 给你单一行。当文件可能很大时,用 for line in f 循环,它一次读一行,这样你从不把全部加载到内存。每行的 .strip() 清除尾部换行符。
Juno读取文件.read() 一次性加载所有内容,对小文件没问题,对大文件则有风险。默认用 for line in f:它每次流传一行,无论大小都保持内存平坦。.readlines() 看起来整洁但用和 .read() 一样多的内存。
Juno读取文件.read().readlines() 都把完整文件保存在内存中,所以它们随文件大小而变化。`for line in f` 循环每次读一行并丢掉它,保持内存平坦,这正是防止你在没有提前估计文件大小时内存不足惊喜的东西。默认使用循环,`.read()` 只在你知道它很小时使用。

写入文件

"w" 模式如果文件存在则完全覆盖它。"a" 模式添加到末尾。.write() 不会自动添加换行符;在每行末尾显式包含 "\n"。要一次写入多行,用 "\n".join() 连接它们。

.write(s) 写入一个字符串,不像 print(),不添加任何东西,没有空格也没有换行符,所以你自己放 "\n".writelines(lines) 将列表中的每个字符串逐个写回,同样没有分隔符添加,所以换行符必须已经在你的字符串中。要小心模式:"w" 在你打开文件的那一刻就清空它,在你写任何东西之前,所以打开 "w" 当你本来想要 "a" 时会丢掉旧内容。

关于写入有两件事会导致真实的错误。首先,你写的数据不一定立即写到磁盘:操作系统将它保存在缓冲区并分批写入,是 .close()with 为你调用)才确保所有内容都在磁盘上。如果一个长期运行的进程写入但从不关闭,输出的尾部可能坐在缓冲区中,在崩溃时丢失,所以在数据必须安全的地方关闭文件或调用 .flush()。其次,模式决定了损害:"w" 在打开时就清空文件,在任何写入之前,所以当你想要 "a" 时使用 "w" 会无警告地销毁之前的内容。.writelines(lines) 不添加它自己的分隔符,所以换行符必须已经在字符串中。

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

python
lines = ["第一行", "第二行", "第三行"]

with open("output.txt", "w") as f:
    f.write("\n".join(lines) + "\n")
Juno写入文件"w" 创建或覆盖文件,"a" 添加到末尾而不清除它。.write() 不为你添加换行符,所以自己在每行末尾加上 "\n"。对于一批行,"\n".join(lines) 先把它们缝在一起。
Juno写入文件"w" 在打开的那一刻清空文件,"a" 追加,所以选错了会静默扔掉旧内容。.write() 不添加任何东西,没有换行符,所以你提供 "\n".writelines() 写列表也不添加分隔符,换行符必须已在字符串中。
Juno写入文件"w" 在打开时立即清空文件,所以当你本来想用 "a" 时使用它会无警告地破坏之前的内容。写入坐在缓冲区中直到 .close()(`with` 调用它)冲刷它们,所以一个写入但从不关闭的进程在崩溃时可能丢失它的尾部。.write().writelines() 不添加换行符,那是你的事。

异常

当 Python 遇到它无法处理的问题时,它会抛出一个异常:一个错误,描述什么出了问题以及在哪里。如果你不处理它,你的程序会崩溃并打印一个追踪。下表显示了你将遇到的最常见的异常。

异常是一个对象,异常形成一个系族树:像 FileNotFoundError 这样的特定异常是更广泛异常(OSError)的一种,几乎你捕获的所有异常都是 Exception 的一种。那个树就是让捕获工作的东西:捕获父类型也会捕获它的子异常。当一个被抛出时,Python 放弃当前行并回溯通过导致它的调用,寻找匹配的 except。如果它找不到,程序停止并打印一个追踪:从错误发生到顶部的调用列表。

异常形成一个类层次结构(类型的族树),那个树是你捕获它们时的全部游戏:一个 except 对于父类型也捕获它下面的每个类型,所以 except Exception 是一个宽网,except FileNotFoundError 是一个窄网。树中值得记住的一件事是 KeyboardInterrupt(用户按 Ctrl-C)和 SystemExit(干净关闭请求)在 Exception 旁边,不是在它下面。这是故意的:它意味着一个宽泛的 except Exception 让这两个通过,所以 Ctrl-C 即使在捕获所有内容的内部也能停止你的程序。这正是为什么你要使用 except Exception 而不是裸 except:,它也捕获这两个,并把用户困在他们试图退出的程序内部。

你会遇到的常见异常:

异常何时发生
FileNotFoundErroropen() 找不到文件
ValueError函数得到类型正确但内容错误的值,例如 int("abc")
TypeError完全错误的类型,例如 "hello" + 5
KeyError字典键不存在
IndexError列表索引超出范围
ZeroDivisionError除以零
AttributeError对象没有那个属性或方法
Juno异常 异常是 Python 告诉你出了问题。你将经常遇到几个:`FileNotFoundError`、`ValueError`、`KeyError`、`TypeError`。不处理它们会停止程序并打印追踪,看起来令人惊讶但实际上是一张指向它破裂的地方的地图。
Juno异常 异常是按族树排列的对象,捕获父类型也会捕获它的子异常。当一个被抛出时 Python 回溯通过调用寻找匹配的 `except`,如果找不到则打印追踪。知道常见的名字,`ValueError`、`KeyError`、`FileNotFoundError`,让你捕获正确的。
Juno异常 类型层次结构是全部要点:`except Exception` 是宽网,一个特定类型是窄网。`KeyboardInterrupt` 和 `SystemExit` 在 `Exception` 旁边,不是在它下面,所以 Ctrl-C 仍然逃脱 `except Exception`。那是使用它而不是裸 `except:` 的原因,它把用户困在他们试图退出的程序内部。

try / except

在可能失败的代码周围包裹 try 块。如果发生异常,匹配的 except 块处理它而不是崩溃。对你捕获的异常类型要具体:用裸 except: 捕获所有东西会隐藏真实的错误。

try/except 捕获一个特定的异常类型并运行处理程序而不是崩溃。命名类型很重要:捕获你实际上能从中恢复的那个,所以无关的错误能大声浮出来而不是被吞掉。当你想读异常的消息时用 as e 绑定它。列出几个 except 语句对于不同的类型,Python 使用第一个匹配的。

一个 except SomeType 匹配当抛出的异常是那个类型或它的子类型(它下面树中更特定的一种),这就是捕获父异常会捕获它所有子异常的原因。Python 从上到下尝试每个 except 并在第一个匹配处停止,所以特定的优先于一般的:一个宽泛的语句放在首位会捕获所有东西,下面更窄的永不运行。阻止 try/except 隐藏错误的纪律:捕获你能处理的最窄的类型,保持 try 体到实际上能抛出的一行,让其他所有都传播。一个 try 包裹二十行会把一个预期的失败变成对其他十九个的掩盖。

python
try:
    value = int("abc")
except ValueError:
    print("那不是一个有效的数字")

对你捕获的异常类型要具体。用裸 except: 捕获所有异常会隐藏错误:

python
# 不好,捕获包括程序员错误的所有东西
try:
    result = do_something()
except:
    pass

# 好,只捕获你预期的和实际能处理的
try:
    result = do_something()
except FileNotFoundError:
    print("文件未找到")
Junotry / except 将可能失败的代码放在 try 块中,匹配的 except 会运行而不是程序崩溃。命名异常你期望的,如 ValueError,而不是裸 except:,它捕获所有东西并从你那里隐藏真实的错误。捕获你实际能处理的,让其他的显示。
Junotry / excepttry/except 捕获一个命名的类型并恢复而不是崩溃;as e 给你消息。捕获你能处理的特定类型所以无关的错误仍然浮出来。保持 try 体很小,包裹二十行会把一个预期的失败变成对其他的掩盖。
Junotry / except 一个 except 匹配它的类型和任何子类型,Python 取第一个匹配的语句,所以特定的优先于一般的或宽泛的会遮蔽其他的。捕获最窄的类型,保持 try 到实际能抛出的那行,让其他所有传播。一个宽泛的 try 隐藏比你想处理的失败多得多的东西。

捕获多个异常

你可以在分开的 except 块中处理不同的错误类型,或用一个元组在一个块中捕获几个类型。as e 部分给你访问错误消息的权限。

堆叠几个不同类型的 except 语句,Python 从上到下检查它们,取第一个匹配的。要以相同的方式处理几个类型,用元组分组它们:except (ValueError, ZeroDivisionError)as e 绑定异常所以你能读它的消息。顺序很重要:在一般的之前放特定的类型,或者一个宽泛的语句在上面会捕获所有东西,下面的永不运行。

JunoCatching multiple exceptions 顺序是陷阱。Python 取第一个 `except` 其类型匹配,一个宽泛的类型匹配它的更窄的亲戚,所以在 `except ValueError` 上面列出 `except Exception` 意味着 `ValueError` 语句是死代码永不运行。特定的优先于一般的。用 `except (A, B) as e` 分组类型。当你捕获一个错误只是为了添加上下文并传递它时,用 `from` 抛出新的:`raise ValueError("bad config") from e`。这在追踪中保持原始错误在新的下面可见,所以报告显示两个高级意义和低级原因而不是用一个替换另一个。
python
try:
    data = int(user_input)
    result = 100 / data
except ValueError:
    print("不是一个数字")
except ZeroDivisionError:
    print("不能除以零")

或在一个元组中捕获多个:

python
except (ValueError, ZeroDivisionError) as e:
    print(f"输入错误:{e}")

as e 绑定异常对象到一个名字所以你能检查消息。

Juno捕获多个异常 给每个错误类型它自己的 except 块,Python 运行第一个匹配的。要以相同的方式处理几个,在元组中列出它们:except (ValueError, ZeroDivisionError)。`as e` 部分给你错误对象所以你能打印它的消息。
Juno捕获多个异常 多个 except 语句从上到下运行,第一个匹配胜出,所以在一般的之前列出特定的类型或宽泛的会遮蔽其他的。一个元组,except (A, B),用一个块处理几个。`as e` 绑定异常所以你能读它的消息。
Juno捕获多个异常 特定的语句优先:一个宽泛的类型在一个窄的上面使那个窄的语句成为死代码。用 except (A, B) as e 分组共享响应。当你重新抛出来添加上下文时,使用 raise NewError("...") from e 所以原始原因在新错误下面保持可见而不是消失。

else 和 finally

else 仅在没有异常发生时运行。finally 总是运行,不管是否有异常。finally 对于必须无论如何都发生的清理很有用。

else 保持仅在 try 体干净完成时应该运行的代码。把它保持在 try 外面使意图清晰:你不是从成功路径意外地捕获异常。finally 是清理保证,它无论如何都运行,是否块成功、抛出、被捕获或甚至是从 return 出来。这就是什么让它成为释放一个无论如何都必须释放的资源的正确家。

else 仅在 try 体干净完成时运行,这保持成功路径代码在 try 外面所以它不能意外地触发你的一个 except 语句。finally 无条件地运行:在成功之后,在一个被捕获的异常之后,在传播中一个未被捕获的之后,甚至在 try 中的 return 之后。那个最后的情况隐藏一个值得知道的陷阱:一个在 finally 内的 return 覆盖一个来自 tryreturn,更糟糕,静默吞掉一个传播中的异常,所以错误消失无踪。保持 finally 用于清理,永不从它 return。对于文件,with 已经给你这个保证,所以主要当你持有一个没有上下文管理器的资源时才使用 finally

python
try:
    with open("data.txt") as f:
        content = f.read()
except FileNotFoundError:
    print("文件未找到,使用默认值")
    content = ""
else:
    print("文件成功加载")
finally:
    print("完成尝试加载文件")   # 总是运行

finally 最有用于清理(关闭连接,释放锁)即使你已经在为文件使用 with

Junoelse 和 finallyelse 仅在 try 没有抛出时运行,成功路径其余部分的整洁地方。finally 每次都运行,异常或否,这使它成为必须无论如何都发生的清理的地方。对于文件 with 已经覆盖清理,所以你会比预期更少地使用 finally
Junoelse 和 finallyelse 仅在干净路径上运行,保持成功代码在 try 外所以它不能绊住你的 exceptfinally 无条件地运行,甚至在 return 之外,所以它是你的清理保证。对于文件 with 已经做了这个,所以为没有上下文管理器的资源保存 finally
Junoelse 和 finallyelse 是干净路径代码在 try 外所以它不能触发你的 except 语句。finally 无论如何都运行,但永不从它 return:那会覆盖吞掉一个传播中的异常且错误无声地消失。保持 finally 用于清理,主要当没有 with 可依赖时。

raise

你可以用 raise 自己抛出异常。这就是你如何让你的函数清楚地向调用者报告问题而不是静默地返回错误值。

raise ExceptionType("message") 从你自己的代码抛出异常,这就是一个函数报告坏输入而不是用错误值继续的方法。在一个 except 块内,一个裸 raise(没有参数)重新抛出你捕获的异常,当你想要记录它或反应并仍让它传播时很有用。有目的地抛出使一个函数的失败模式成为它的契约的一部分,所以调用者能按名字捕获和处理它们。

raise SomeError("message") 有目的地报告失败,当函数获得它无法接受的输入时是正确的举动:大声拒绝打败返回一个稍后作为谜团浮出的错误答案。在一个 except 内,一个裸 raise 重新抛出你捕获的异常,它的原始追踪保持完整,所以你能记录和重新抛出而不擦除它来自哪里。当你想把一个低级错误转换为一个域错误时,使用 fromraise ConfigError("bad settings") from e 保持原始的在新的下面在报告中,所以读者看到意义和原因两者而不是用一个替换另一个。经验法则:重新抛出来添加上下文,永不来隐藏它。

python
def divide(a, b):
    if b == 0:
        raise ValueError("不能除以零")
    return a / b

这使你的函数显式它们期望什么并清楚地向调用者报告问题。

Junoraiseraise ExceptionType("message") 让你自己的代码大声标记一个问题而不是继续用一个坏值。这就是一个函数说"我不能用这个工作"所以调用者处理它。清楚和早期打败一个坏答案在稍后显示。
Junoraiseraise ExceptionType("message") 从你的代码报告失败,使一个函数的错误情况成为调用者如何使用它的一部分。一个裸 raise 在一个 except 内重新抛出你捕获的,保持原始追踪,所以你能记录并仍让它传播。
Junoraise 当输入无法被接受时有目的地抛出:大声拒绝打败稍后作为谜团浮出的错误答案。一个裸 raise 重新抛出原始追踪完整。要把低级错误翻译为域错误,使用 raise DomainError("...") from e 所以原因保持可见,重新抛出来添加上下文,永不来埋没它。

自定义异常类

对于更大的程序,你能通过继承 Exception 定义你自己的异常类型。这让调用者分开地捕获你的特定错误从其他类型的错误。

一个自定义异常是你自己的继承 Exception 的类(它变成一个新的异常种类,所以它插入相同的族树中)。收益是调用者能精确地捕获的一个名字,分开于内置错误。对于相关失败的一个组,做一个基类并为每个特定情况子类它:调用者捕获基础来处理整个组,或一个子类来单独一个,而不需解析消息字符串。

子类 Exception,不是 BaseException(也覆盖 Ctrl-C 和关闭的根,你不想要你的错误与其分组)。定义你自己的类型而不是重用 ValueError 的理由是族树:建立像 PaymentError 的基础并为每个模式子类它(InsufficientFundsErrorCardDeclinedError),一个调用者能捕获 PaymentError 来处理整个种类或子类对于一个情况,而不解析消息字符串。给类一个 __init__ 携带相关领域(帐户,金额)所以处理程序能检查代码中发生了什么而不是搔文本,这使错误成为一个程序能行动的东西,不仅打印。

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"无法取款 {amount},余额是 {self.balance}"
            )
        self.balance -= amount
python
try:
    account.withdraw(1000)
except InsufficientFundsError as e:
    print(f"交易被拒绝:{e}")
Juno自定义异常类 通过继承 Exception 做你自己的错误类型,一个空类体是足够开始的。它给调用者一个名字来捕获那是你的,分开于内置错误,所以你程序的问题独立站立。
Juno自定义异常类 一个自定义异常是继承 Exception 的类,给调用者一个精确的名字来捕获。对于相关失败,做一个基类并为每个情况子类它:捕获基础对于整个组,一个子类对于一个。你能添加领域来携带在消息字符串之外的细节。
Juno自定义异常类 子类 Exception,不是 BaseException,所以你的错误不与 Ctrl-C 分组。定义你自己的理由是树:一个 PaymentError 基础带特定的子类让调用者处理种类或一个情况而不搔消息文本。在 __init__ 中放相关领域所以处理程序作用于数据,不是字符串。

JSON

JSON 是每个人都说的格式:API、配置文件、数据导出。Python 的 json 模块直接处理它。json.load() 从文件读 JSON 到一个 Python dictionarylistjson.dump() 把一个 dictionary 或 list 写回到一个文件作为 JSON。

对分开在他们接触的。json.load() / json.dump() 和一个文件对象工作,json.loads() / json.dumps()(带 s 的那些)和一个在内存中的字符串工作。当一个人会读输出时传递 indent=2dump/dumps,否则它在一行出现。无效的 JSON 抛出 json.JSONDecodeError,这是一个 ValueError 的种类,所以一个处理程序捕获 ValueError 已经覆盖它。

四个名字是两个操作跨越两个源:load/dump 对于文件,loads/dumps 对于字符串(s 是"字符串")。在生产中捕获人们的细节是什么 JSON 无法代表。它没有日期、setDecimal 的概念,所以 json.dumps(datetime.now()) 抛出 TypeError。你通过传递 default= 处理它,一个函数 dump/dumps 调用在任何值上它不知道,应该返回 JSON 能持有的东西,通常 str(value)。另一个值得保持的规则:数字作为 float 往返,所以一个你写为 Decimal 为精确的值回来作为 float 并丢失它。永不存储金钱或需要精确小数的任何东西(见 Numbers 章)作为一个普通 JSON 数字。

从文件读 JSON:

python
import json

with open("config.json", "r") as f:
    config = json.load(f)    # 解析 JSON 到一个 Python dict/list

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
numberint or float
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")从一个字符串读。

JunoJSONjson.load(f) 把文件中的 JSON 变成 Python dicts 和 lists,json.dump(data, f) 写它们回出来。`s` 版本,json.loadsjson.dumps,对一个字符串做相同的而不是一个文件。当你想要输出可读时添加 indent=2
JunoJSONload/dump 处理文件,loads/dumps 处理字符串,`s` 是为了字符串。indent=2 使它人类可读。坏的 JSON 抛出 json.JSONDecodeError,这是一个 ValueError,所以捕获 ValueError 已经覆盖解析失败。
JunoJSON 两个操作跨越两个源:load/dump 对于文件,loads/dumps 对于字符串。JSON 没有日期、集合或 Decimal,所以转储那些抛出 TypeError 除非你传递 default=。并且数字作为 float 往返,所以永不存储金钱作为一个普通 JSON 数字,它在返回时丢失它的精确性。

实际中

一个小游戏的保存/加载模式:写状态到 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("游戏已保存。")

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)

加载配置文件和保存结果,每个失败模式都有特定的异常处理:

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"未找到配置文件:{path}")
    except json.JSONDecodeError as e:
        raise ValueError(f"在 {path} 中无效的 JSON:{e}")

def save_results(results: list[dict], path: str) -> None:
    with open(path, "w") as f:
        json.dump(results, f, indent=2)
    print(f"保存 {len(results)} 个结果到 {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"开始任务,配置:{config_path}")
    try:
        with open(config_path) as f:
            config = json.load(f)
        log("info", f"加载配置:{config}")
    except FileNotFoundError:
        log("error", f"未找到配置:{config_path}")
        raise
    except json.JSONDecodeError as e:
        log("error", f"配置中的坏 JSON:{e}")
        raise

try:
    process("config.json")
except Exception as e:
    log("critical", f"任务失败:{e}")

在记录后重新抛出保持原始追踪对于调用者。顶级 except Exception 捕获任何滑过的,将其记录为严重,并让进程干净地退出。