在通过python或ipython终端运行.py文件时抑制matplotlib图形
我正在写一个 test_examples.py
文件,用来测试一个包含多个 Python 示例的文件夹。目前我使用 glob
来解析这个文件夹,然后用 subprocess
来执行每个 Python 文件。问题是,有些文件是绘图的,它们会打开一个 Figure
窗口,这个窗口会一直停在那里,直到我把它关掉。
很多关于这个问题的提问都给出了在文件内部的解决方案,但我想知道,如何在不修改文件的情况下,外部运行这些文件时抑制输出呢?
到目前为止,我做了以下工作:
import subprocess as sb
import glob
from nose import with_setup
def test_execute():
files = glob.glob("../*.py")
files.sort()
for fl in files:
try:
sb.call(["ipython", "--matplotlib=Qt4", fl])
except:
assert False, "File: %s ran with some errors\n" % (fl)
这样做有点效果,可以抑制图形的显示,但它不会抛出任何异常(即使程序出错)。我也不太确定它到底在做什么。是把所有的图形都加到 Qt4 里,还是说当那个脚本执行完后,图形就会从内存中移除呢?
理想情况下,我希望能运行每个 .py
文件,并捕获它的 stdout
和 stderr
,然后根据退出状态来报告 stderr
并标记测试失败。这样当我运行 nosetests
时,它就会执行示例文件夹中的程序,并检查它们是否都能正常运行。
2 个回答
虽然我来得有点晚,但我也在尝试解决类似的问题,这里是我目前的想法。基本上,如果你的图表是通过调用比如说 matplotlib.pyplot.show
来显示的,你可以用一个叫做 mock 的方法来替代这个功能,使用一个 patch 装饰器。大概是这样的:
from unittest.mock import patch
@patch('matplotlib.pyplot.show') # passes a mock object to the decorated function
def test_execute(mock_show):
assert mock_show() == None # shouldn't do anything
files = glob.glob("../*.py")
files.sort()
for fl in files:
try:
sb.call(["ipython", fl])
except:
assert False, "File: %s ran with some errors\n" % (fl)
简单来说,这个 patch 装饰器应该会把被装饰的函数中所有对 matplotlib.pyplot.show
的调用替换成一个不做任何事情的假对象。理论上应该是这样工作的。不过在我的应用中,我的终端还是试图打开图表,这导致了错误。希望对你来说能更顺利一些,如果我发现上面有什么问题导致我的情况不对,我会更新的。
编辑:为了完整起见,你可能是通过调用 matplotlib.pyplot.figure()
或 matplotlib.pyplot.subplots()
来生成图形,这种情况下你应该替代的就是这些,而不是 matplotlib.pyplot.show()
。用法和上面一样,你只需要用:
@patch('matplotlib.pyplot.figure')
或者:
@patch('matplotlib.pyplot.subplots')
你可以通过在每个源文件的顶部插入以下几行代码,强制 matplotlib 使用 Agg
后端(这样就不会打开任何窗口):
import matplotlib
matplotlib.use('Agg')
这里有一个一行的命令,可以动态地在 my_script.py
的顶部插入这些行(而不修改磁盘上的文件),然后将输出传递给 Python 解释器执行:
~$ sed "1i import matplotlib\nmatplotlib.use('Agg')\n" my_script.py | python
你也可以使用 subprocess
来实现类似的调用,像这样:
p1 = sb.Popen(["sed", "1i import matplotlib\nmatplotlib.use('Agg')\n", fl],
stdout=sb.PIPE)
exit_cond = sb.call(["python"], stdin=p1.stdout)
你可以通过将 stdout=
和 stderr=
参数传递给 sb.call()
来捕获脚本的 stderr
和 stdout
。当然,这只在有 sed
工具的 Unix 环境中有效。
更新
这个问题其实挺有意思的。我想了想,觉得有一个更优雅的解决方案(虽然还是有点小技巧):
#!/usr/bin/python
import sys
import os
import glob
from contextlib import contextmanager
import traceback
set_backend = "import matplotlib\nmatplotlib.use('Agg')\n"
@contextmanager
def redirected_output(new_stdout=None, new_stderr=None):
save_stdout = sys.stdout
save_stderr = sys.stderr
if new_stdout is not None:
sys.stdout = new_stdout
if new_stderr is not None:
sys.stderr = new_stderr
try:
yield None
finally:
sys.stdout = save_stdout
sys.stderr = save_stderr
def run_exectests(test_dir, log_path='exectests.log'):
test_files = glob.glob(os.path.join(test_dir, '*.py'))
test_files.sort()
passed = []
failed = []
with open(log_path, 'w') as f:
with redirected_output(new_stdout=f, new_stderr=f):
for fname in test_files:
print(">> Executing '%s'" % fname)
try:
code = compile(set_backend + open(fname, 'r').read(),
fname, 'exec')
exec(code, {'__name__':'__main__'}, {})
passed.append(fname)
except:
traceback.print_exc()
failed.append(fname)
pass
print ">> Passed %i/%i tests: " %(len(passed), len(test_files))
print "Passed: " + ', '.join(passed)
print "Failed: " + ', '.join(failed)
print "See %s for details" % log_path
return passed, failed
if __name__ == '__main__':
run_exectests(*sys.argv[1:])
从概念上讲,这个方法和我之前的解决方案非常相似——它通过将测试脚本作为字符串读取,并在前面加上几行代码来导入 matplotlib 并设置为非交互式后端。然后将这个字符串编译成 Python 字节码并执行。主要的优点是,这个方法应该是平台无关的,因为不需要 sed
。
如果你像我一样,习惯这样写脚本,那么 {'__name__':'__main__'}
这个小技巧和全局变量是必要的:
def run_me():
...
if __name__ == '__main__':
run_me()
需要考虑几点:
- 如果你在一个已经导入了 matplotlib 并设置了交互式后端的 ipython 会话中运行这个函数,
set_backend
的技巧就不管用了,你仍然会看到图形弹出来。最简单的方法是直接从命令行运行它(~$ python exectests.py testdir/ logfile.log
),或者在一个没有为 matplotlib 设置交互式后端的(i)python 会话中运行。如果你在 ipython 会话中从不同的子进程运行它,也应该可以工作。 - 我使用了来自 这个回答 的
contextmanager
技巧,将stdin
和stdout
重定向到日志文件。请注意,这个方法不是线程安全的,但我觉得脚本打开子进程的情况比较少见。