高级调试工具#

如果您到了这里,说明您想要深入研究或使用更高级的工具.对于首次贡献者和大多数日常开发而言,这通常是不必要的.这些工具的使用频率较低,例如在新的 NumPy 版本发布前后,或者在进行了大型或特别复杂的更改时.

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

使用其他工具查找 C 错误#

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

我们不希望大多数贡献者运行这些工具中的任何一个.但是,您可以确保我们能够更轻松地追踪此类问题:

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

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

  • 如果读取/使用数据,则永远不要使用 np.empty . valgrind 会注意到这一点并报告错误.如果您不关心值,则可以改为生成随机值.

这将帮助我们在您的更改发布之前发现任何疏忽,并且意味着您不必担心制造引用计数错误,这可能会令人望而却步.

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 上没有可用的调试 wheel,则需要从源代码构建依赖项,并确保您的依赖项也编译为调试构建.

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

运行 Numpy 测试或交互式终端通常就像:

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

并且已经在 Debugging 中提到过.

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 支持“抑制”来忽略其中一些,并且 Python 确实有一个抑制文件(甚至是一个编译时选项),如果您觉得有必要,它可能会有所帮助.

Valgrind 有助于:

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

  • 检测内存访问违规(在已分配内存之外读取或写入).

  • 查找许多内存泄漏.请注意,对于大多数泄漏,python 调试版本方法(和 pytest-leaks )更加敏感.原因是 valgrind 只能检测内存是否确实丢失. 如果:

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

    对于 dtype 的引用计数不正确,这是一个错误,但是 valgrind 看不到它,因为 np.dtype(np.int64) 总是返回相同的对象. 但是,并非所有 dtype 都是单例,因此这可能会泄漏不同输入的内存. 在极少数情况下,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 的示例命令).