第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。 - 使用断言确保
a和b都是数字(int 或 float)。 - 调用函数并测试正常情况和会触发断言的情况。
- 预期输出:正常情况返回除法结果;触发断言时打印错误信息。
练习2:配置日志记录器
要求:
- 创建一个日志记录器,名字为
MyApp。 - 将日志级别设置为
DEBUG。 - 添加两个处理器:
StreamHandler输出到控制台,格式包含时间、级别和消息。FileHandler输出到文件my_app.log,格式包含时间、记录器名称、级别和消息。
- 在你的代码中至少使用
DEBUG、INFO、WARNING各一条日志消息进行测试。
练习3:综合应用 - 密码验证器
要求:编写一个 password_validator(password) 函数,它应该:
- 使用日志记录验证过程的每一步(如"开始检查长度"、"检查是否包含大写字母")。
- 使用断言确保传入的
password参数是字符串类型(这是一个不应发生的逻辑错误)。 - 返回一个布尔值(
True/False)和一条消息。 - 验证规则(至少实现3条,例如:最小长度、必须包含数字、必须包含特殊字符)。
- 编写测试代码,验证不同密码,观察日志输出。
5. 常见错误
-
误用断言进行数据验证:
# 错误!文件可能不存在,这是运行时错误,应用异常处理 assert os.path.exists(file_path), "文件不存在" # 应该这样: if not os.path.exists(file_path): raise FileNotFoundError(f"文件不存在:{file_path}") -
在断言中执行有副作用的代码:
# 错误!因为生产环境断言可能被禁用,这个`print`和`add_to_db`就不会执行 assert validate_data(data) and print("验证中...") and add_to_db(data) -
日志级别设置不当:
- 在生产环境将级别设为
DEBUG会产生海量日志,难以查找真正问题。 - 在开发环境将级别设为
WARNING会错过大量有用的调试信息。 - 建议:开发时用
DEBUG,生产时用INFO或WARNING。
- 在生产环境将级别设为
-
忘记初始化日志记录器或配置:
- 直接调用
logging.info()使用的是根记录器,可能没有按你想要的格式输出。 - 最佳实践是为每个模块创建自己的记录器:
logger = logging.getLogger(__name__)。
- 直接调用
-
在日志中记录敏感信息:
- 永远不要将密码、信用卡号、身份证号等敏感信息记录到日志中。
- 正确做法:记录操作本身(如"用户登录尝试"),而非具体数据。
6. 小结
- 断言 (
assert) 是开发阶段的"哨兵",用于守护代码的内部逻辑一致性。条件为假意味着程序存在Bug,应立即失败。请勿用于处理外部输入或可预见的运行时错误。 - 日志记录 (
logging) 是贯穿程序生命周期的"记录仪",用于记录运行状态和事件,是监控、调试和审计的核心工具。它分为不同级别,应输出到合适的地方(控制台、文件等)。 - 两者定位不同,互补使用:
- 用断言快速发现"不可能发生"的代码缺陷。
- 用日志跟踪"正在发生"的程序流程和状态。
- 记住:在最终交付的程序中,通常通过
-O(优化)选项或设置PYTHONOPTIMIZE环境变量来禁用所有断言,以提升性能。而日志记录则必须保留。
通过合理运用断言和日志记录,你的调试效率将大幅提升,代码的健壮性和可维护性也会更强。
练习编辑器
python
Loading...