在 Matplotlib 3.1.2 中使用 qt5agg 后端,get_backend() 会改变行为
Posted
技术标签:
【中文标题】在 Matplotlib 3.1.2 中使用 qt5agg 后端,get_backend() 会改变行为【英文标题】:Using qt5agg backend with Matplotlib 3.1.2, get_backend() changes behavior 【发布时间】:2020-01-09 02:58:55 【问题描述】:我一直在使用一个在 Qt 窗口中嵌入 Matplotlib 图形的项目,当我将 Matplotlib 更新到高于 3.1.0rc1 的版本时发现它失败了。我创建了一个最小的工作示例 (testImports.py
):
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
import numpy as np
f = plt.figure()
x = np.arange(0, 10, 0.001)
ysin = np.sin(x)
plt.plot(x, ysin, '--')
plt.show()
使用 Matplotlib 3.1.0rc1 或更早版本,它可以正常运行,显示图形。使用 Matplotlib 3.1.0rc2 或更高版本时,出现以下错误(请注意,我在虚拟环境中工作,并且已将路径的根更改为 test_env):
Traceback (most recent call last):
File "testImports.py", line 14, in <module>
f = plt.figure()
File "test_env/lib/python3.6/site-packages/matplotlib/pyplot.py", line 534, in figure
**kwargs)
File "test_env/lib/python3.6/site-packages/matplotlib/backend_bases.py", line 3249, in new_figure_manager
return cls.new_figure_manager_given_figure(num, fig)
File "test_env/lib/python3.6/site-packages/matplotlib/backend_bases.py", line 3255, in new_figure_manager_given_figure
canvas = cls.FigureCanvas(figure)
TypeError: 'NoneType' object is not callable
我发现如果我在 testImports.py
的顶部包含以下几行,它适用于所有版本的 Matplotlib 3+。
import matplotlib
matplotlib.use('qt5agg')
因此,Matplotlib 注册后端的方式似乎发生了一些变化。
在我的测试中,我还发现在 testImports.py 的顶部添加以下内容可以解决问题(也无需调用 use('qt5agg')
:
import matplotlib
print(matplotlib.get_backend())
这对我来说似乎很奇怪,因为 matplotlib.get_backend()
函数只是从字典中返回一个值(来自 source):
def get_backend():
"""
Return the name of the current backend.
See Also
--------
matplotlib.use
"""
return rcParams['backend']
这让我想到了我的问题:
-
Matplotlib 3.1.0rc1 和 3.1.0rc2 之间发生了什么变化,导致必须在导入特定后端函数之前使用
matplotlib.use()
函数?
为什么调用 matplotlib.get_backend()
会极大地改变我的程序的行为,从而无需调用 matplotlib.use()
?
如果有帮助,我正在使用 Python 3.6.9 和 Numpy 1.18.0。
编辑: 我安装了 Matplotlib 3.2.0rc2 并且仍然遇到相同的行为。
【问题讨论】:
如果您导入from PyQt5 import QtWidgets
,它也可以正常工作。
看起来像是开发版中已经修复的bug,所以在matplotlib 3.2中可能不会再出现
【参考方案1】:
我不知道我要描述的是matplotlib
的预期行为还是一个错误。也许维护者可以参与进来。
本质上,matplotlib.get_backend()
是 NOT 只是从字典中返回一个值。事实上,它通过在钩子下导入 pyplot 触发了“后端”的延迟初始化,我可以想象会产生一些行为变化,因为仅仅调用 import matplotlib
不足以触发初始化(至少在所有例)。
当您导入 matplotlib 或其任何模块时,matplotlib.__init__.py
会隐式运行。
如果键值设置为rcsetup._auto_backend_sentinel
,rcParams.__getitem__()
似乎打算对“后端”进行延迟初始化。
当第一次构造 rcParam 对象时,dict 中没有“后端”键,即使所需的默认值(可能取决于 matplotlibrc?)是rcsetup._auto_backend_sentinel
。
__setitem__()
方法的这些行可能涉及key == backend
和val == rcsetup._auto_backend_sentinel
,但backend
还不是rcParams(MutableMapping, dict)
对象中的有效键的情况。因此,当第 3 行 if 'backend' in self:
(隐式调用 __getitem__
并使用 'backend' 键检查该键是否存在)被处理时,不会发生后端的延迟初始化,因为我标记为“THIS MAY THROW”的__getitem__
行实际上确实会抛出,并防止随后的延迟初始化。
# Excerpt of rcParams(MutableMapping, dict).__setitem__ from around lines 672
elif key == 'backend':
if val is rcsetup._auto_backend_sentinel:
if 'backend' in self:
return
# Excerpt of rcParams(MutableMapping, dict).__getitem__ from around lines 699
elif key == "backend":
val = dict.__getitem__(self, key) # THIS MAY THROW
if val is rcsetup._auto_backend_sentinel:
from matplotlib import pyplot as plt
plt.switch_backend(rcsetup._auto_backend_sentinel)
但是,在__setitem__
中,代码确实成功地将rcParams['backend']
设置为rcsetup._auto_backend_sentinel
(x in X
调用忽略 KeyErrors 是正常的)。因此rcParams
已填充,但延迟初始化尚未完成。
现在当你调用matplotlib.get_backend()
(或其他相关函数)时,部分代码路径会做rcParams['backend']
,这一次实际上会触发__getitem__
中的延迟初始化。
【讨论】:
如果你想要一个简单的例子来确认matplotlib.get_backend()
函数不是简单地从字典中返回一个值,而是当第一次发出实际导致Qt事件循环生成并绑定到REPL循环时,除其他外,您可以在新的终端会话中运行以下命令:import matplotlib as mpl
; from PyQt5.QtWidgets import QApplication
;并确认QApplication.instance() is None
。现在“简单地”使用 mpl.rcParams['backend']
查看后端。如果再次检查,您会看到由于延迟初始化,Qt 实例正在运行。以上是关于在 Matplotlib 3.1.2 中使用 qt5agg 后端,get_backend() 会改变行为的主要内容,如果未能解决你的问题,请参考以下文章
在同一进程中将 matplotlib 与 Qt5 后端一起使用并运行现有 QApplication
使用 Qt Designer 表单和 PyQt5 在 QWidget 中绘制 matplotlib 图形
Matplotlib 事件处理:他们何时发送 matplotlib 事件以及何时发送 Qt 事件
用于 Qt 4、Python 3 的 matplotlib 小部件