Python Try Except:如何正确处理异常
Updated on
你的 Python 脚本读取一个文件,解析数据,然后将其发送到 API。在你的电脑上它运行得完美无缺。然后它在一台服务器上运行,那里的文件路径是错误的,JSON 格式不正确,或者网络断开了。程序崩溃并输出回溯信息,整个管道停止。这就是 try/except 要解决的问题。与其让错误终止你的程序,不如捕获它们、处理它们,然后继续运行。
Python 中的异常是什么?
异常是一个打断程序正常流程的事件。当 Python 遇到无法执行的操作时——除以零、访问不存在的字典键、打开一个不存在的文件——它会创建一个异常对象并停止执行。如果没有任何东西捕获该异常,程序就会终止并打印回溯信息。
# This crashes the program
result = 10 / 0输出:
Traceback (most recent call last):
File "example.py", line 2, in <module>
result = 10 / 0
ZeroDivisionError: division by zero异常与语法错误不同。语法错误意味着 Python 根本无法解析你的代码。异常发生在执行期间,即代码已经被成功解析之后。
基本的 Try/Except 语法
try/except 代码块让你尝试可能失败的代码,并定义失败时该怎么做。
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero.")输出:
Cannot divide by zero.程序不会崩溃。Python 执行 try 中的代码。当 ZeroDivisionError 发生时,执行跳转到 except 代码块。try/except 之后的所有代码都正常继续。
你也可以捕获异常对象来查看错误消息:
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}")输出:
Error: division by zero捕获特定异常
Python 有数十种内置异常类型。捕获正确的类型使你的代码更精确。以下是你最常处理的异常。
ValueError
当函数接收到类型正确但值不合适的参数时引发。
try:
number = int("not_a_number")
except ValueError as e:
print(f"Invalid value: {e}")输出:
Invalid value: invalid literal for int() with base 10: 'not_a_number'TypeError
当操作应用于错误类型的对象时引发。
try:
result = "hello" + 42
except TypeError as e:
print(f"Type error: {e}")输出:
Type error: can only concatenate str (not "int") to strFileNotFoundError
当你尝试打开一个不存在的文件时引发。
try:
with open("nonexistent_file.txt", "r") as f:
content = f.read()
except FileNotFoundError:
print("File not found. Check the file path.")KeyError
当你访问一个不存在的字典键时引发。
data = {"name": "Alice", "age": 30}
try:
email = data["email"]
except KeyError as e:
print(f"Missing key: {e}")输出:
Missing key: 'email'IndexError
当你访问超出范围的列表索引时引发。
items = [10, 20, 30]
try:
value = items[5]
except IndexError:
print("Index out of range.")多个 Except 代码块
你可以用不同的 except 代码块处理不同的异常类型。Python 按顺序检查并执行第一个匹配的。
def parse_config(raw_value):
try:
parts = raw_value.split(":")
key = parts[0]
value = int(parts[1])
return {key: value}
except IndexError:
print("Config format error: missing colon separator.")
except ValueError:
print("Config format error: value is not a number.")
except AttributeError:
print("Config format error: input is not a string.")
parse_config("timeout:30") # Returns {'timeout': 30}
parse_config("timeout") # Config format error: missing colon separator.
parse_config("timeout:abc") # Config format error: value is not a number.
parse_config(12345) # Config format error: input is not a string.你也可以使用元组在一个 except 代码块中捕获多个异常:
try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Invalid input: {e}")Else 代码块
else 代码块仅在 try 代码块中没有发生异常时执行。它将"正常路径"代码与错误处理代码分开。
try:
number = int("42")
except ValueError:
print("That is not a valid number.")
else:
print(f"Successfully parsed: {number}")输出:
Successfully parsed: 42else 代码块之所以有用,是因为其中引发的任何异常都不会被前面的 except 代码块捕获。这防止了意外地隐藏成功路径代码中的 bug。
filename = "data.txt"
try:
f = open(filename, "r")
except FileNotFoundError:
print(f"File '{filename}' does not exist.")
else:
content = f.read()
f.close()
print(f"Read {len(content)} characters.")如果 open() 失败,except 代码块会处理。如果 open() 成功,else 代码块读取文件。f.read() 期间的任何错误不会在这里被捕获——它会向上传播,这是正确的行为。
Finally 代码块
finally 代码块总是会执行,无论是否发生异常。它是放置清理代码的正确位置:关闭文件、释放锁、断开数据库连接。
f = None
try:
f = open("data.txt", "r")
content = f.read()
except FileNotFoundError:
print("File not found.")
finally:
if f is not None:
f.close()
print("File handle closed.")即使 try 代码块返回一个值或引发一个未处理的异常,finally 代码块也会执行。
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
finally:
print("Division attempted.")
result = divide(10, 3)
# Output: Division attempted.
# result = 3.3333333333333335
result = divide(10, 0)
# Output: Division attempted.
# result = None完整的 Try/Except/Else/Finally 模式
以下是四个代码块如何协同工作:
import json
def load_config(filepath):
"""Load and parse a JSON config file."""
f = None
try:
f = open(filepath, "r")
data = json.load(f)
except FileNotFoundError:
print(f"Config file '{filepath}' not found.")
return {}
except json.JSONDecodeError as e:
print(f"Invalid JSON in '{filepath}': {e}")
return {}
else:
print(f"Config loaded successfully with {len(data)} keys.")
return data
finally:
if f is not None:
f.close()
print("File handle released.")
config = load_config("settings.json")| 代码块 | 何时执行 | 用途 |
|---|---|---|
try | 总是(最先) | 包含可能引发异常的代码 |
except | 仅当匹配的异常发生时 | 处理错误 |
else | 仅当 try 中没有异常发生时 | 执行成功路径逻辑 |
finally | 总是(最后) | 无论如何都必须运行的清理代码 |
常见内置异常参考
Python 包含一个内置异常的层次结构。以下是你最常遇到的那些。
| 异常 | 何时发生 | 示例 |
|---|---|---|
Exception | 大多数异常的基类 | 所有非系统退出异常的父类 |
ValueError | 类型正确但值不合适 | int("abc") |
TypeError | 操作的类型不正确 | "text" + 5 |
KeyError | 字典键不存在 | d["missing"] |
IndexError | 列表索引超出范围 | [1,2,3][10] |
FileNotFoundError | 文件不存在 | open("no.txt") |
ZeroDivisionError | 除以零或取模零 | 1 / 0 |
AttributeError | 对象没有该属性 | None.append(1) |
ImportError | 模块导入失败 | import nonexistent |
OSError | 操作系统级操作失败 | 磁盘满、权限被拒绝 |
StopIteration | 迭代器没有更多元素 | 在耗尽的迭代器上调用 next() |
RuntimeError | 通用运行时错误 | 各种故障的统称 |
所有这些都继承自 BaseException。在实践中,你应该捕获 Exception 或其子类,永远不要直接捕获 BaseException(它包含 SystemExit 和 KeyboardInterrupt)。
使用 raise 引发异常
当你的代码检测到无效条件时,可以显式引发异常。这是你强制前置条件并向调用者发出错误信号的方式。
def set_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"Age must be between 0 and 150, got {age}")
return age
# Valid usage
print(set_age(25)) # 25
# Invalid usage
try:
set_age(-5)
except ValueError as e:
print(e) # Age must be between 0 and 150, got -5
try:
set_age("thirty")
except TypeError as e:
print(e) # Age must be an integer, got str你也可以在 except 代码块内使用不带参数的 raise 重新引发当前异常。当你想记录错误然后让它继续传播时,这非常有用。
import logging
try:
result = 10 / 0
except ZeroDivisionError:
logging.error("Division by zero encountered")
raise # re-raises the original ZeroDivisionError自定义异常类
对于较大的项目,定义你自己的异常类。自定义异常使错误处理更具可读性,并允许调用者捕获特定的故障模式。
class ValidationError(Exception):
"""Raised when input data fails validation."""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"Validation failed on '{field}': {message}")
class DatabaseConnectionError(Exception):
"""Raised when the database connection fails."""
pass
# Usage
def validate_email(email):
if "@" not in email:
raise ValidationError("email", "Must contain @ symbol")
if "." not in email.split("@")[1]:
raise ValidationError("email", "Domain must contain a dot")
return email
try:
validate_email("userexample.com")
except ValidationError as e:
print(e) # Validation failed on 'email': Must contain @ symbol
print(e.field) # email
print(e.message) # Must contain @ symbol自定义异常应该继承 Exception,而不是 BaseException。将相关的异常分组在一个共同的基类下,以便调用者可以捕获广泛或狭窄的类别:
class AppError(Exception):
"""Base exception for this application."""
pass
class ConfigError(AppError):
pass
class NetworkError(AppError):
pass
# Caller can catch all app errors or specific ones
try:
raise NetworkError("Connection timed out")
except AppError as e:
print(f"Application error: {e}")Python 异常处理最佳实践
1. 永远不要捕获裸异常
裸露的 except: 会捕获一切,包括 KeyboardInterrupt 和 SystemExit。这会隐藏 bug 并使调试变成噩梦。
# BAD - catches everything, hides real bugs
try:
do_something()
except:
pass
# GOOD - catches specific exceptions
try:
do_something()
except ValueError as e:
logging.warning(f"Invalid value: {e}")如果你必须捕获广泛的范围,请改用 except Exception。这仍然允许 KeyboardInterrupt 和 SystemExit 传播。
2. 保持 Try 代码块小巧
只将可能引发异常的代码放在 try 代码块内。大的 try 代码块使得不清楚是哪一行导致了错误。
# BAD - too much code in try
try:
data = load_data()
cleaned = clean_data(data)
result = analyze(cleaned)
save_results(result)
except Exception as e:
print(f"Something failed: {e}")
# GOOD - narrow try blocks
data = load_data()
cleaned = clean_data(data)
try:
result = analyze(cleaned)
except ValueError as e:
print(f"Analysis failed: {e}")
result = default_result()
save_results(result)3. 记录异常,不要静默处理
用 pass 吞掉异常会产生不可见的 bug。始终记录或报告错误。
import logging
try:
process_record(record)
except ValueError as e:
logging.error(f"Failed to process record {record['id']}: {e}")4. 使用具体的异常类型
尽可能捕获最具体的异常类型。这防止意外处理你没有预料到的错误。
| 方式 | 捕获范围 | 风险等级 |
|---|---|---|
except: | 包括 SystemExit 在内的一切 | 非常高 |
except Exception: | 所有标准异常 | 高 |
except ValueError: | 仅 ValueError | 低 |
except (ValueError, TypeError): | 两个特定类型 | 低 |
5. 在 Finally 中清理资源或使用上下文管理器
对于文件句柄、数据库连接和锁,始终使用 finally 或(更好的是)with 语句。
# Prefer context managers for resource cleanup
with open("data.txt", "r") as f:
content = f.read()
# File is automatically closed, even if an exception occurs实际应用示例
读取和解析 JSON 文件
import json
def read_json_config(filepath):
"""Read a JSON configuration file with proper error handling."""
try:
with open(filepath, "r") as f:
config = json.load(f)
except FileNotFoundError:
print(f"Config file not found: {filepath}")
return None
except PermissionError:
print(f"No permission to read: {filepath}")
return None
except json.JSONDecodeError as e:
print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
return None
else:
print(f"Loaded config with keys: {list(config.keys())}")
return config
config = read_json_config("app_config.json")
if config:
db_host = config.get("database_host", "localhost")发起 HTTP API 调用
import urllib.request
import urllib.error
import json
def fetch_user(user_id):
"""Fetch user data from an API with retry logic."""
url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
with urllib.request.urlopen(url, timeout=5) as response:
data = json.loads(response.read().decode())
return data
except urllib.error.HTTPError as e:
if e.code == 404:
print(f"User {user_id} not found.")
return None
print(f"HTTP error {e.code} on attempt {attempt}")
except urllib.error.URLError as e:
print(f"Network error on attempt {attempt}: {e.reason}")
except json.JSONDecodeError:
print("API returned invalid JSON.")
return None
print(f"All {max_retries} attempts failed.")
return None
user = fetch_user(1)
if user:
print(f"Found user: {user['name']}")处理 CSV 数据
import csv
def process_sales_data(filepath):
"""Process a CSV file with robust error handling."""
results = []
try:
with open(filepath, "r", newline="") as f:
reader = csv.DictReader(f)
for row_num, row in enumerate(reader, start=2):
try:
amount = float(row["amount"])
quantity = int(row["quantity"])
results.append({
"product": row["product"],
"total": amount * quantity,
})
except KeyError as e:
print(f"Row {row_num}: Missing column {e}")
except ValueError as e:
print(f"Row {row_num}: Invalid number - {e}")
except FileNotFoundError:
print(f"File not found: {filepath}")
except PermissionError:
print(f"Cannot read file: {filepath}")
return resultsTry/Except 与 If/Else:何时使用
Python 遵循 EAFP 原则:"Easier to Ask Forgiveness than Permission"(请求宽恕比请求许可更容易)。这与 LBYL 方法形成对比:"Look Before You Leap"(三思而后行)。
# LBYL (Look Before You Leap) - using if/else
if "email" in user_data:
email = user_data["email"]
else:
email = "unknown"
# EAFP (Easier to Ask Forgiveness) - using try/except
try:
email = user_data["email"]
except KeyError:
email = "unknown"| 标准 | if/else (LBYL) | try/except (EAFP) |
|---|---|---|
| 最佳场景 | 检查廉价且错误常见 | 错误罕见或检查代价高 |
| 性能(无错误) | 稍慢(每次都有额外检查) | 稍快(无检查开销) |
| 性能(发生错误) | 相同 | 较慢(创建异常有开销) |
| 竞态条件 | 可能(状态可能在检查和使用之间改变) | 无(原子操作) |
| 可读性 | 对简单条件清晰 | 对可能以多种方式失败的操作更好 |
| 文件操作 | if os.path.exists(path) -- 文件可能在检查和打开之间被删除 | try: open(path) -- 处理实际的失败 |
| 字典访问 | if key in dict -- 简单快速 | try: dict[key] -- 或者直接使用 dict.get(key, default) |
使用 try/except 的场景:
- 失败情况很少(异常针对"无错误"路径进行了优化)。
- 检查本身与操作一样昂贵(例如,检查文件是否存在然后再打开它)。
- 一系列操作中可能出现多个问题。
- 竞态条件是一个问题(特别是涉及文件和网络资源时)。
使用 if/else 的场景:
- 条件检查廉价且失败常见。
- 你在处理之前验证用户输入。
- 逻辑作为条件语句更清晰易读。
使用 RunCell 更快地调试异常
当你在 Jupyter 笔记本中工作时,异常可能会打断你的分析流程。你在 DataFrame 的第 50,000 行遇到一个 KeyError,或者一个 TypeError 在管道的三个单元格深处浮出水面。追踪根本原因意味着滚动查看回溯信息和手动检查变量。
RunCell (opens in a new tab) 是一个直接在 Jupyter 内运行的 AI 代理。它读取完整的回溯信息,检查你当前作用域中的变量,并在上下文中建议修复方案。以下是它如何帮助异常处理:
- 回溯分析。 RunCell 解析异常链并精确定位哪个变量或操作导致了故障,即使在嵌套函数调用中也是如此。
- 修复建议。 不需要搜索 Stack Overflow,RunCell 生成一个可以立即运行的修正代码单元。它知道应该添加
try/except、修复类型转换还是处理缺失的键。 - 预防性检查。 RunCell 可以扫描你的代码并标记可能引发异常的操作——比如不使用
.get()访问字典键,或不检查零就进行除法——在你运行单元格之前。
由于 RunCell 在你现有的 Jupyter 环境中运行,它可以访问你的实际数据和变量。它提供的建议是针对你的具体情况的,而非通用建议。
常见问题
Python 中 try/except 和 try/catch 有什么区别?
Python 使用 try/except,而不是 try/catch。try/catch 语法属于 Java、JavaScript 和 C++ 等语言。在 Python 中,关键字是 except。功能是相同的:你尝试可能失败的代码并为失败情况定义一个处理程序。
Python 中可以使用多个 except 代码块吗?
可以。你可以链接任意数量的 except 代码块,每个捕获不同的异常类型。Python 按顺序评估并执行第一个匹配的代码块。你也可以使用元组在一个代码块中捕获多个异常:except (ValueError, TypeError) as e:。
什么时候应该在 try/except 中使用 else?
当你有只在 try 代码块成功时才应该运行的代码时,使用 else 代码块。主要好处是 else 代码块中引发的异常不会被前面的 except 代码块捕获,防止你意外地静默不相关的错误。
Python 中 finally 总是会执行吗?
是的。finally 代码块无论 try 代码块是正常完成、引发了已处理的异常还是引发了未处理的异常都会执行。即使 try 或 except 代码块包含 return 语句也会执行。唯一的例外是 Python 进程被外部终止或调用了 os._exit()。
如何在 Python 中创建自定义异常?
创建一个继承自 Exception 的新类。你可以添加自定义属性并重写 __init__ 方法。例如:class MyError(Exception): pass。对于更复杂的情况,添加错误代码或上下文数据等字段。始终继承 Exception,而不是 BaseException。
总结
Python 的 try/except 是处理运行时错误的标准机制。它让你捕获特定异常、运行清理代码,并在出现问题时保持程序稳定。完整的模式——try/except/else/finally——涵盖了每种场景:尝试操作、处理故障、运行成功逻辑和清理资源。
关键原则很简单。捕获具体的异常,而不是宽泛的异常。保持 try 代码块小巧。始终记录或报告错误而不是静默处理。使用 finally 或上下文管理器进行清理。在你自己的代码中引发有意义的异常并附带清晰的错误消息。
无论你是在读取文件、发起 API 调用还是处理用户输入,适当的异常处理是凌晨 2 点崩溃的脚本和记录错误并继续运行的脚本之间的区别。从基础开始——在可能失败的代码周围包裹一个简单的 try/except——然后随着项目的增长构建自定义异常层次结构。