
本文旨在解决Sphinx doctest 在处理包含Matplotlib绘图示例的文档字符串时,因 plt.show() 导致测试中断的问题。核心策略是通过重构绘图函数,使其接受可选的 Axes 对象并移除 plt.show() 的直接调用,从而将绘图逻辑与图形显示解耦。这种方法不仅提升了函数的复用性,也确保了 doctest 能够顺畅运行,实现自动化测试与文档生成。
理解问题:Sphinx Doctest与Matplotlib的交互式困境
在Python项目中,Sphinx是一个广泛使用的文档生成工具,其内置的 doctest 扩展能够从文档字符串中的示例代码直接运行测试,确保代码示例的准确性。然而,当这些文档字符串包含Matplotlib绘图代码,并且在函数内部调用了 plt.show() 时,doctest 可能会遇到一个常见的阻塞问题。
plt.show() 函数的作用是显示当前活动的Matplotlib图形窗口。在交互式环境中,这通常是期望的行为。但在自动化测试(如 doctest)或非交互式脚本中,plt.show() 会暂停程序执行,直到用户手动关闭图形窗口。这对于自动化流程来说是不可接受的,因为它需要人工干预,严重阻碍了测试的连续性。
考虑以下一个典型的Matplotlib绘图函数及其文档字符串:
import matplotlib.pyplot as plt
def plot_numbers_problematic(x):
"""
显示一组数字的折线图。
参数
----------
x : list
要绘制的数字列表。
示例
-------
>>> import your_module # 假设此函数在your_module中
>>> x_data = [1, 2, 5, 6, 8.1, 7, 10.5, 12]
>>> your_module.plot_numbers_problematic(x_data)
"""
_, ax = plt.subplots()
ax.plot(x, marker="o", mfc="red", mec="red")
ax.set_xlabel("X轴标签")
ax.set_ylabel("Y轴标签")
ax.set_title("图表标题")
plt.show() # 此处会导致doctest阻塞当 sphinx-build -b doctest . _build 命令执行到 your_module.plot_numbers_problematic(x_data) 这一行时,plt.show() 会弹出一个图形窗口,并暂停 doctest 的执行,直到该窗口被手动关闭。
核心策略:解耦绘图与显示
解决此问题的关键在于将Matplotlib函数的绘图逻辑与图形显示行为进行解耦。一个设计良好的Matplotlib绘图函数不应该自行决定何时显示图形,而应该将这一控制权交给调用者。这样,函数可以更灵活地应用于各种场景,包括:
- 交互式显示: 调用者在函数执行后手动调用 plt.show()。
- 非交互式保存: 调用者在函数执行后将图形保存到文件,而不显示。
- 子图组合: 调用者在同一个 Figure 上创建多个 Axes,并将这些 Axes 传递给不同的绘图函数。
- 自动化测试: 测试框架可以调用绘图函数而不显示图形,只检查返回的 Axes 对象或其内容。
实现这一解耦的常用方法是让绘图函数接受一个可选的 Axes 对象作为参数。如果提供了 Axes 对象,函数就在其上绘图;如果没有提供,函数则自行创建一个新的 Figure 和 Axes。最重要的是,从函数内部移除 plt.show() 的调用。
优化Matplotlib绘图函数
以下是优化后的 plot_numbers 函数示例:
import matplotlib.pyplot as plt
def plot_numbers(x, *, ax=None):
"""
显示一组数字的折线图。
参数
----------
x : list
要绘制的数字列表。
ax : matplotlib.axes.Axes, 可选
用于绘制数字的Matplotlib Axes对象。如果未提供,将创建一个新的Figure和Axes。
返回
-------
matplotlib.axes.Axes
绘制了数据的Axes对象。
示例
-------
>>> import your_module # 假设此函数在your_module中
>>> x_data = [1, 2, 5, 6, 8.1, 7, 10.5, 12]
>>> # 在doctest中,这不会打开窗口。
>>> # 如果需要在交互式环境中显示,请在调用函数后手动调用 plt.show()。
>>> ax_result = your_module.plot_numbers(x_data)
>>> # plt.show() # 在需要显示图形时取消注释
>>> # 验证 Axes 对象是否已创建
>>> assert isinstance(ax_result, plt.Axes)
>>> # 进一步的doctest可以检查ax_result的属性,例如标题、标签等
>>> ax_result.get_title()
'图表标题'
"""
if ax is None:
# 如果没有提供Axes,则创建一个新的Figure和Axes
_, ax = plt.subplots()
# 在提供的或新创建的Axes上进行绘图
ax.plot(x, marker="o", mfc="red", mec="red")
ax.set_xlabel("X轴标签")
ax.set_ylabel("Y轴标签")
ax.set_title("图表标题")
return ax # 返回Axes对象,以便调用者进行进一步操作关键改动点说明:
- ax=None 参数: 函数现在接受一个可选的 ax 参数,类型为 matplotlib.axes.Axes。* 用作分隔符,表示 ax 是一个仅限关键字参数,提高了API的清晰度。
- 条件性 plt.subplots(): 如果 ax 为 None,函数会调用 plt.subplots() 创建一个新的 Figure 和 Axes。这意味着函数既可以在现有 Axes 上绘图,也可以作为独立的绘图函数使用。
- 移除 plt.show(): 最重要的改变是移除了 plt.show() 的调用。现在,函数只负责在 Axes 上绘制内容。
- 返回 ax: 函数现在返回绘制数据的 Axes 对象。这使得调用者能够访问和操作 Axes,例如保存图形、修改属性或在交互式环境中显示。
- 更新文档字符串示例: doctest 示例也相应更新,以接收返回的 ax 对象,并注释掉 plt.show(),明确告知用户在 doctest 环境下不会显示图形。
Doctest中的应用与优势
通过上述优化,当 doctest 运行时,它会执行 your_module.plot_numbers(x_data),函数会正常执行绘图逻辑并返回一个 Axes 对象,但不会弹出图形窗口,从而避免了阻塞。
这种方法带来了多方面的好处:
- 无缝自动化测试: doctest 和其他测试框架(如 pytest)可以无障碍地运行包含Matplotlib示例的测试。
- 更高的函数复用性: 绘图函数变得更加通用,可以轻松地集成到更复杂的图形布局(例如,使用 plt.figure().add_subplot() 或 plt.subplots() 创建的多个子图)或自定义的应用程序中。
- 清晰的职责分离: 函数专注于“如何绘图”,而“何时显示”的职责则交给了调用者,这符合软件设计的单一职责原则。
- 更好的控制: 调用者可以根据需要选择显示图形、将其保存到文件(fig.savefig(...))或在更高级的GUI应用程序中嵌入它。
注意事项与最佳实践
-
谁来调用 plt.show()? 在优化后的设计中,plt.show() 应该由最终的用户代码或顶层脚本来调用,而不是由库函数内部调用。例如:
import matplotlib.pyplot as plt # 假设 plot_numbers 是优化后的函数 import your_module if __name__ == "__main__": x_data = [1, 3, 2, 4, 5] # 在新的Figure和Axes上绘图并显示 ax1 = your_module.plot_numbers(x_data) ax1.set_title("第一个图") # 在同一个Figure上创建另一个Axes,并用另一个函数绘图 fig, (ax2, ax3) = plt.subplots(1, 2) your_module.plot_numbers([5, 4, 3, 2, 1], ax=ax2) ax2.set_title("第二个图") your_module.plot_numbers([10, 8, 6, 4, 2], ax=ax3) ax3.set_title("第三个图") plt.tight_layout() # 调整子图布局 plt.show() # 在这里统一显示所有图形 文档清晰度: 在函数的文档字符串中明确说明 ax 参数的作用,以及函数不会自行调用 plt.show()。
参考官方指南: Matplotlib官方文档也推荐了这种编写辅助函数的方式,例如在其“Making a helper functions”部分(https://www.php.cn/link/dda4087216e15d1784efc310005dd683)中有所提及。虽然该示例可能要求 ax 参数是强制的,但核心思想是相同的:将 Axes 对象作为参数传递。
总结
通过将Matplotlib绘图函数的职责限制在纯粹的绘图操作上,并移除内部的 plt.show() 调用,我们可以有效地解决Sphinx doctest 在遇到交互式图形窗口时的阻塞问题。这种设计模式不仅使测试自动化成为可能,还显著提高了绘图函数的灵活性、可复用性和API的清晰度,是编写高质量Matplotlib库代码的重要实践。开发者应养成习惯,让绘图函数返回 Axes 对象,并将图形的显示或保存逻辑留给调用者处理。










