Day 30 - 异常处理:try-except-else-finally
什么是异常?
异常(Exception)是 Python 程序在运行过程中发生的错误。当异常发生时,程序会停止执行,并创建一个异常对象。如果异常没有被捕获和处理,程序会打印一个回溯(traceback)并终止。
Python 的异常处理机制允许我们捕获异常并自定义处理方式,而不是让程序直接崩溃。这对于编写健壮的程序至关重要。
# 常见的异常类型
print(10 / 0) # ZeroDivisionError: division by zero
print(int("abc")) # ValueError: invalid literal for int()
print([1, 2, 3][10]) # IndexError: list index out of range
print({}["key"]) # KeyError: 'key'
try-except 基本语法
try:
# 可能发生异常的代码
result = 10 / 0
except ZeroDivisionError:
# 处理 ZeroDivisionError 异常
print("不能除以零!")
完整的异常处理结构
try:
# 可能发生异常的代码
num = int(input("请输入一个数字:"))
result = 10 / num
except ValueError:
# 处理值错误(输入不是数字)
print("输入无效,请输入数字!")
except ZeroDivisionError:
# 处理除以零错误
print("不能除以零!")
else:
# 只有在没有异常时才会执行
print(f"结果:{result}")
finally:
# 无论是否有异常都会执行
print("程序结束")
except 的多种写法
捕获特定异常
try:
value = int("abc")
except ValueError:
print("ValueError 被捕获")
try:
lst = [1, 2, 3]
value = lst[10]
except IndexError:
print("IndexError 被捕获")
捕获多种异常(方式1)
try:
value = int("abc")
except (ValueError, TypeError):
print("ValueError 或 TypeError 被捕获")
捕获多种异常(方式2)
try:
value = int("abc")
except ValueError:
print("ValueError 被捕获")
except TypeError:
print("TypeError 被捕获")
获取异常信息
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"异常信息:{e}")
print(f"异常类型:{type(e).__name__}")
完整的 try-except-else-finally
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("错误:除数不能为零")
return None
except TypeError:
print("错误:参数类型错误")
return None
else:
# 只有在没有异常时执行
print(f"计算成功:{a} / {b} = {result}")
return result
finally:
# 无论是否有异常都会执行
print("divide 函数执行完毕")
print("测试 10 / 2:")
divide(10, 2)
print("\n测试 10 / 0:")
divide(10, 0)
print("\n测试 '10' / 2:")
divide('10', 2)
异常对象的属性和方法
异常对象是 Python 的内置异常类的实例,具有以下常用属性:
try:
raise ValueError("这是一个错误信息", "额外的参数")
except ValueError as e:
print(f"异常类型:{type(e).__name__}")
print(f"异常消息:{e.args}") # ('这是一个错误信息', '额外的参数')
print(f"第一个参数:{e.args[0]}")
自定义异常类
class ValidationError(Exception):
"""验证错误异常"""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class PositiveNumberError(Exception):
"""必须是正数错误"""
pass
def validate_positive(value, name="value"):
if value <= 0:
raise PositiveNumberError(f"{name} 必须是正数,当前值:{value}")
try:
validate_positive(-5, "年龄")
except PositiveNumberError as e:
print(f"捕获异常:{e}")
try:
raise ValidationError("email", "邮箱格式不正确")
except ValidationError as e:
print(f"字段:{e.field},错误:{e.message}")
异常的层次结构
Python 的异常有一个层次结构:
BaseException ├── SystemExit ├── KeyboardInterrupt ├── GeneratorExit └── Exception ├── StopIteration ├── ArithmeticError │ ├── FloatingPointError │ ├── OverflowError │ └── ZeroDivisionError ├── LookupError │ ├── IndexError │ └── KeyError ├── OSError │ ├── FileNotFoundError │ ├── PermissionError │ └── TimeoutError └── ...# 捕获所有非系统退出的异常
try:
result = 10 / 0
except Exception as e:
print(f"捕获异常:{type(e).__name__}: {e}")
# 捕获多个相关异常
try:
result = 10 / 0
except (ZeroDivisionError, FloatingPointError) as e:
print(f"算术错误:{e}")
# except 子句的顺序很重要!
# 应该先捕获具体异常,再捕获通用异常
try:
result = 10 / 0
except Exception: # 这会捕获所有异常
print("捕获 Exception")
except ZeroDivisionError: # 这个永远不会执行
print("捕获 ZeroDivisionError")
raise 语句
# 重新抛出当前异常
try:
try:
raise ValueError("原始错误")
except ValueError:
print("内部:捕获 ValueError,重新抛出")
raise # 重新抛出当前异常
except ValueError:
print("外部:再次捕获 ValueError")
# 手动抛出异常
raise ValueError("手动抛出的异常")
# raise without argument(重新抛出当前异常)
try:
raise RuntimeError("错误")
except RuntimeError:
print("处理错误")
raise # 重新抛出
异常链(Exception Chaining)
Python 3 支持异常链,可以在抛出新异常时保留原始异常。
# 显式异常链(from)
try:
raise ValueError("原始错误")
except ValueError as e:
raise RuntimeError("新错误") from e
# 隐式异常链
try:
raise ValueError("原始错误")
except ValueError:
print("处理中发生新错误") # 如果这里抛出异常,会自动链接
raise RuntimeError("新错误")
# 抑制异常链
try:
raise ValueError("原始错误")
except ValueError as e:
raise RuntimeError("新错误") from None # 不链接原始异常
使用 else 的场景
else 子句只在 try 块没有发生任何异常时执行。
# 文件操作示例
def read_config(filename):
try:
with open(filename, 'r') as f:
config = {}
for line in f:
line = line.strip()
if line and not line.startswith('#'):
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
except FileNotFoundError:
print(f"配置文件不存在:{filename}")
return {}
except PermissionError:
print(f"没有读取权限:{filename}")
return {}
else:
# 只有成功读取并解析完才执行
print("配置读取成功!")
return config
finally:
# 清理工作
print("read_config 函数执行完毕")
使用 finally 的场景
finally 子句无论是否发生异常都会执行,常用于资源清理。
# 数据库连接示例
def fetch_data(query):
conn = None
cursor = None
try:
conn = create_connection()
cursor = conn.cursor()
cursor.execute(query)
result = cursor.fetchall()
return result
except DatabaseError as e:
print(f"数据库错误:{e}")
return None
finally:
# 确保资源被释放
if cursor:
cursor.close()
if conn:
conn.close()
print("数据库连接已关闭")
上下文管理器与异常
使用 with 语句可以更优雅地处理资源清理:
# 手动关闭文件(不推荐)
f = open('file.txt', 'w')
try:
f.write('hello')
except IOError:
print("写入失败")
finally:
f.close()
# 使用上下文管理器(推荐)
try:
with open('file.txt', 'w') as f:
f.write('hello')
except IOError:
print("写入失败")
# 文件自动关闭,无需 finally
捕获异常的最佳实践
- 具体优先于通用:先捕获具体异常,再捕获通用异常
- 不过度捕获:不要用
except Exception来隐藏所有错误 - 记录异常:在 except 块中记录错误日志
- 不要忽略异常:至少打印或记录异常信息
- 清理资源:使用 finally 或 with 语句
# 不推荐的写法
try:
result = risky_operation()
except Exception: # 太宽泛,隐藏了真实问题
pass
# 推荐的写法
try:
result = risky_operation()
except SpecificError as e:
logger.error(f"操作失败:{e}")
raise # 重新抛出,让调用者知道发生了错误
except AnotherError as e:
logger.warning(f"操作警告:{e}")
return default_value
常见异常处理模式
模式1:重试机制
import time
def retry_operation(func, max_attempts=3, delay=1):
"""重试操作"""
for attempt in range(max_attempts):
try:
return func()
except TemporaryError as e:
if attempt == max_attempts - 1:
raise
print(f"尝试 {attempt + 1} 失败:{e},{delay}秒后重试...")
time.sleep(delay)
# 使用
def fetch_data():
return api_call()
result = retry_operation(fetch_data, max_attempts=3)
模式2:优雅降级
def get_config_value(key, default=None):
"""获取配置值,失败时返回默认值"""
try:
return config[key]
except (KeyError, TypeError):
return default
# 使用
timeout = get_config_value('timeout', 30)
模式3:验证后执行
def process_data(data):
"""处理数据,验证失败时跳过"""
results = []
for item in data:
try:
validate(item)
results.append(transform(item))
except ValidationError:
print(f"跳过无效项:{item}")
return results
练习题
练习 1:安全的除法计算器
def safe_divide(a, b):
"""安全的除法运算"""
try:
result = a / b
except ZeroDivisionError:
print("错误:除数不能为零")
return None
except TypeError:
print("错误:参数必须是数字")
return None
else:
return result
finally:
print("计算完成")
# 测试
print(f"10 / 2 = {safe_divide(10, 2)}")
print(f"10 / 0 = {safe_divide(10, 0)}")
print(f"'10' / 2 = {safe_divide('10', 2)}")
print(f"10 / '2' = {safe_divide(10, '2')}")
练习 2:配置解析器
class ConfigParseError(Exception):
"""配置解析错误"""
pass
def parse_config_line(line):
"""解析配置行"""
line = line.strip()
# 跳过空行和注释
if not line or line.startswith('#'):
return None
# 解析键值对
if '=' not in line:
raise ConfigParseError(f"无效的配置行:{line}")
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
if not key:
raise ConfigParseError(f"配置键不能为空")
return key, value
def load_config(content):
"""加载配置"""
config = {}
for line_num, line in enumerate(content.split('\n'), 1):
try:
result = parse_config_line(line)
if result:
key, value = result
config[key] = value
except ConfigParseError as e:
print(f"第 {line_num} 行错误:{e}")
return config
# 测试
config_text = """
# 数据库配置
db_host=localhost
db_port=3306
db_name=testdb
# 应用配置
app_name=MyApp
debug=true
# 错误配置
invalid line
"""
config = load_config(config_text)
print(f"解析的配置:{config}")
练习 3:带超时的函数执行
import time
import threading
class TimeoutError(Exception):
"""超时错误"""
pass
def with_timeout(func, timeout, *args, **kwargs):
"""在指定超时时间内执行函数"""
result = [None]
exception = [None]
finished = [False]
def worker():
try:
result[0] = func(*args, **kwargs)
except Exception as e:
exception[0] = e
finally:
finished[0] = True
thread = threading.Thread(target=worker)
thread.start()
thread.join(timeout)
if not finished[0]:
raise TimeoutError(f"函数执行超时({timeout}秒)")
if exception[0]:
raise exception[0]
return result[0]
# 测试
def slow_function():
time.sleep(2)
return "完成"
def fast_function():
return "快速完成"
try:
print(with_timeout(fast_function, 1))
except TimeoutError as e:
print(f"超时:{e}")
try:
print(with_timeout(slow_function, 1))
except TimeoutError as e:
print(f"超时:{e}")
总结
异常处理是 Python 编程中的重要组成部分:
- try-except:捕获和处理异常
- else:在没有异常时执行
- finally:无论是否有异常都执行
- raise:抛出异常
- 异常链:保留原始异常的上下文
- 自定义异常:创建自己的异常类型
- 最佳实践:具体异常优先、记录异常、清理资源
掌握异常处理对于编写健壮的 Python 程序至关重要。在下一节中,我们将学习如何自定义异常和使用 raise 语句。