Matplotlib前端、后端概念

Matplotlib是采用面向对象方法设计的,绘图过程中的各种元素,如图像、子图、坐标轴、曲线等都有相应的类,通过类的接口函数和属性可以对图像和图像的各个组成元素进行控制。
Matplotlib的绘图结果可以有各种输出形式,例如常见用的将绘图结果嵌入到wxpython、pygtk、Qt等GUI窗体中,或者将绘图结果输出为图片文件,或在Web应用程序中输出绘图结果。
为了便于用户实现这些不同的输出,Matplotlib在设计上对用户编写的绘图代码和对不同输出形式的处理方法进行了隔离,因此出现了前端(frontend)和后端的概念(backend)。后端可以认为就是不同输出形式的处理功能,前端可以认为就是用户所要绘制的图像。
就像Web开发中的前后端分离一样,用户只用关心如何绘图即可,Matplotlib会根据用户选择的后端进行输出。这样相同的前端绘图代码,就可以便捷地实现各种绘图输出。

Matplotlib前后端分离原理

Matplotlib中有四个模块与后端相关,matplotlib.backend_basesmatplotlib.backend_managersmatplotlib.backend_toolsmatplotlib.backends,通过这四个模块Matplotlib实现前后端分离。

  • matplotlib.backend_bases模块:用于定义每个后端必须实现的六个抽象类。
    • RendererBase:底层渲染处理抽象类
    • FigureCanvasBase:图像与后端界面隔离抽象类。
    • GraphicsContextBase:颜色、线条样式功能抽象类。
    • Event:事件处理抽象类。
    • ShowBase:图像显示抽象类。
    • ToolContainerBase:工具栏抽象类。
  • matplotlib.backend_managers:用于定义工具栏的相关类。
  • matplotlib.backend_tools:用于定义工具栏工具项的基类。
  • matplotlib.backends:用于定义各种不同后端的具体实现,每种不同实现均为单独的模块,例如matplotlib.backends.backend_pyqt5模块为PyQT后端的抽象实现。

Matplotlib后端分类

根据功能Matplotlib的后端可以分为两种:

  • 用户界面后端(也称为交互式后端),这类后端往往具有GUI界面可以与用户进行交互。例如用于wxpython、pygtk、tkinter、qt4、qt5、macosx的后端。对于用户界面后端,Matplotlib还将渲染器(renderer)和画布(canvas)分离开来,以实现更灵活的定制功能。Matplotlib使用的主要的渲染器是基于Anti-GrainGeometry C++库的Agg渲染器。除了macosx,所有的用户界面都使用Agg渲染器,因而有WXAgg、GTK3Agg、QT4Agg、QT5Agg、TkAgg等。有些用户界面也支持其他的渲染器,如Cairo渲染器,因而有GTK3Cairo、QT4Cairo、QT5Cairo等。
  • 用于生成图片文件的后端,如生成PNG、SVG、PDF等文件。

matplotlib.backends模块中包含了各种不同后端的具体实现,matplotlib.backends模块的目录结构如下:

backends
│  backend_agg.py
│  backend_cairo.py
│  backend_gtk3.py
│  backend_gtk3agg.py
│  backend_gtk3cairo.py
│  backend_macosx.py
│  backend_mixed.py
│  backend_nbagg.py
│  backend_pdf.py
│  backend_pgf.py
│  backend_ps.py
│  backend_qt4.py
│  backend_qt4agg.py
│  backend_qt4cairo.py
│  backend_qt5.py
│  backend_qt5agg.py
│  backend_qt5cairo.py
│  backend_svg.py
│  backend_template.py
│  backend_tkagg.py
│  backend_tkcairo.py
│  backend_webagg.py
│  backend_webagg_core.py
│  backend_wx.py
│  backend_wxagg.py
│  backend_wxcairo.py
│  qt_compat.py
│  _backend_agg.cp37-win_amd64.pyd
│  _backend_pdf_ps.py
│  _backend_tk.py
│  _tkagg.cp37-win_amd64.pyd
│  __init__.py

查看默认使用的后端

matplotlib.get_backend()返回当前使用的后端的名称。
案例:

import matplotlib
print(matplotlib.get_backend())

结果

Qt5Agg

选择后端

matplotlib与选择后端相关的主要函数有matplotlib.use()函数和plt.switch_backend()函数。

matplotlib.use(backend, *, force=True)函数有两个参数:backend为后端名称;force为是否强制使用后端,如果后端不存在,则会抛出异常。
matplotlib.use()函数执行流程如下:

  • 通过matplotlib.rcsetup模块中的 validate_backend函数检查后端名称并返回后端名称。
  • 检查rcParams[backend]的值是否与后端名称一致,一致退出检查,不一致继续流程。
  • 检查是否导入matplotlib.pyplot模块,如果没有导入,将rcParams[backend]的值修改为后端名称,如果已导入,尝试使用plt.switch_backend()函数切换后端。

plt.switch_backend(backend)函数只有一个参数,即后端名称。

  • plt.switch_backend(backend)首先检测后端名称,如果名称不正常抛出错误,检测正常继续。
  • 随后导入后端对应模块,并创建相关对象。
  • matplotlib.backends.backend设为后端名称。

由此可知,两者的区别在于:如果没有导入matplotlib.pyplot模块,matplotlib.use()函数仅修改rcParams[backend]的值,而没有实际切换后端;如果已导入matplotlib.pyplot模块,调用plt.switch_backend()函数设置后端。

matplotlib内置的后端名称

  • 交互式后端:
    GTK3Agg, GTK3Cairo, MacOSX, nbAgg,
    Qt4Agg, Qt4Cairo, Qt5Agg, Qt5Cairo,
    TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo

  • 非交互式后端:
    agg, cairo, pdf, pgf, ps, svg, template

matplotlib切换后端案例

import matplotlib
import matplotlib.pyplot as plt
plt.switch_backend('tkagg')
print(matplotlib.get_backend())
print( matplotlib.backends.backend)

结果:

TkAgg
tkagg

matplotlib.use()函数源码

def use(backend, *, force=True):
    """
    Select the backend used for rendering and GUI integration.

    Parameters
    ----------
    backend : str
        The backend to switch to.  This can either be one of the standard
        backend names, which are case-insensitive:

        - interactive backends:
          GTK3Agg, GTK3Cairo, MacOSX, nbAgg,
          Qt4Agg, Qt4Cairo, Qt5Agg, Qt5Cairo,
          TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo

        - non-interactive backends:
          agg, cairo, pdf, pgf, ps, svg, template

        or a string of the form: ``module://my.module.name``.

    force : bool, default: True
        If True (the default), raise an `ImportError` if the backend cannot be
        set up (either because it fails to import, or because an incompatible
        GUI interactive framework is already running); if False, ignore the
        failure.

    See Also
    --------
    :ref:`backends`
    matplotlib.get_backend
    """
    name = validate_backend(backend)
    # we need to use the base-class method here to avoid (prematurely)
    # resolving the "auto" backend setting
    if dict.__getitem__(rcParams, 'backend') == name:
        # Nothing to do if the requested backend is already set
        pass
    else:
        # if pyplot is not already imported, do not import it.  Doing
        # so may trigger a `plt.switch_backend` to the _default_ backend
        # before we get a chance to change to the one the user just requested
        plt = sys.modules.get('matplotlib.pyplot')
        # if pyplot is imported, then try to change backends
        if plt is not None:
            try:
                # we need this import check here to re-raise if the
                # user does not have the libraries to support their
                # chosen backend installed.
                plt.switch_backend(name)
            except ImportError:
                if force:
                    raise
        # if we have not imported pyplot, then we can set the rcParam
        # value which will be respected when the user finally imports
        # pyplot
        else:
            rcParams['backend'] = backend
    # if the user has asked for a given backend, do not helpfully
    # fallback
    rcParams['backend_fallback'] = False

plt.switch_backend()函数源码

def switch_backend(newbackend):
    """
    Close all open figures and set the Matplotlib backend.

    The argument is case-insensitive.  Switching to an interactive backend is
    possible only if no event loop for another interactive backend has started.
    Switching to and from non-interactive backends is always possible.

    Parameters
    ----------
    newbackend : str
        The name of the backend to use.
    """
    global _backend_mod
    # make sure the init is pulled up so we can assign to it later
    import matplotlib.backends
    close("all")

    if newbackend is rcsetup._auto_backend_sentinel:
        current_framework = cbook._get_running_interactive_framework()
        mapping = {'qt5': 'qt5agg',
                   'qt4': 'qt4agg',
                   'gtk3': 'gtk3agg',
                   'wx': 'wxagg',
                   'tk': 'tkagg',
                   'macosx': 'macosx',
                   'headless': 'agg'}

        best_guess = mapping.get(current_framework, None)
        if best_guess is not None:
            candidates = [best_guess]
        else:
            candidates = []
        candidates += ["macosx", "qt5agg", "gtk3agg", "tkagg", "wxagg"]

        # Don't try to fallback on the cairo-based backends as they each have
        # an additional dependency (pycairo) over the agg-based backend, and
        # are of worse quality.
        for candidate in candidates:
            try:
                switch_backend(candidate)
            except ImportError:
                continue
            else:
                rcParamsOrig['backend'] = candidate
                return
        else:
            # Switching to Agg should always succeed; if it doesn't, let the
            # exception propagate out.
            switch_backend("agg")
            rcParamsOrig["backend"] = "agg"
            return

    # Backends are implemented as modules, but "inherit" default method
    # implementations from backend_bases._Backend.  This is achieved by
    # creating a "class" that inherits from backend_bases._Backend and whose
    # body is filled with the module's globals.

    backend_name = cbook._backend_module_name(newbackend)

    class backend_mod(matplotlib.backend_bases._Backend):
        locals().update(vars(importlib.import_module(backend_name)))

    required_framework = _get_required_interactive_framework(backend_mod)
    if required_framework is not None:
        current_framework = cbook._get_running_interactive_framework()
        if (current_framework and required_framework
                and current_framework != required_framework):
            raise ImportError(
                "Cannot load backend {!r} which requires the {!r} interactive "
                "framework, as {!r} is currently running".format(
                    newbackend, required_framework, current_framework))

    _log.debug("Loaded backend %s version %s.",
               newbackend, backend_mod.backend_version)

    rcParams['backend'] = rcParamsDefault['backend'] = newbackend
    _backend_mod = backend_mod
    for func_name in ["new_figure_manager", "draw_if_interactive", "show"]:
        globals()[func_name].__signature__ = inspect.signature(
            getattr(backend_mod, func_name))

    # Need to keep a global reference to the backend for compatibility reasons.
    # See https://github.com/matplotlib/matplotlib/issues/6092
    matplotlib.backends.backend = newbackend
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐