课程
当 Python 代码在运行时遇到问题时,通常会抛出异常。如果不处理,异常会导致程序崩溃。不过,使用 try-except 代码块,您可以捕获异常,从容恢复,让应用继续运行。
本教程并不讲解异常的基础知识,我们已在 Python 异常与错误处理指南中覆盖。这里我们将深入实践 try-except:如何组织代码块、避免常见错误,并应用让代码在真实场景更可靠的最佳实践。
读完后,您不仅会理解 try-except 如何工作,更会掌握“Pythonic”的用法;编写清晰、易维护、可用于生产的错误处理代码。
如果您仍在学习 Python,我建议查看 Python 编程基础技能路径,帮助您构建所有关键技能。
为何聚焦 try-except?
在 Python 中,错误处理的根本在于设计能应对意外情况的程序。尽管有多种工具可用于处理错误,try-except 是 Python 方法论的支柱。
为何它如此重要?
- 它体现了 Python 的哲学。 在 Python 中,常见风格是 EAFP;“先斩后奏”比“事前请示”更容易。与其事前检查一切(文件是否存在?输入是否有效?服务器是否可用?),不如直接尝试操作。如果失败,再捕获异常并处理。这样代码更短、更干净。
- 它让代码在真实环境中保持运行。 用户输入会很乱、文件会丢失、API 会失败、网络会中断。借助
try-except,您可以让代码不至于在第一个问题处就崩溃,而是能智能地作出反应。 - 它对新手与专业人士都足够灵活。 初学者可能用
try-except捕获将输入转换为整数时的ValueError。生产系统可能用它记录错误、重试失败操作,或抛出自定义异常以便更易调试。
这就是本文聚焦 try-except 的原因。掌握它,意味着从只在完美条件下才有效的小脚本,迈向健壮、可维护、可用于生产的软件。
Try‑Except 代码块的结构
最简单的形式:
try:
risky_thing()
except SomeError:
handle_it()
当 Python 在 try 块中遇到错误时,会跳转到匹配的 except 块,而不是让程序崩溃。
您也可以用 else 和 finally 子句扩展这个模式:
try:
do_something()
except ValueError:
print("Bad value!")
else:
print("All good.")
finally:
clean_up()
下面是一个基础可运行的示例:
try:
x = int(input("Enter a number: "))
except ValueError:
print("That wasn’t a number.")
else:
print("You entered", x)
finally:
print("Done.")
except 负责处理错误;else 仅在成功时运行;finally 无论如何都会运行,即使您按下 Ctrl‑C 或退出。
您可以通过我们的 Python 用户输入:处理、验证与最佳实践 教程,系统掌握从基础提示到高级验证与错误处理的用户输入处理要点。
处理多个异常
有时,一个 except 并不够。不同的代码路径会以不同方式失败,而用裸 except: 去兜底并无助益;它会隐藏 bug,让调试变成噩梦。
假设您要把某个值转换为整数再执行除法。下面是不要这样做的示例:
try:
result = int(user_input) / 2
except:
print("Something went wrong.")
这会捕获一切,包括您可能并不想隐藏的内容,比如变量名拼写错误,或指向真实问题的意外异常。
相反,要具体化:
try:
result = int(user_input) / 2
except ValueError:
print("That wasn't a number.")
except ZeroDivisionError:
print("Division by zero?")
如果有几种异常需要用相同方式处理,您可以这样分组:
try:
result = some_function()
except (TypeError, ValueError):
print("Something was wrong with the data.")
这样,既能清楚表达可能出错的地方,又无需重复相同的处理代码块。
另外还有 except Exception,它比裸 except 要好,但仍然过宽。最好只针对您预期会发生的具体错误。
使用 else 与 finally 块
当 try 块中的代码成功运行且未抛出异常时,Python 会执行 else 块。
try:
user_id = get_user_id()
except LookupError:
print("User not found.")
else:
print("Welcome, user", user_id)
这有助于将异常处理逻辑与“正常路径”分开,使代码更易读。
然后是 finally。它不关心发生了什么;无论如何都会运行。有异常?会运行。无异常?也会运行。按下 Ctrl‑C 程序中断?仍会运行。非常适合做清理工作:
try:
f = open("data.txt")
process(f)
except IOError:
print("Couldn’t open file.")
finally:
f.close()
务必注意,如果文件未成功打开,f 变量可能不存在,因此要小心。处理文件时,可以使用 Python 的上下文管理器(with open(...) as f:),更安全。
如果您想学习在 Python 中有效处理 KeyError 异常的关键技巧(如异常处理与预防),推荐阅读我们的 Python KeyError 异常及其修复方法 教程。
Python try-except 的最佳实践与常见陷阱
说实话:写好 try‑except 有点像艺术。写得好,能防止程序崩溃;写得差,会把 bug 深埋,直到应用崩掉、用户投诉,您才发现。
以下是值得养成的一些习惯:
让 try 块尽量精简。不要把上百行代码包在一个 try 里。那样很难定位问题来源。只包裹可能失败的代码:
# Good
try:
value = int(data)
except ValueError:
print("Couldn’t convert.")
# Bad
try:
# Tons of unrelated logic
value = int(data)
do_more()
something_else()
except ValueError:
print("Huh?")
避免在操作前做条件检查,如果直接尝试并捕获错误更简单。Python 将此称为 EAFP(先斩后奏)。如果您先检查文件是否存在再打开,就引入了竞争条件。更好的做法:
try:
with open("file.txt") as f:
content = f.read()
except FileNotFoundError:
print("No file.")
这种模式避免了常见问题:文件可能在检查与打开之间被删除。直接尝试操作;若失败,再捕获失败。
同时,避免通过“捕获一切并什么也不做”来静默错误。不要这样:
try:
something()
except:
pass
除非您真的知道自己在做什么,否则这就是 bug 藏身并滋生的地方。若必须静默,请具体化:
try:
something()
except TimeoutError:
# okay to ignore in this case
pass
并把错误记录到某处。完全吞掉意味着未来的您将毫无头绪。
内置异常 vs 自定义异常
Python 自带了大量异常,覆盖的场景超乎想象。您可能已经遇到过:
ValueError:类型正确但值无效,比如int("hello")。TypeError:对错误类型执行操作,比如把字符串与数字相加。ZeroDivisionError:没错,就是除以零。FileNotFoundError:尝试打开不存在的文件。KeyError:字典缺少键。
这些很有用,对大多数脚本或应用已绰绰有余。但有时您会希望抛出更具描述性的异常,使之在您项目的语境内更合理,而不只是 Python 的语境。
比如,您在写一个处理在线订单的应用,当用户购买缺货商品时,希望抛出错误。您可以用 ValueError,但这太泛了,后续读代码的人看不出发生了什么。
这正是自定义异常大显身手的地方。
class OutOfStockError(Exception):
pass
def check_inventory(product_id):
if not in_stock(product_id):
raise OutOfStockError(f"Product {product_id} is out of stock.")
通过创建自定义异常,您引入了更丰富的语义。同时也获得了更强的控制力;您可以只捕获这一特定情形:
try:
check_inventory("shirt-001")
except OutOfStockError:
print("Sorry, that item is sold out.")
虽是小改动,却让代码更易理解与维护,尤其在大型项目中。此外,它也利于日志与监控:自定义异常名比模糊的 ValueError 更易检索。
日志、重新抛出与异常链
调试时常有这样的瞬间:看到回溯(traceback),盯着看,却不知道为什么失败。这时就需要日志。它不仅是记录错误,更是为未来的您(或团队)留下线索,方便定位问题。
比如,您想在捕获异常后记录一下再继续:
import logging
logging.basicConfig(level=logging.ERROR)
try:
do_something()
except ValueError as e:
logging.error("Failed to do something: %s", e)
raise
结尾的 raise 很重要——它在记录后重新抛出相同异常。没有它,您就把错误吞了。有时这样可以,但通常不行。
接下来是 raise from 小技巧。当您在处理一种错误时需要抛出另一种错误,又不想丢失原始异常,Python 允许将异常链起来:
try:
connect_to_database()
except TimeoutError as e:
raise ConnectionError("Database unavailable.") from e
这样回溯能讲全故事。您会看到新的 ConnectionError,也能看到导致它的 TimeoutError。
您也可以抑制原始错误(通常不建议)如下:
raise ConnectionError("Just this error, nothing else.") from None
除非有充分理由,保留完整异常链更有助于大家理解问题及其因果。
您可以在我们的 Python 日志记录教程 中学习日志记录基础。
真实世界的示例与用例
抽象地谈论异常处理是一回事,看到实际代码中的应用就会豁然开朗。
以用户输入为例。问问任何做过命令行工具或表单校验的人:用户能输入各种稀奇古怪的东西。您要一个数字?有人会输入“twelve”。或者粘贴电话号码。或者直接回车。这很常见。
与其写一长串“如果……怎么办”的检查,不如这样做:
while True:
user_input = input("Enter a number: ")
try:
number = int(user_input)
break
except ValueError:
print("Try again with a whole number.")
这段代码在遇到垃圾输入时不会慌张。它会提示用户重试,并循环直到输入有效。比为每个边界情况堆 if 语句干净多了。
再看一个:读取可能不存在的文件。
try:
with open("config.json") as f:
settings = f.read()
except FileNotFoundError:
print("Missing config file. Using defaults.")
settings = "{}"
无需先检查文件是否存在。直接尝试打开,如果失败,再继续处理。若您事先检查(os.path.exists()),文件可能会在检查与打开之间被他人删除。这就是竞争条件,调起来很痛苦。
网络请求更是异常的高发地带。您无法指望互联网总是稳定。服务器会宕机、连接会断、DNS 会失败。所以如果您这样做:
import requests
try:
response = requests.get("https://example.com/data")
response.raise_for_status()
except requests.exceptions.RequestException as e:
print("Network problem:", e)
这个 RequestException 基类能方便地捕获 requests 可能抛出的几乎所有异常,从超时到错误响应。除非要区别对待,否则无需写十个不同的 except。
若您在编写自动化脚本或后端服务,用 try‑except 包裹关键逻辑,往往决定了是单个任务失败还是整个系统“黑屏”。您希望错误被记录、可恢复的任务被重试、不可恢复的任务能干净地停止,而不是让晦涩的堆栈轨迹在日志中没完没了地滚动。
通过我们的 Python 自动化:完整指南 教程,了解自动化的基础概念、关键库、数据处理、AI 增强与最佳实践。
Try-except 的高级用法
到现在,您已经见识到 try‑except 的灵活性。但灵活是一把双刃剑,很容易从有用滑向草率。以下是保持克制的方法。
捕获具体异常。若知道可能出错的类型,就把它写出来:
try:
result = int(data)
except ValueError:
# Only catches invalid numbers, not everything else under the sun
避免裸 except。除非处理非常特殊的情况,否则不要用。它会捕获 KeyboardInterrupt、SystemExit 等您大概率不想静默的异常。
在能让代码更清晰时使用 else 与 finally。不要为了用而用。如果“正常路径”的代码被埋在 try 里,也许该把它挪到 else。
让 try 块保持小而精。包得越多,就越难定位是哪一行出错。只包裹可能失败的部分。
在需要时记录错误,尤其在生产环境。即使程序没崩,知道“何时何地、什么失败了”,也能让后续调试轻松很多。
自定义异常不是必须,但很有帮助。若存在应用特定的问题,定义自己的错误类型。它们能让日志更易读、代码更自解释。
最后一点:不要把异常当作流程控制,除非别无更好办法。写“先试这个;失败了再那样”的逻辑很诱人,但如果这是高频路径,通常有更干净的实现方式。
结语
正如本文所述,异常处理不只是为了避免崩溃。它是写出能预见麻烦、从容应对、继续运行,或在必要时以合乎情理的方式停下的代码。这决定了用户是收到有帮助的提示,还是被丢回终端面对一长串难以阅读的回溯。
我强烈建议通过我们的 OOP in Python 课程中的 Catching Exceptions in Python 章节进行更多关于异常的动手学习。
Python try-except 常见问答
Python 中的错误与异常有什么区别?
“错误”(error)通常指阻止 Python 启动执行代码的问题,比如缺少冒号或关键字拼写错误,这些是语法错误。异常(exception)发生在代码运行过程中,比如试图除以零,或打开一个不存在的文件。异常可以被捕获并处理,从而避免程序崩溃。
在 Python 中使用裸 except 是否不好?
大多数情况下是的。裸 except 会捕获一切,包括您并不想捕获的内容,比如键盘中断或系统退出信号。这会让调试变得困难。最好捕获具体异常,例如 ValueError 或 FileNotFoundError,这样您清楚自己在处理什么。
何时应在 try-except 中使用 else?
当您希望仅在 try 块未发生异常时运行某段代码时使用 else。它有助于将成功路径的逻辑与错误处理逻辑分开,使代码更易读、更易维护。
如果我已经有 except,还需要 finally 做什么?
finally 无论是否发生错误都会运行。它非常适合做清理工作:关闭文件、释放资源、回滚事务等。即使发生错误或提前退出,finally 仍会执行。
我是否应总是使用 try-except 而不是先检查条件?
不总是,但很多时候最好直接 try 操作并在失败时 catch 错误。Python 开发者称之为 EAFP(“先斩后奏比事前许可更容易”)。这通常更快,也能避免某些 bug,特别是当检查与操作之间状态可能变化时(比如文件被删除)。
