高级调试工具#

如果您来到这里,说明您想深入了解或使用更高级的工具。对于初次贡献者和大多数日常开发来说,这通常不是必需的。这些工具使用频率较低,例如在新版 NumPy 发布前夕,或者在进行了大规模或特别复杂的更改时。

由于并非所有这些工具都经常使用,并且只在某些系统上可用,因此请预料到可能存在差异、问题或怪癖;如果您遇到困难,我们将很乐意提供帮助,并感谢您对这些工作流程的任何改进或建议。

使用额外工具查找 C 错误#

大多数开发不需要超出 调试 中所示的典型调试工具链。但例如内存泄漏可能特别微妙或难以缩小范围。

我们不期望大多数贡献者运行这些工具中的任何一个。但是,您可以确保我们更容易地追踪这些问题

  • 测试应覆盖所有代码路径,包括错误路径。

  • 尽量编写简短简单的测试。如果您的测试非常复杂,请考虑额外创建一个更简单的测试。这会很有帮助,因为通常很容易找到是哪个测试触发了问题,而不是测试的哪一行。

  • 如果数据被读取/使用,切勿使用 np.emptyvalgrind 会注意到这一点并报告错误。当您不关心值时,可以生成随机值代替。

这将帮助我们在您的更改发布之前发现任何疏忽,这意味着您不必担心出现引用计数错误,这可能会令人望而生畏。

Python 调试构建#

Python 调试构建版本很容易获得,例如通过 Linux 系统上的包管理器,但它们也在其他平台上可用,可能以不太方便的格式。如果您无法通过系统包管理器轻松安装 Python 的调试构建版本,您可以使用 pyenv 自己构建一个。例如,要安装并全局激活 Python 3.13.3 的调试构建版本,可以这样做

pyenv install -g 3.13.3
pyenv global 3.13.3

请注意,pyenv install 从源代码构建 Python,因此在构建之前必须确保已安装 Python 的依赖项,请参阅 pyenv 文档以获取平台特定的安装说明。您可以使用 pip 安装调试会话可能需要的 Python 依赖项。如果 pypi 上没有调试轮子 (debug wheel) 可用,您将需要从源代码构建依赖项,并确保您的依赖项也编译为调试版本。

通常,Python 的调试构建版本将 Python 可执行文件命名为 pythond 而不是 python。要检查您是否安装了 Python 的调试构建版本,您可以运行例如 pythond -m sysconfig 来获取 Python 可执行文件的构建配置。调试构建版本将在 CFLAGS 中使用调试编译器选项(例如 -g -Og)进行构建。

运行 Numpy 测试或交互式终端通常很简单,只需

python3.8d runtests.py
# or
python3.8d runtests.py --ipython

并且已在 调试 中提及。

Python 调试构建将有所帮助

  • 查找可能导致随机行为的错误。一个例子是对象被删除后仍在使用的情况。

  • Python 调试构建允许检查正确的引用计数。这通过使用额外命令来实现

    sys.gettotalrefcount()
    sys.getallocatedblocks()
    
  • Python 调试构建允许使用 gdb 和其他 C 调试器进行更轻松的调试。

pytest 结合使用#

仅使用 Python 调试构建来运行测试套件本身不会发现很多错误。Python 调试构建的另一个优点是它允许检测内存泄漏。

一个简化此过程的工具是 pytest-leaks,可以使用 pip 进行安装。不幸的是,pytest 本身可能会泄漏内存,但通常(目前)可以通过移除以下内容来获得良好的结果

@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
    doctest_namespace['np'] = numpy

@pytest.fixture(autouse=True)
def env_setup(monkeypatch):
    monkeypatch.setenv('PYTHONHASHSEED', '0')

来自 numpy/conftest.py(这可能会随着新的 pytest-leaks 版本或 pytest 更新而改变)。

这允许方便地运行测试套件或其中一部分

python3.8d runtests.py -t numpy/_core/tests/test_multiarray.py -- -R2:3 -s

其中 -R2:3pytest-leaks 命令(请参阅其文档),-s 会导致输出打印并可能需要(在某些版本中,捕获的输出被检测为内存泄漏)。

请注意,有些测试已知(甚至设计成)会泄漏引用,我们尝试标记它们,但请预料到会有一些误报。

valgrind#

Valgrind 是一个强大的工具,用于查找某些内存访问问题,应在复杂的 C 代码上运行。 valgrind 的基本使用通常只需要

PYTHONMALLOC=malloc valgrind python runtests.py

其中 PYTHONMALLOC=malloc 是必要的,以避免来自 Python 本身的误报。根据系统和 valgrind 版本,您可能会看到更多误报。 valgrind 支持“抑制 (suppressions)”以忽略其中一些,Python 确实有一个抑制文件(甚至一个编译时选项),如果您认为有必要,这可能会有所帮助。

Valgrind 有助于

  • 查找未初始化变量/内存的使用。

  • 检测内存访问违规(读写分配内存之外的区域)。

  • 查找许多内存泄漏。请注意,对于大多数内存泄漏,Python 调试构建方法(和 pytest-leaks)要敏感得多。原因是 valgrind 只能检测内存是否确定丢失。如果

    dtype = np.dtype(np.int64)
    arr.astype(dtype=dtype)
    

    dtype 的引用计数不正确,这是一个错误,但 valgrind 无法看到它,因为 np.dtype(np.int64) 总是返回相同的对象。然而,并非所有 dtype 都是单例(singleton),因此这可能会导致不同输入的内存泄漏。在极少数情况下,NumPy 使用 malloc 而不是 Python 内存分配器,后者对 Python 调试构建是不可见的。malloc 通常应避免使用,但也有一些例外(例如 PyArray_Dims 结构是公共 API,不能使用 Python 分配器)。

尽管使用 valgrind 进行内存泄漏检测速度较慢且不那么敏感,但它很方便:您可以在不修改大多数程序的情况下运行 valgrind。

注意事项

  • Valgrind 不支持 numpy 的 longdouble,这意味着测试会失败或被标记为错误,而实际上它们完全正常。

  • 预计在运行 NumPy 代码之前和之后会出现一些错误。

  • 缓存可能意味着错误(特别是内存泄漏)可能不会被检测到,或者只在稍后不相关的时间才被检测到。

valgrind 的一个巨大优势是,除了 valgrind 本身之外,它没有其他要求(尽管您可能希望使用调试构建以获得更好的回溯信息)。

pytest 结合使用#

您可以使用 valgrind 运行测试套件,这可能在您只对少数测试感兴趣时足够了

PYTHONMALLOC=malloc valgrind python runtests.py \
 -t numpy/_core/tests/test_multiarray.py -- --continue-on-collection-errors

请注意 --continue-on-collection-errors,由于缺少 longdouble 支持导致失败,目前这是必需的(如果您不运行完整的测试套件,通常不需要此选项)。

如果您希望检测内存泄漏,您还需要 --show-leak-kinds=definite 以及可能的更多 valgrind 选项。就像 pytest-leaks 一样,某些测试已知会导致 valgrind 中出现泄漏错误,并且可能已标记或未标记。

我们开发了 pytest-valgrind,它可以

  • 单独报告每个测试的错误

  • 将内存泄漏缩小到单个测试(默认情况下,valgrind 只在程序停止后检查内存泄漏,这非常繁琐)。

请参阅其 README 获取更多信息(其中包含 NumPy 的示例命令)。