Veloris.
返回索引
概念基础 2026-02-14

Python异常处理:try-except让程序不再动不动就崩溃

3 分钟
784 words

Python异常处理:try-except让程序不再动不动就崩溃

异常处理是编写健壮程序的关键。Python使用try-except机制来捕获和处理运行时错误,使程序能够优雅地处理意外情况而不是直接崩溃。本篇将详细介绍异常的概念、处理方法和最佳实践。


1. 什么是异常

异常是程序运行时发生的错误,会中断程序的正常执行流程。

# 常见的异常示例
print(10 / 0)           # ZeroDivisionError: division by zero
print(int("abc"))       # ValueError: invalid literal for int()
print(my_list[100])     # IndexError: list index out of range
print(my_dict["key"])   # KeyError: 'key'
open("不存在.txt")      # FileNotFoundError

# 异常与错误的区别
# 语法错误(SyntaxError):代码不符合Python语法,无法运行
# 异常(Exception):代码语法正确,但运行时出错

与C语言的对比

特性C语言Python
错误处理返回错误码、errno异常机制
处理方式检查返回值try-except
传播方式手动传递自动向上传播

2. 异常处理语法

2.1 基本的try-except

try:
    # 可能出错的代码
    result = 10 / 0
except:
    # 处理异常
    print("发生了错误")

# 不推荐捕获所有异常,应该捕获特定异常

2.2 捕获特定异常

try:
    num = int(input("请输入数字:"))
    result = 10 / num
    print(f"结果:{result}")
except ValueError:
    print("输入的不是有效数字")
except ZeroDivisionError:
    print("除数不能为零")

2.3 捕获多个异常

# 方式1:多个except块
try:
    # 代码
    pass
except ValueError:
    print("值错误")
except TypeError:
    print("类型错误")
except Exception as e:
    print(f"其他错误:{e}")

# 方式2:元组形式
try:
    # 代码
    pass
except (ValueError, TypeError) as e:
    print(f"值或类型错误:{e}")

2.4 else子句

else块在没有异常发生时执行。

try:
    num = int(input("请输入数字:"))
except ValueError:
    print("输入无效")
else:
    # 没有异常时执行
    print(f"你输入的是:{num}")
    print(f"它的平方是:{num ** 2}")

2.5 finally子句

finally块无论是否发生异常都会执行,常用于清理资源。

try:
    f = open("file.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("文件不存在")
finally:
    # 无论如何都会执行
    print("清理资源")
    if 'f' in locals() and not f.closed:
        f.close()

# 完整的try语句
try:
    # 尝试执行的代码
    result = risky_operation()
except SpecificError:
    # 处理特定异常
    handle_error()
else:
    # 没有异常时执行
    process_result(result)
finally:
    # 总是执行(清理)
    cleanup()

3. 常见内置异常

异常说明示例
Exception所有异常的基类-
ValueError值错误int("abc")
TypeError类型错误"2" + 2
IndexError索引越界lst[100]
KeyError字典键不存在d["missing"]
FileNotFoundError文件不存在open("no.txt")
ZeroDivisionError除零错误1 / 0
AttributeError属性不存在"str".foo()
ImportError导入失败import xxx
NameError名称未定义print(undefined)
RuntimeError运行时错误-
StopIteration迭代器耗尽next(empty_iter)
PermissionError权限错误无权限访问文件
TimeoutError超时错误网络超时
# 异常层次结构(部分)
# BaseException
# ├── SystemExit
# ├── KeyboardInterrupt
# ├── GeneratorExit
# └── Exception
#     ├── ValueError
#     ├── TypeError
#     ├── IndexError
#     ├── KeyError
#     ├── FileNotFoundError
#     ├── ZeroDivisionError
#     └── ...

4. 异常信息获取

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"异常类型:{type(e).__name__}")
    print(f"异常信息:{e}")
    print(f"异常参数:{e.args}")

# 获取完整的堆栈信息
import traceback

try:
    result = 10 / 0
except ZeroDivisionError:
    # 打印堆栈信息
    traceback.print_exc()
    
    # 获取堆栈信息字符串
    error_info = traceback.format_exc()
    print(error_info)

# 使用logging记录异常
import logging

logging.basicConfig(level=logging.ERROR)

try:
    result = 10 / 0
except ZeroDivisionError:
    logging.exception("发生除零错误")  # 自动包含堆栈信息

5. 抛出异常

# 使用raise抛出异常
def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(e)

# 重新抛出异常
try:
    result = 10 / 0
except ZeroDivisionError:
    print("记录错误...")
    raise  # 重新抛出当前异常

# 抛出带有原因的异常
try:
    result = int("abc")
except ValueError as original:
    raise RuntimeError("数据处理失败") from original

# 条件性抛出
def validate_age(age):
    if not isinstance(age, int):
        raise TypeError("年龄必须是整数")
    if age < 0 or age > 150:
        raise ValueError("年龄必须在0-150之间")
    return True

6. 自定义异常

# 简单的自定义异常
class MyError(Exception):
    pass

# 带有额外信息的自定义异常
class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

# 使用自定义异常
def validate_user(data):
    if not data.get("name"):
        raise ValidationError("name", "姓名不能为空")
    if not data.get("email"):
        raise ValidationError("email", "邮箱不能为空")
    if "@" not in data.get("email", ""):
        raise ValidationError("email", "邮箱格式不正确")

try:
    validate_user({"name": "张三"})
except ValidationError as e:
    print(f"验证失败 - 字段:{e.field},原因:{e.message}")

# 异常层次结构
class AppError(Exception):
    """应用程序基础异常"""
    pass

class DatabaseError(AppError):
    """数据库相关异常"""
    pass

class NetworkError(AppError):
    """网络相关异常"""
    pass

class ConfigError(AppError):
    """配置相关异常"""
    pass

7. 异常链

# 异常链:保留原始异常信息
def read_config(filename):
    try:
        with open(filename) as f:
            return f.read()
    except FileNotFoundError as e:
        raise ConfigError(f"配置文件不存在:{filename}") from e

# 显式禁用异常链
try:
    result = int("abc")
except ValueError:
    raise RuntimeError("处理失败") from None  # 不显示原始异常

# 隐式异常链(在except块中抛出新异常)
try:
    try:
        result = 10 / 0
    except ZeroDivisionError:
        raise ValueError("计算错误")  # 隐式链接到ZeroDivisionError
except ValueError as e:
    print(f"异常:{e}")
    print(f"原因:{e.__cause__}")      # 显式链接的原因
    print(f"上下文:{e.__context__}")  # 隐式链接的上下文

8. 上下文管理器与异常

# with语句自动处理异常和资源清理
with open("file.txt") as f:
    content = f.read()
# 即使发生异常,文件也会被正确关闭

# 自定义上下文管理器
class DatabaseConnection:
    def __init__(self, host):
        self.host = host
        self.connection = None
    
    def __enter__(self):
        print(f"连接到 {self.host}")
        self.connection = "connected"
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("关闭连接")
        self.connection = None
        # 返回True表示异常已处理,不再传播
        # 返回False或None表示异常继续传播
        if exc_type is ValueError:
            print("处理了ValueError")
            return True
        return False

with DatabaseConnection("localhost") as db:
    print("执行数据库操作")
    # raise ValueError("测试异常")

# 使用contextlib简化
from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    print(f"获取资源:{name}")
    try:
        yield name
    finally:
        print(f"释放资源:{name}")

with managed_resource("文件") as r:
    print(f"使用资源:{r}")

9. 调试技巧

# 1. 使用assert进行调试断言
def calculate_average(numbers):
    assert len(numbers) > 0, "列表不能为空"
    assert all(isinstance(n, (int, float)) for n in numbers), "必须是数字"
    return sum(numbers) / len(numbers)

# 注意:assert可以被禁用(python -O)

# 2. 使用pdb调试器
import pdb

def buggy_function(x):
    result = x * 2
    pdb.set_trace()  # 在此处暂停,进入调试模式
    result = result + 10
    return result

# pdb常用命令:
# n (next): 执行下一行
# s (step): 进入函数
# c (continue): 继续执行
# p variable: 打印变量
# l (list): 显示代码
# q (quit): 退出

# 3. 使用breakpoint()(Python 3.7+)
def another_function(x):
    breakpoint()  # 等同于pdb.set_trace()
    return x * 2

# 4. 打印调试信息
def debug_print(*args, **kwargs):
    import sys
    print(*args, file=sys.stderr, **kwargs)

# 5. 使用logging
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def process_data(data):
    logging.debug(f"输入数据:{data}")
    result = data * 2
    logging.debug(f"处理结果:{result}")
    return result

10. 最佳实践

✅ 推荐做法

# 1. 捕获特定异常,而不是所有异常
try:
    value = int(user_input)
except ValueError:
    print("请输入有效数字")

# 2. 使用else分离正常逻辑
try:
    f = open("file.txt")
except FileNotFoundError:
    print("文件不存在")
else:
    content = f.read()
    f.close()

# 3. 使用finally确保资源清理
# 或更好的方式:使用with语句

# 4. 提供有意义的错误信息
if not data:
    raise ValueError("数据不能为空,请检查输入")

# 5. 记录异常信息
import logging

try:
    risky_operation()
except Exception as e:
    logging.exception("操作失败")
    raise

# 6. 使用自定义异常表达业务逻辑
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"余额不足:当前{balance},需要{amount}")

❌ 避免的做法

# 1. 不要捕获所有异常然后忽略
try:
    risky_operation()
except:
    pass  # 危险!隐藏了所有错误

# 2. 不要使用异常控制正常流程
# 错误
try:
    value = my_dict[key]
except KeyError:
    value = default

# 正确
value = my_dict.get(key, default)

# 3. 不要捕获BaseException
# 这会捕获KeyboardInterrupt和SystemExit
try:
    operation()
except BaseException:  # 不推荐
    pass

# 4. 不要在except块中引入新的bug
try:
    result = process(data)
except ValueError:
    result = process(None)  # 可能引发新的异常

11. 实战练习

练习1:安全的文件读取

"""
练习:实现一个安全的文件读取函数
"""
from pathlib import Path

def safe_read_file(filepath, default=None, encoding='utf-8'):
    """
    安全地读取文件内容
    
    Args:
        filepath: 文件路径
        default: 读取失败时的默认值
        encoding: 文件编码
    
    Returns:
        文件内容或默认值
    """
    try:
        path = Path(filepath)
        if not path.exists():
            print(f"文件不存在:{filepath}")
            return default
        
        return path.read_text(encoding=encoding)
    
    except PermissionError:
        print(f"没有权限读取文件:{filepath}")
        return default
    except UnicodeDecodeError:
        print(f"文件编码错误:{filepath}")
        return default
    except Exception as e:
        print(f"读取文件失败:{e}")
        return default

# 测试
content = safe_read_file("test.txt", default="")
print(content)

练习2:带重试的网络请求

"""
练习:实现带重试机制的函数装饰器
"""
import time
from functools import wraps

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """
    重试装饰器
    
    Args:
        max_attempts: 最大尝试次数
        delay: 重试间隔(秒)
        exceptions: 需要重试的异常类型
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    print(f"尝试 {attempt}/{max_attempts} 失败:{e}")
                    
                    if attempt < max_attempts:
                        print(f"等待 {delay} 秒后重试...")
                        time.sleep(delay)
            
            raise last_exception
        
        return wrapper
    return decorator

# 使用示例
@retry(max_attempts=3, delay=0.5, exceptions=(ValueError, ConnectionError))
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise ValueError("随机失败")
    return "成功"

# result = unreliable_function()

练习3:输入验证

"""
练习:实现用户输入验证
"""
class InputValidator:
    """输入验证器"""
    
    @staticmethod
    def get_int(prompt, min_val=None, max_val=None):
        """获取整数输入"""
        while True:
            try:
                value = int(input(prompt))
                
                if min_val is not None and value < min_val:
                    print(f"值不能小于 {min_val}")
                    continue
                
                if max_val is not None and value > max_val:
                    print(f"值不能大于 {max_val}")
                    continue
                
                return value
            
            except ValueError:
                print("请输入有效的整数")
    
    @staticmethod
    def get_float(prompt, min_val=None, max_val=None):
        """获取浮点数输入"""
        while True:
            try:
                value = float(input(prompt))
                
                if min_val is not None and value < min_val:
                    print(f"值不能小于 {min_val}")
                    continue
                
                if max_val is not None and value > max_val:
                    print(f"值不能大于 {max_val}")
                    continue
                
                return value
            
            except ValueError:
                print("请输入有效的数字")
    
    @staticmethod
    def get_choice(prompt, choices):
        """获取选择输入"""
        choices_str = "/".join(choices)
        while True:
            value = input(f"{prompt} ({choices_str}): ").strip()
            if value in choices:
                return value
            print(f"请输入以下选项之一:{choices_str}")

# 使用示例
# validator = InputValidator()
# age = validator.get_int("请输入年龄:", min_val=0, max_val=150)
# choice = validator.get_choice("是否继续", ["y", "n"])

12. 总结

🔑 核心要点

知识点要点
try-except捕获和处理异常
else没有异常时执行
finally总是执行,用于清理
raise抛出异常
自定义异常继承Exception类
异常链使用from保留原始异常
最佳实践捕获特定异常,提供有意义的错误信息

✅ 学习检查清单

  • 掌握try-except-else-finally语法
  • 了解常见内置异常类型
  • 能够抛出和自定义异常
  • 理解异常链的概念
  • 掌握调试技巧
  • 了解异常处理的最佳实践

📖 下一步学习

掌握了异常处理后,让我们学习Python的模块与包:


常见问题 FAQ

💬 except后面要不要指定异常类型?

必须指定!裸except:会捕获所有异常包括KeyboardInterruptSystemExit,导致Ctrl+C都无法终止程序。至少写except Exception:

💬 什么时候该抛异常,什么时候该返回错误码?

Python风格是用异常。C语言习惯返回-1表示错误,但Python推崇EAFP(先试再说)而不是LBYL(先检查再做)。该抛就抛,让调用者决定怎么处理。


系列导航

End of file.