20·模块五:错误处理与调试进阶

断言(assert)与日志记录(logging)

assertloggingdebugging

第20课:断言(assert)与日志记录(logging)

1. 学习目标

通过本课学习,你将能够:

  • 理解断言(assert)与日志记录(logging)的核心思想与适用场景
  • 掌握使用 assert 语句进行开发阶段的快速检查
  • 学会使用 Python 内置的 logging 模块进行程序运行状态的记录
  • 区分断言与异常处理,并能在实际调试中合理选择工具
  • 实践在一个简单程序中结合使用断言和日志记录

2. 核心概念

断言 (assert)

断言就像是你在代码里设置的一个**"安全检查员"**。它的意思是:"我坚信这个条件在程序运行到这里时必然为真,如果为假,那一定是我的代码逻辑有严重错误。"

  • 目的:在开发、测试阶段,快速发现并定位那些"理论上绝不该发生"的逻辑错误。
  • 本质:如果条件为 True,程序继续;如果为 False,程序立即抛出一个 AssertionError 异常并终止。
  • 关键区别:它不是用来处理运行时可能出现的正常错误(如文件不存在、用户输入错误),而是用来捕获程序本身的逻辑缺陷。在最终交付的生产环境中,通常会禁用所有断言以提升性能。

日志记录 (logging)

日志记录就像是程序的**"航班记录仪"**。它可以在程序运行的关键节点留下记录,帮助你了解程序执行了什么操作、遇到了什么情况,尤其是在出现问题后,日志是回溯问题最重要的线索。

  • 目的:记录程序运行时的状态、信息、警告和错误,用于监控、调试和审计。
  • 核心组件
    • Logger(记录器):程序代码直接调用的对象。
    • Handler(处理器):决定日志发送到哪里(控制台、文件、网络等)。
    • Formatter(格式器):决定日志输出的格式(时间、级别、消息等)。
  • 日志级别:从低到高,代表事件的严重程度:
    • DEBUG:详细调试信息。
    • INFO:确认程序按预期工作。
    • WARNING:发生了意外,但程序还能继续。
    • ERROR:由于更严重的问题,程序的某个功能失败了。
    • CRITICAL:严重错误,程序可能无法继续运行。

3. 代码示例

示例1:断言的基本用法

def calculate_discount_price(original_price, discount_rate):
    """
    计算折扣后价格
    :param original_price: 原始价格
    :param discount_rate: 折扣率 (0.0 到 1.0 之间)
    :return: 折扣后价格
    """
    # 断言:折扣率必须在0到1之间(不包括0,因为0折扣没意义)
    assert 0 < discount_rate <= 1, f"折扣率必须在(0, 1]之间,但收到了:{discount_rate}"
    
    discounted_price = original_price * (1 - discount_rate)
    
    # 断言:折扣后价格不应为负(理论上不可能,除非逻辑出错)
    assert discounted_price >= 0, f"计算出的折扣价格为负数:{discounted_price}"
    
    return discounted_price

# 测试
try:
    price = calculate_discount_price(100, 0.2)  # 正常情况
    print(f"折扣后价格:{price}")  # 输出:折扣后价格:80.0
    
    price = calculate_discount_price(100, 1.5)  # 违反第一个断言:折扣率>1
    print(price)  # 这行不会执行
except AssertionError as e:
    print(f"捕获到断言错误:{e}")

示例2:日志记录的基础配置与使用

import logging

# 第一步:基本配置(通常在程序最开始执行一次)
logging.basicConfig(
    level=logging.DEBUG,  # 记录级别为DEBUG及以上的所有日志
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',  # 日志格式
    datefmt='%Y-%m-%d %H:%M:%S',  # 时间格式
    handlers=[
        logging.FileHandler('app.log'),  # 同时输出到文件
        logging.StreamHandler()         # 同时输出到控制台
    ]
)

# 第二步:创建一个记录器(通常用模块名)
logger = logging.getLogger(__name__)

# 第三步:在代码中使用不同级别的日志
def process_data(data):
    logger.debug(f"开始处理数据,数据长度:{len(data)}")  # DEBUG级别
    
    if not data:
        logger.warning("传入的数据为空列表!")  # WARNING级别
        return []
    
    try:
        result = [item * 2 for item in data]
        logger.info("数据处理成功完成。")  # INFO级别
        return result
    except TypeError as e:
        logger.error(f"数据处理过程中发生类型错误:{e}")  # ERROR级别
        raise
    except Exception as e:
        logger.critical(f"发生未预期的致命错误:{e}", exc_info=True)  # CRITICAL级别,打印完整堆栈
        raise

# 测试
data_sample = [1, 2, 3]
processed = process_data(data_sample)
print(processed)  # 输出:[2, 4, 6]

empty_data = []
processed_empty = process_data(empty_data)  # 会触发WARNING日志
print(processed_empty)  # 输出:[]

示例3:结合使用断言和日志进行调试

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("DebugDemo")

def validate_user_registration(username, password):
    """
    验证用户注册信息
    """
    logger.info(f"开始验证用户注册,用户名:{username}")
    
    # 使用断言检查函数内部逻辑的"不变式"(应始终为真的条件)
    # 例如:我们假设在调用这个函数前,用户名已经被去除了首尾空格
    assert username == username.strip(), "内部逻辑错误:用户名在传入前应该已被去除首尾空格"
    
    # 使用日志记录检查的开始和结果
    logger.debug(f"检查用户名长度...")
    if len(username) < 3 or len(username) > 20:
        logger.error(f"用户名长度无效(长度:{len(username)})")
        return False, "用户名长度需在3-20个字符之间"
    
    logger.debug(f"检查密码强度...")
    if len(password) < 8:
        logger.error("密码长度不足8位")
        return False, "密码长度需至少8位"
    
    logger.info("用户注册信息验证通过。")
    return True, "验证成功"

# 测试
success, message = validate_user_registration("Alice", "securepassword123")
print(f"结果:{success}, 信息:{message}")

# 触发断言错误(假设我们错误地传递了带空格的用户名)
try:
    validate_user_registration("  Bob  ", "password123")  # 注意:用户名有空格
except AssertionError as e:
    logger.critical(f"程序存在逻辑缺陷:{e}")

4. 实践练习

练习1:使用断言验证函数参数

要求:编写一个函数 divide_numbers(a, b),用于计算 a / b

  • 使用断言确保 b 不为0。
  • 使用断言确保 ab 都是数字(int 或 float)。
  • 调用函数并测试正常情况和会触发断言的情况。
  • 预期输出:正常情况返回除法结果;触发断言时打印错误信息。

练习2:配置日志记录器

要求

  1. 创建一个日志记录器,名字为 MyApp
  2. 将日志级别设置为 DEBUG
  3. 添加两个处理器:
    • StreamHandler 输出到控制台,格式包含时间、级别和消息。
    • FileHandler 输出到文件 my_app.log,格式包含时间、记录器名称、级别和消息。
  4. 在你的代码中至少使用 DEBUGINFOWARNING 各一条日志消息进行测试。

练习3:综合应用 - 密码验证器

要求:编写一个 password_validator(password) 函数,它应该:

  1. 使用日志记录验证过程的每一步(如"开始检查长度"、"检查是否包含大写字母")。
  2. 使用断言确保传入的 password 参数是字符串类型(这是一个不应发生的逻辑错误)。
  3. 返回一个布尔值(True/False)和一条消息。
  4. 验证规则(至少实现3条,例如:最小长度、必须包含数字、必须包含特殊字符)。
  5. 编写测试代码,验证不同密码,观察日志输出。

5. 常见错误

  1. 误用断言进行数据验证

    # 错误!文件可能不存在,这是运行时错误,应用异常处理
    assert os.path.exists(file_path), "文件不存在"
    # 应该这样:
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"文件不存在:{file_path}")
    
  2. 在断言中执行有副作用的代码

    # 错误!因为生产环境断言可能被禁用,这个`print`和`add_to_db`就不会执行
    assert validate_data(data) and print("验证中...") and add_to_db(data)
    
  3. 日志级别设置不当

    • 在生产环境将级别设为 DEBUG 会产生海量日志,难以查找真正问题。
    • 在开发环境将级别设为 WARNING 会错过大量有用的调试信息。
    • 建议:开发时用 DEBUG,生产时用 INFOWARNING
  4. 忘记初始化日志记录器或配置

    • 直接调用 logging.info() 使用的是根记录器,可能没有按你想要的格式输出。
    • 最佳实践是为每个模块创建自己的记录器:logger = logging.getLogger(__name__)
  5. 在日志中记录敏感信息

    • 永远不要将密码、信用卡号、身份证号等敏感信息记录到日志中。
    • 正确做法:记录操作本身(如"用户登录尝试"),而非具体数据。

6. 小结

  • 断言 (assert) 是开发阶段的"哨兵",用于守护代码的内部逻辑一致性。条件为假意味着程序存在Bug,应立即失败。请勿用于处理外部输入或可预见的运行时错误。
  • 日志记录 (logging) 是贯穿程序生命周期的"记录仪",用于记录运行状态和事件,是监控、调试和审计的核心工具。它分为不同级别,应输出到合适的地方(控制台、文件等)。
  • 两者定位不同,互补使用
    • 断言快速发现"不可能发生"的代码缺陷。
    • 日志跟踪"正在发生"的程序流程和状态。
  • 记住:在最终交付的程序中,通常通过 -O(优化)选项或设置 PYTHONOPTIMIZE 环境变量来禁用所有断言,以提升性能。而日志记录则必须保留。

通过合理运用断言和日志记录,你的调试效率将大幅提升,代码的健壮性和可维护性也会更强。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「文件读写操作(open, read, write)」 以巩固所学知识。