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

捕获异常的最佳实践

  1. 具体优先于通用:先捕获具体异常,再捕获通用异常
  2. 不过度捕获:不要用 except Exception 来隐藏所有错误
  3. 记录异常:在 except 块中记录错误日志
  4. 不要忽略异常:至少打印或记录异常信息
  5. 清理资源:使用 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 编程中的重要组成部分:

  1. try-except:捕获和处理异常
  2. else:在没有异常时执行
  3. finally:无论是否有异常都执行
  4. raise:抛出异常
  5. 异常链:保留原始异常的上下文
  6. 自定义异常:创建自己的异常类型
  7. 最佳实践:具体异常优先、记录异常、清理资源

掌握异常处理对于编写健壮的 Python 程序至关重要。在下一节中,我们将学习如何自定义异常和使用 raise 语句。