飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • 第1章 Python简介

    • Python是什么
    • 安装与运行
    • 交互式解释器
    • 注释与编码规范
  • 第2章 变量与数据类型

    • 变量与对象
    • 整数 int
    • 浮点数 float
    • 复数 complex
    • 布尔值 bool
    • 字符串 str
    • 空值 None
    • 类型转换
  • 第3章 运算符与表达式

    • 算术运算符
    • 比较运算符
    • 赋值运算符
    • 逻辑运算符
    • 位运算符
    • 身份与成员运算符
    • 海象运算符
    • 运算符优先级
  • 第4章 流程控制

    • if 语句
    • if-else 语句
    • if-elif-else 语句
    • match-case 语句
    • 条件表达式(三元运算符)
    • while 循环
    • for 循环
    • range 函数
    • break 与 continue
    • 循环的 else 子句
    • pass 语句
  • 第5章 数据结构

    • 列表创建与索引
    • 列表方法
    • 列表推导式
    • 元组
    • 序列解包
    • 集合
    • 字典创建与访问
    • 字典方法
    • 字典推导式
    • range 对象
  • 第6章 函数

    • 定义函数
    • 位置参数与关键字参数
    • 默认参数
    • 可变参数
    • 解包实参
    • 函数返回值
    • lambda 表达式
    • 文档字符串与注解
    • 作用域与命名空间
    • global 与 nonlocal
  • 第7章 模块与包

    • 模块导入
    • 模块搜索路径
    • 包与相对导入
    • 标准库概览
  • 第8章 文件与输入输出

    • 文件读写
    • 上下文管理器
    • 字符串格式化
    • JSON 与 CSV
  • 第9章 面向对象

    • 类与对象
    • 方法
    • 实例变量与类变量
    • 私有变量
    • 继承
    • 多重继承
    • 魔术方法
    • 属性装饰器
    • 数据类 dataclass
  • 第10章 异常处理

    • 语法错误与异常
    • try-except
    • 异常链与 raise
    • 清理操作
    • 自定义异常
  • 第11章 迭代器与生成器

    • 迭代器协议
    • 生成器
    • 生成器表达式
    • 迭代工具
  • 第12章 高级特性

    • 装饰器
    • 函数式编程
  • 第13章 工程实践

    • 测试与调试
    • 代码质量
    • 虚拟环境

测试与调试

代码能运行不等于正确。测试验证预期行为,调试定位异常根因,两者结合才能构建可靠的系统。Python 提供了从简单断言到专业框架的多层次测试工具,以及从打印变量到交互式调试器的多种调试手段。

assert 语句

assert expression [, message] 在 expression 为假时抛出 AssertionError。它主要用于开发和测试阶段验证内部不变量,不应替代正式的数据校验或错误处理:

def divide(a, b):
    assert isinstance(a, (int, float)) and isinstance(b, (int, float)), "参数必须是数字"
    assert b != 0, "除数不能为零"
    return a / b

print(divide(10, 2))   # 5.0
# divide(10, "2")      # AssertionError: 参数必须是数字
# divide(10, 0)        # AssertionError: 除数不能为零

关键性质:当 Python 以优化模式运行(python -O 或 python -OO)时,所有 assert 语句会被完全移除,不会执行也不会报错。因此绝不能用 assert 检查用户输入、文件存在性或安全边界:

# ❌ 错误:优化模式下 assert 消失,文件不存在也不会报错
assert os.path.exists("config.json")
config = json.load(open("config.json"))

# ✅ 正确:用常规异常处理
if not os.path.exists("config.json"):
    raise FileNotFoundError("config.json 不存在")

__debug__ 是一个内置常量,在正常情况下为 True,在优化模式下为 False。可以基于它编写仅在调试时执行的代码:

if __debug__:
    print("调试模式:加载详细日志配置")

doctest:文档即测试

doctest 从文档字符串中提取交互式会话示例并执行验证,确保文档与代码同步。它特别适合测试纯函数和工具函数:

def calc_bonus(salary: float, years: int) -> float:
    """
    根据工龄计算年终奖。

    工龄满 5 年发 2 倍月薪,不满发 0.5 倍。

    >>> calc_bonus(10000.0, 6)
    20000.0
    >>> calc_bonus(10000.0, 3)
    5000.0
    >>> calc_bonus(8000.0, 5)
    16000.0
    """
    if years >= 5:
        return salary * 2
    return salary * 0.5

命令行运行 doctest:

python -m doctest -v your_module.py   # -v 显示详细输出

Doctest 支持指令(directives)控制测试行为,写在 >>> 行末尾的 # doctest: 之后:

def get_timestamp():
    """
    返回当前时间戳。

    >>> import time
    >>> t = get_timestamp()
    >>> isinstance(t, float)  # 只要类型对即可
    True
    >>> print(t)  # doctest: +ELLIPSIS
    1717...
    """
    return time.time()

def random_choice(items):
    """
    随机选择一项。

    >>> random_choice([1, 2, 3]) in [1, 2, 3]  # doctest: +SKIP
    True
    """
    import random
    return random.choice(items)

常用指令包括 +ELLIPSIS(允许 ... 匹配任意输出)、+SKIP(跳过该测试)、+NORMALIZE_WHITESPACE(忽略空白差异)。Doctest 的局限在于不适合测试复杂状态、外部依赖和异常场景。

unittest 框架

unittest 是 Python 标准库中的 xUnit 风格测试框架,基于 TestCase 类组织测试。它无需安装,适合中大型项目和 CI/CD 集成:

import unittest
from datetime import datetime

class Employee:
    def __init__(self, name: str, salary: float, join_year: int):
        self.name = name
        self.salary = salary
        self.join_year = join_year

    def years_of_service(self) -> int:
        return datetime.now().year - self.join_year

    def annual_bonus(self) -> float:
        return self.salary * 2 if self.years_of_service() >= 5 else self.salary * 0.5

class TestEmployee(unittest.TestCase):
    def setUp(self):
        """每个测试方法执行前运行,初始化共享资源"""
        self.senior = Employee("翼王", 232000.0, 2015)
        self.junior = Employee("航仔", 18888.0, 2023)

    def tearDown(self):
        """每个测试方法执行后运行,清理资源"""
        pass

    def test_years_of_service(self):
        self.assertGreaterEqual(self.senior.years_of_service(), 5)
        self.assertEqual(self.junior.years_of_service(), 1)

    def test_annual_bonus_senior(self):
        self.assertEqual(self.senior.annual_bonus(), 232000.0 * 2)

    def test_annual_bonus_junior(self):
        self.assertEqual(self.junior.annual_bonus(), 18888.0 * 0.5)

    def test_invalid_salary(self):
        with self.assertRaises(TypeError):
            Employee("测试", "不是数字", 2020)

    @unittest.skip("功能尚未实现")
    def test_promotion(self):
        pass

    @unittest.expectedFailure
    def test_bug_demo(self):
        """已知失败的测试,修复后应移除装饰器"""
        self.assertEqual(1, 2)

if __name__ == "__main__":
    unittest.main()

unittest 提供丰富的断言方法:assertEqual、assertTrue、assertFalse、assertIsNone、assertIn、assertIsInstance、assertRaises(上下文管理器形式可捕获异常实例)。setUp/tearDown 支持测试级别的资源管理,类级别的 setUpClass/tearDownClass(配合 @classmethod)用于昂贵的全局资源(如数据库连接)。

测试发现功能允许批量运行:

python -m unittest discover -s tests -p "test_*.py" -v

pdb 调试器

pdb 是 Python 内置的交互式源码调试器。Python 3.7+ 推荐使用内置函数 breakpoint() 进入调试,它默认调用 pdb.set_trace(),但可通过 PYTHONBREAKPOINT 环境变量切换为其他调试器(如 ipdb、pudb):

def process_payroll(employees):
    total = 0.0
    for emp in employees:
        breakpoint()  # 程序暂停,进入 pdb 交互
        total += emp["salary"]
    return total

staff = [
    {"name": "航仔", "salary": 18888.0},
    {"name": "翼王", "salary": 232000.0},
]
# process_payroll(staff)

进入 pdb 后常用的命令:

命令作用
n (next)执行下一行,不进入函数内部
s (step)进入函数调用内部
c (continue)继续运行到下一个断点或结束
l (list)显示当前位置附近的源码
p <expr>打印表达式值
pp <expr>美化打印(适合字典和列表)
b <line>在指定行设置断点
q (quit)退出调试器,终止程序

事后调试(post-mortem)在程序崩溃后进入调试状态,查看栈帧和变量:

import pdb
import sys

def buggy():
    return 1 / 0

try:
    buggy()
except Exception:
    pdb.post_mortem(sys.exc_info()[2])  # 在异常发生处进入调试

print 调试与日志

在不便使用 pdb 的场景(如生产环境、多线程程序),打印变量是快速定位问题的手段。使用 repr() 或 !r 转换可以暴露字符串的引号和转义字符,避免空白字符造成的误导:

name = "航仔\n"
print(f"name={name}")        # 换行被直接输出,难以察觉
print(f"name={name!r}")      # name='航仔\n' —— 转义可见
print(f"salary={salary:.2f}")  # 格式化数字,保留两位小数

对于长期运行的程序,应使用 logging 模块替代 print。logging 支持分级输出、多目标(文件/控制台/网络)、格式化字符串和运行时级别控制:

import logging

# 基础配置(通常在程序入口执行一次)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger("hr_system")

def update_salary(emp_name: str, new_salary: float):
    logger.info("开始更新 %s 的工资", emp_name)
    if new_salary < 2300:
        logger.warning("%s 的工资 %.2f 低于最低标准", emp_name, new_salary)
    try:
        # ... 数据库操作 ...
        logger.info("%s 工资更新成功", emp_name)
    except Exception:
        logger.exception("%s 工资更新失败", emp_name)  # 自动记录异常栈

日志级别从低到高为 DEBUG < INFO < WARNING < ERROR < CRITICAL。设置 level=logging.INFO 后,DEBUG 级别的消息不会输出。logger.exception() 在 ERROR 级别记录消息的同时自动附加异常 traceback,是捕获异常时的首选方法。

生产环境建议:不要依赖 basicConfig 的默认设置,应显式配置 FileHandler(按大小或日期轮转)和 Formatter,并通过配置文件或环境变量控制日志级别,避免重启服务修改代码。

下一页
代码质量