Python - Pyramid 和 matplotlib - 不能有多个视图输出一个 SVG?

Python - Pyramid and matplotlib - Cannot Have More Than One View Output A SVG?

我正在开发一个 Python Pyramid 应用程序,我打算在其中创建多个 SVG 图像以使用饼图绘制统计数据。在我的测试中,我发现一个 SVG 视图工作正常,只要我添加第二个 SVG 输出视图,并且加载第二个 SVG 图像(SVG 图像加载的顺序无关紧要),无论是直接通过它的视图,还是通过引用此视图的另一个视图,SVG 图像 "are combined" 在任何其他进一步调用中加载 SVG 文件。这似乎是 Python 堆栈中某处的错误,因为它似乎没有正确清除内存(主要是在多个 SVG 文件的情况下,请参阅下面的详细信息)。还要注意下面,在足够的 image/page 加载后遇到 TclError。

因为我在一个有更多视图的更详细的应用程序中使用 SVG,所以我在 minimized/reduced 应用程序中重现它以表明它不是我正在做的额外的事情并且这个代码是正确生成的来自 Pyramid alchemy 模板和数据库调用不涉及。该数据库在我的更多详细信息应用程序中得到积极利用。此应用程序只有 3 个视图,其中第一个视图是原始模板的一部分。我还添加了 DEBUG 日志记录,以明确没有迹象表明存在对其他 SVG 视图的任何内部调用。

部分视图代码基于Matplotlib svg as string and not a file,主要供StringIO使用。请注意,由于需要饼图,这是我的代码与参考问题中的代码不同的主要原因。我发现无论我使用 StringIO 还是 cStringIO,问题本质上都是一样的。在我的代码中,我使用 cStringIO.

完整的应用程序代码位于:https://github.com/danielpronych/pyramidapp

PyPlot 文档:: pyplot api

更新:来自第一个 SVG 视图的代码(图形句柄和关闭命令):

@view_config(route_name='view_test_svg')
def test_svg_view(request):
    # Full module import is not allowed by Pyramid
    #from pylab import *
    # Do individual required imports instead
    from pylab import close, figure, axes, pie, title, savefig
    # For clarity, note that the above, and below, function the same
    #from matplotlib.pyplot import close, figure, axes, pie, title, savefig
    log.debug('In test_svg_view')
    fig = figure(1, figsize=(6,6))
    ax = axes([0.1, 0.1, 0.8, 0.8])
    labels = ['Frogs', 'Hogs', 'Dogs', 'Logs']
    fracs = [15, 30, 45, 10]
    explode=(0, 0.05, 0, 0)
    pie(fracs, explode=explode, labels=labels,
                                autopct='%1.1f%%', shadow=True, startangle=90)
    title('Raining Hogs and Dogs', bbox={'facecolor':'0.8', 'pad':5})
    imgdata = cStringIO.StringIO()
    fig.savefig(imgdata, format='svg')
    imgdata.seek(0)
    svg_dta = imgdata.getvalue()
    # Close the StringIO buffer
    imgdata.close()
    close('all')
    return Response(svg_dta, content_type='image/svg+xml')

此代码的步骤:

  1. 加载http://localhost:6543/test.svg
  2. 加载http://localhost:6543/test2.svg
  3. 再次加载 (1) 或 (2)。 pserve 将连续 "spin" 并且命令 window 没有任何迹象。

注意:使用此代码加载相同的 SVG 视图 3 次也具有与上述步骤相同的结果,不会发生使用下面的代码。

另请注意 pyplot pie 确实有一个 return;然而,不是 "handle" 本身。

来自第一个 SVG 视图的代码(原创,以下步骤来自此代码):

@view_config(route_name='view_test_svg')
def test_svg_view(request):
    # Full module import is not allowed by Pyramid
    #from pylab import *
    # Do individual required imports instead
    from pylab import figure, axes, pie, title, savefig
    # For clarity, note that the above, and below, function the same
    #from matplotlib.pyplot import figure, axes, pie, title, savefig
    log.debug('In test_svg_view')
    figure(1, figsize=(6,6))
    ax = axes([0.1, 0.1, 0.8, 0.8])
    labels = ['Frogs', 'Hogs', 'Dogs', 'Logs']
    fracs = [15, 30, 45, 10]
    explode=(0, 0.05, 0, 0)
    pie(fracs, explode=explode, labels=labels,
                                autopct='%1.1f%%', shadow=True, startangle=90)
    title('Raining Hogs and Dogs', bbox={'facecolor':'0.8', 'pad':5})
    imgdata = cStringIO.StringIO()
    savefig(imgdata, format='svg')
    imgdata.seek(0)
    svg_dta = imgdata.getvalue()
    # Close the StringIO buffer
    imgdata.close()
    return Response(svg_dta, content_type='image/svg+xml')

Python 版本: Python 2.7.5

Python 包配置(仅限主包)

重现步骤:

  1. 保护金字塔应用程序。

命令:pserve development.ini --reload

Starting server in PID 4912.
serving on http://0.0.0.0:6543
  1. 加载http://localhost:6543/test.svg

注意这可以正常工作

DEBUG [pyramidapp.views:22][Dummy-2] In test_svg_view

  1. 加载http://localhost:6543/test2.svg

请注意 "combines" 两个 SVG 文件放在一起

DEBUG [pyramidapp.views:45][Dummy-3] In test2_svg_view

  1. 加载http://localhost:6543/test.svg

请注意,这与 test2.svg 完全一样,标题正确,因为它们的长度也相似,现在图像也在此视图中合并

DEBUG [pyramidapp.views:22][Dummy-4] In test_svg_view

  1. 重新托管应用程序并仅加载 http://localhost:6543/test2.svg

请注意,这对于第一次加载工作正常,因为此视图是在 test.svg 之前加载的,这次

DEBUG [pyramidapp.views:45][Dummy-2] In test2_svg_view

使用 Control+C 终止 pserve 进程时的 Tracelog

Error in sys.exitfunc:
Traceback (most recent call last):
  File "--python_path--\lib\atexit.py", line 24, in _run_exitfuncs
    func(*targs, **kargs)
  File "--python_path--\lib\site-packages\matplotlib-1.4.3-py2.7-win32.egg\ma
tplotlib\_pylab_helpers.py", line 89, in destroy_all
    manager.destroy()
  File "--python_path--\lib\site-packages\matplotlib-1.4.3-py2.7-win32.egg\ma
tplotlib\backends\backend_tkagg.py", line 588, in destroy
    self.window.destroy()
  File "--python_path--\lib\lib-tk\Tkinter.py", line 1789, in destroy
    for c in self.children.values(): c.destroy()
  File "--python_path--\lib\lib-tk\Tkinter.py", line 2042, in destroy
    self.tk.call('destroy', self._w)
_tkinter.TclError: out of stack space (infinite loop?)
^C caught in monitor process

重要提示:加载足够的 SVG 图片后会遇到以下情况:

目前修复的唯一方法是重新启动pserve。另请注意,只要 SVG 图像未被此类视图引用或使用,视图(例如 my_view 就会正确加载。

另一个重要说明,只要在 pserve 的整个时间内只加载一个 SVG 文件,即 http://localhost:6543/test.svg,图像似乎可以 reloaded/refreshed(可能)无限次没有任何明显问题,或遇到以下情况:

_tkinter.TclError
TclError: out of stack space (infinite loop?)
Traceback (most recent call last)
File "--python_path--\lib\site-packages\pyramid_debugtoolbar-2.0.2-py2.7.egg\pyramid_debugtoolbar\panels

\performance.py", line 69, in noresource_timer_handler
Display the sourcecode for this frameOpen an interactive python shell in this frameresult = handler(request)
File "--python_path--\lib\site-packages\pyramid-1.6a1-py2.7.egg\pyramid\tweens.py", line 20, in excview_tween
Display the sourcecode for this frameOpen an interactive python shell in this frameresponse = handler(request)
File "--python_path--\lib\site-packages\pyramid_tm-0.11-py2.7.egg\pyramid_tm\__init__.py", line 94, in tm_tween
Display the sourcecode for this frameOpen an interactive python shell in this framereraise(*exc_info)
File "--python_path--\lib\site-packages\pyramid_tm-0.11-py2.7.egg\pyramid_tm\__init__.py", line 75, in tm_tween
Display the sourcecode for this frameOpen an interactive python shell in this frameresponse = handler(request)
File "--python_path--\lib\site-packages\pyramid-1.6a1-py2.7.egg\pyramid\router.py", line 145, in handle_request
Display the sourcecode for this frameOpen an interactive python shell in this frameview_name
File "--python_path--\lib\site-packages\pyramid-1.6a1-py2.7.egg\pyramid\view.py", line 527, in _call_view
Display the sourcecode for this frameOpen an interactive python shell in this frameresponse = view_callable

(context, request)
File "--python_path--\lib\site-packages\pyramid-1.6a1-py2.7.egg\pyramid\config\views.py", line 384, in 

viewresult_to_response
Display the sourcecode for this frameOpen an interactive python shell in this frameresult = view(context, 

request)
File "--python_path--\lib\site-packages\pyramid-1.6a1-py2.7.egg\pyramid\config\views.py", line 506, in 

_requestonly_view
Display the sourcecode for this frameOpen an interactive python shell in this frameresponse = view(request)
File "c:\projects\python\pyramid\pyramidapp\pyramidapp\views.py", line 55, in test2_svg_view
Display the sourcecode for this frameOpen an interactive python shell in this framesavefig(imgdata, 

format='svg')
File "--python_path--\lib\site-packages\matplotlib-1.4.3-py2.7-win32.egg\matplotlib\pyplot.py", line 578, in 

savefig
Display the sourcecode for this frameOpen an interactive python shell in this framedraw()   # need this if 

'transparent=True' to reset colors
File "--python_path--\lib\site-packages\matplotlib-1.4.3-py2.7-win32.egg\matplotlib\pyplot.py", line 571, in 

draw
Display the sourcecode for this frameOpen an interactive python shell in this frameget_current_fig_manager

().canvas.draw()
File "--python_path--\lib\site-packages\matplotlib-1.4.3-py2.7-win32.egg\matplotlib\backends\backend_tkagg.py", 

line 350, in draw
Display the sourcecode for this frameOpen an interactive python shell in this frametkagg.blit(self._tkphoto, 

self.renderer._renderer, colormode=2)
File "--python_path--\lib\site-packages\matplotlib-1.4.3-py2.7-win32.egg\matplotlib\backends\tkagg.py", line 

24, in blit
Display the sourcecode for this frameOpen an interactive python shell in this frametk.call("PyAggImagePhoto", 

photoimage, id(aggimage), colormode, id(bbox_array))
TclError: out of stack space (infinite loop?)

更新(2015 年 9 月): 我希望看到 Sergey 的进一步更新,因为他建议的解决方案一开始似乎确实有帮助;但是,他的解决方案并没有解决问题,因为即使我已经等待了相当长的时间 none 已经发生,我发现只有我的解决方案才真正解决了这个问题。最后,需要说明的是,我的解决方案适用于 web-based 实施和批处理,因为这个问题也出现在批处理中。

我不熟悉 pylab,但看起来模块有一些全局状态,它在调用之间累积(注意你如何调用 savefig() 而没有将任何类型的句柄传递给你创建的图表通过之前的调用 - 但不知何故它应该知道绘制了什么。所以,我猜后续调用只是累积在同一个 "buffer" 中,这解释了你观察到的行为。

这是文档的摘录(重点是我的):

Therefore, everything in matplotlib is organized in a hierarchy. At the top of the hierarchy is the matplotlib “state-machine environment” which is provided by the matplotlib.pyplot module. At this level, simple functions are used to add plot elements (lines, images, text, etc.) to the current axes in the current figure.

Note Pyplot’s state-machine environment behaves similarly to MATLAB and should be most familiar to users with MATLAB experience. The next level down in the hierarchy is the first level of the object-oriented interface, in which pyplot is used only for a few functions such as figure creation, and the user explicitly creates and keeps track of the figure and axes objects. At this level, the user uses pyplot to create figures, and through those figures, one or more axes objects can be created. These axes objects are then used for most plotting actions.

For even more control – which is essential for things like embedding matplotlib plots in GUI applications – the pyplot level may be dropped completely, leaving a purely object-oriented approach.

这种 "state machine" 方法可能适用于 "linear" 脚本,它处理一些 数据和出口我看不出它如何在网络环境中工作 (考虑并发请求等)

我想知道使用 "raw" Matplotlib(在他们的文档中称为 "object-oriented approach")是否可以让你避免这个问题——据我所知,它的 API 更传统- 你创建了一个 Figure class 的实例并调用它的方法来创建你的图表,因此所有状态都封装在该实例中,而不是在请求之间存储。

这是 Matplotlib 文档中的示例:agg_oo.py。注意,它完全避免了pylab和pyplot的使用(后者实际上实现了"state machine layer")。

我想代码可能看起来像这样(未经测试):

from matplotlib.figure import Figure

@view_config(route_name='view_test_svg')
def test_svg_view(request):
    fig = Figure(figsize=(6,6))
    ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
    labels = ['Frogs', 'Hogs', 'Dogs', 'Logs']
    fracs = [15, 30, 45, 10]
    explode=(0, 0.05, 0, 0)
    ax.pie(
        fracs, explode=explode, labels=labels,
        autopct='%1.1f%%', shadow=True, startangle=90)
    ax.set_title('Raining Hogs and Dogs', bbox={'facecolor':'0.8', 'pad':5})
    imgdata = cStringIO.StringIO()
    fig.savefig(imgdata, format='svg')
    imgdata.seek(0)
    svg_dta = imgdata.getvalue()
    # Close the StringIO buffer
    imgdata.close()
    return Response(svg_dta, content_type='image/svg+xml')

Sergey 提供的答案绝对有帮助。我能够发现,如果根据 example 的部分内容向代码添加更多片段,包括 matplotlib.use 语句,内存问题似乎已得到解决。我在我的测试应用程序(上面发布的 2 个视图生成 SVG 文件)和我的主要应用程序中测试了这个,它目前有 3 个 SVG 视图,我将扩展到更多创建 SVG 文件的视图。

在我的主要应用程序中,我可以在所有 3 个现有视图之间刷新多次,而无需 SVG 图像组合,这是我在此代码的先前版本中发现的。该设置对于更多 concurrent 使用似乎是必要的,这可能就是为什么其他 SO 问题似乎没有反映此问题,除非我能够找到导致此问题的其他原因。我不确定这是否是最佳答案;但是,目前这似乎确实可以正常工作,我计划进行监控以查看是否发现任何其他问题。

@view_config(route_name='view_test_svg')
def test_svg_view(request):
    from matplotlib import use as matplotlib_use
    matplotlib_use("Svg")
    from matplotlib.pyplot import close, figure, axes, pie, title, savefig
    log.debug('In test_svg_view')
    fig = figure(1, figsize=(6,6))
    ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
    labels = ['Frogs', 'Hogs', 'Dogs', 'Logs']
    fracs = [15, 30, 45, 10]
    explode=(0, 0.05, 0, 0)
    pie(fracs, explode=explode, labels=labels,
                                autopct='%1.1f%%', shadow=True, startangle=90)
    title('Raining Hogs and Dogs', bbox={'facecolor':'0.8', 'pad':5})
    imgdata = cStringIO.StringIO()
    savefig(imgdata, format='svg')
    imgdata.seek(0)
    svg_dta = imgdata.getvalue()
    # Close the StringIO buffer
    imgdata.close()
    close('all')
    return Response(svg_dta, content_type='image/svg+xml')