try-except
try 语句是 Python 异常处理的核心机制,它允许程序在可能出错的代码周围设置"防护网",并在异常发生时执行预定的恢复逻辑,而不是直接崩溃。
基本结构:try/except
try 子句包裹可能触发异常的代码。如果执行期间发生异常,Python 会跳过 try 中剩余的语句,转而寻找匹配的 except 子句。
while True:
try:
x = int(input("请输入一个整数:"))
break
except ValueError:
print("输入无效,请重新输入...")
这个循环会持续请求输入,直到用户提供一个能被 int() 转换的有效整数。如果输入 "abc",int() 抛出 ValueError,except ValueError 捕获它并打印提示,然后循环继续。如果用户按 Ctrl+C,会触发 KeyboardInterrupt——由于它不是 ValueError 的子类,不会被这个 except 捕获,程序正常中断。
多 except 子句与捕获顺序
一个 try 可以跟随多个 except 子句,为不同异常类型指定不同的处理逻辑。但最多只有一个处理程序会被执行,即第一个匹配的类型决定处理路径。
try:
f = open("data.txt")
s = f.readline()
i = int(s.strip())
result = 100 / i
except OSError as err:
print(f"文件操作失败:{err}")
except ValueError:
print("文件内容不是有效整数")
except ZeroDivisionError:
print("文件中存储的整数为零,无法做除法")
异常匹配遵循继承层次:except 子句中指定的类会匹配该类本身及其所有子类的实例,但不会匹配其父类。这意味着 except 子句的顺序至关重要——应该把更具体的子类放在前面,宽泛的父类放在后面。
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
# 输出:B C D
如果颠倒顺序,把 except B 放在最前面,由于 C 和 D 都是 B 的子类,所有异常都会被第一个子句捕获,输出变成 B B B。
元组合并捕获
当多种异常需要相同的处理逻辑时,可以用圆括号将异常类型组成元组,写在一个 except 子句中:
try:
data = json.loads(raw)
value = data["key"]
except (json.JSONDecodeError, KeyError, TypeError) as e:
print(f"数据解析失败:{type(e).__name__}: {e}")
这种方式让代码更紧凑,同时保持了对不同异常类型的精确捕获。注意元组内的顺序不影响匹配行为,因为它们在同一个 except 子句中。
as 绑定:获取异常实例
在 except 子句中使用 as 可以将异常实例绑定到一个变量,从而访问异常的参数、类型和自定义属性。
try:
raise Exception("参数1", "参数2")
except Exception as inst:
print(type(inst)) # <class 'Exception'>
print(inst.args) # ('参数1', '参数2')
print(inst) # ('参数1', '参数2')
x, y = inst.args # 解包参数
print(f"x={x}, y={y}")
异常实例的 args 属性以元组保存构造时传入的所有参数。内置异常通常覆盖 __str__(),使得 print(inst) 显示人类可读的消息。对于自定义异常,可以在类中定义 __str__() 来控制输出格式。
try:
open("/不存在的路径/文件.txt")
except OSError as e:
print(f"错误码:{e.errno}") # 2
print(f"错误消息:{e.strerror}") # No such file or directory
print(f"文件名:{e.filename}") # /不存在的路径/文件.txt
OSError 及其子类(如 FileNotFoundError)提供了 errno、strerror、filename 等属性,便于程序根据错误码做精细化处理。
else 子句
try 语句可以包含一个 else 子句,它必须放在所有 except 子句之后。else 中的代码只在 try 子句没有触发任何异常时执行。
for arg in sys.argv[1:]:
try:
f = open(arg, "r")
except OSError:
print(f"无法打开文件:{arg}")
else:
print(f"{arg} 有 {len(f.readlines())} 行")
f.close()
使用 else 的好处是避免意外捕获非 try 子句保护的代码所触发的异常。如果把 print(...) 和 f.close() 直接放进 try,当 len(f.readlines()) 或 f.close() 触发异常时,可能会被外层的 except OSError 误捕——尽管这些操作与文件打开无关。
finally 子句
finally 子句定义了无论是否发生异常都必须执行的清理操作。它会在 try 语句结束前最后执行,即使 try 中触发了未被捕获的异常,finally 也会先执行,然后再重新抛出该异常。
try:
raise KeyboardInterrupt
finally:
print("清理工作已完成")
# 输出:清理工作已完成
# 然后重新抛出 KeyboardInterrupt
finally 的典型用途是释放外部资源:关闭文件、断开网络连接、释放锁等。无论操作成功还是失败,资源都必须被妥善释放。
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("除数为零!")
return None
else:
print(f"结果是 {result}")
return result
finally:
print("执行 finally 子句")
print(divide(2, 1)) # 结果正常,finally 执行
print(divide(2, 0)) # 捕获异常,finally 执行
print(divide("2", "1")) # TypeError 未被捕获,finally 执行后重新抛出
注意一个特殊规则:如果 finally 子句中包含 return、break 或 continue,未被处理的异常将不会被重新引发。此外,如果 try 和 finally 中都有 return,最终返回值来自 finally。
def demo():
try:
return "try 的返回值"
finally:
return "finally 的返回值"
print(demo()) # finally 的返回值
Exception 通配捕获
Exception 是所有非致命异常的基类,可以用作通配符捕获绝大多数运行时异常。但好的做法是尽可能具体地声明要处理的异常类型,让意外的异常继续向上传播,便于发现真正的程序缺陷。
try:
risky_operation()
except Exception as err:
print(f"未预期的错误:{err=}, {type(err)=}")
raise # 记录后重新抛出,让上层处理
except Exception 不会捕获 SystemExit、KeyboardInterrupt 和 GeneratorExit,因为它们直接继承自 BaseException 而非 Exception。这使得按 Ctrl+C 仍能正常中断程序。最危险的写法是裸 except: 或 except BaseException:,它会吞掉所有异常包括系统退出信号,通常应当避免。
# ❌ 危险:会捕获 KeyboardInterrupt,导致 Ctrl+C 无法退出
# try:
# long_running_task()
# except:
# pass
# ✅ 安全:只捕获常规异常,保留中断能力
try:
long_running_task()
except Exception as e:
logger.error(f"任务失败:{e}")
raise
异常在调用链中的传播
异常处理程序不仅处理 try 子句中直接发生的异常,还处理在 try 子句中调用的函数(包括间接调用)内部发生的异常。
def this_fails():
x = 1 / 0
try:
this_fails()
except ZeroDivisionError as err:
print(f"处理运行时错误:{err}")
this_fails() 内部触发的 ZeroDivisionError 会沿着调用栈向上传播,直到被 try/except 捕获。如果没有任何处理程序匹配,它就是未处理异常,程序终止并打印完整的 Traceback。