无法让 matplotlib 事件处理程序与 Boost.Python 一起使用

Posted

技术标签:

【中文标题】无法让 matplotlib 事件处理程序与 Boost.Python 一起使用【英文标题】:Can't get matplotlib event handler working with Boost.Python 【发布时间】:2019-01-04 22:32:56 【问题描述】:

我正在开发一个带有解释器(用 C++ 编写)的 C++11 程序,它调用各种类方法来完成工作。我想添加使用 Python 3.5 和 matplotlib(后端是 TkAgg)创建数据图的能力。我还希望用户能够在程序中启动 Python 解释器或运行 Python 脚本,以便实时对绘图进行详细调整/增强。

到目前为止,我已经成功地使用 Boost.Python 1.65.1 作为我的接口层创建和保存了绘图,并使用 Python 的代码模块启动了 Python 解释器。但是,matplotlib 图的事件循环不会在我的 C++ 解释器提示符下运行(即,如果我在我的图上放置另一个窗口,该图将变为空白并且不会重绘)。只有当用户启动 Python 解释器时,事件循环才会起作用并重新绘制图形。我希望程序的行为就像用户在发出 matplotlib.pyplot.ion() 命令后在本机 Python 解释器中创建图一样。我在用于创建绘图的 Python 代码中添加了 plt.ion() 调用,但这似乎对结果行为没有影响。

我试图通过在不同线程上执行 Python 绘图和 C++ 解释来解决此问题,但它似乎没有任何帮助。我想知道我是否应该以不同于我目前的方式来处理 Python GIL,或者我是否可以做其他事情来让 matplotlib 事件循环在后台线程中运行。我发布了一个简单的示例,重现了我遇到的核心问题。任何帮助表示赞赏。

我尝试过的另一件事是手动调用 matplotlib 的 canvas.start_event_loop()。这似乎可行,但它阻止我创建新的情节,直到它返回/超时,这并不理想。

这是创建绘图的 Python 代码

# Import matplotlib and pyplot and turn on interactive mode
import matplotlib
import matplotlib.pyplot as plt
plt.ion()

def PlotData(num):
    """ Create a simple plot and return the figure.
    """
    data_list = [[1,2,3,4],[3,2,4,1],[4,3,2,1]]
    data = data_list[num]

    f = plt.figure()
    ax = f.gca()

    ax.set_xlabel('x label')
    ax.set_ylabel('y label')

    ax.plot([1,2,3,4],data,'bo-')

    title = 'Data Set ' + str(num+1)
    f.suptitle(title)
    print(title)
    f.show()
    return f

这是 C++ 代码

#include <boost/python.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/chrono.hpp>

#include <iostream>
#include <string>

PyThreadState* mainThreadState = nullptr;
PyThreadState* pts = nullptr; /*!< Pointer for the current thread state */

/*! Initialize Python */
void init()

    Py_Initialize();
    PyEval_InitThreads();
    mainThreadState = PyEval_SaveThread();


void init_thread()

    pts = PyThreadState_New(mainThreadState->interp);


/*! Calls Python to create a simple plot interactively */
void PlotData(const int& inp)

    using namespace boost::python;

    // Acquire the GIL
    std::cout << "Python Thread GIL State (before): " << PyGILState_Check() << std::endl;
    PyEval_RestoreThread(pts);

    object background = import("sto");
    object fig = background.attr("PlotData")(inp);

    // The code below will show the plot, but the plot won't update in real time at the C++ command line
    object plt = import("matplotlib.pyplot");
    fig.attr("show")();
    plt.attr("pause")(.1);

    std::cout << "Python Thread GIL State (during): " << PyGILState_Check() << std::endl;

    // Release the GIL
    pts = PyEval_SaveThread();
    std::cout << "Python Thread GIL State (after): " << PyGILState_Check() << std::endl;


int main(int argc, char* argv[])

    // Create a thread pool with 1 thread for all Python code
    boost::asio::io_service ioService;
    boost::thread_group threadpool;

    // Start the service
    auto work = std::make_shared<boost::asio::io_service::work>(ioService);

    // Add a single thread to the thread pool for Python operations
    threadpool.create_thread(boost::bind(&boost::asio::io_service::run, &ioService));

    // Initialize Python
    init();
    std::cout << "Submitting init_thread" << std::endl;
    ioService.post(boost::bind(init_thread));

    // Create some plots on the background thread
    std::cout << "Submitting PlotData calls" << std::endl;
    ioService.post(boost::bind(PlotData,0));
    ioService.post(boost::bind(PlotData,1));

    // Pause to allow plots to update in realtime
    boost::this_thread::sleep_for(boost::chrono::seconds(4));

    // Receive inputs from command line (the plots should update during this time but they don't)
    std::string inp"1";
    std::cout << "Enter to quit\n";
    while (!inp.empty())
    
        std::cout << ">> ";
        std::getline(std::cin,inp);
        std::cout << "GIL State: " << PyGILState_Check() << std::endl;
    

    // Finalize Python
    std::cout << "Submitting Py_Finalize" << std::endl;
    ioService.post(boost::bind(PyEval_RestoreThread,pts));
    ioService.post(boost::bind(Py_Finalize));

    // Allow jobs to complete and shut down the thread pool
    work.reset();
    threadpool.join_all();
    ioService.stop();

    return 0;

【问题讨论】:

【参考方案1】:

如果有人偶然发现这篇文章并感兴趣,我找到了一个合适的前进方向。我放弃了 Boost.Asio 方法,转而使用 std::future 更直接的方法。此外,我更新了代码以在后台线程上获取输入,而不是在后台运行 Python。我不得不玩弄超时时间来获得我想要的响应能力,但是现在 Python 图形更新而没有成为焦点,用户可以在 C++ 提示符下输入命令。下面的代码假设当 C++ 提示符启动时,一个图形已经在 Python 中打开。

这是更新后的 C++ 代码:

#include <iostream>
#include <string>
#include <future>
#include <chrono>

#include <Python.h>
#include <boost/python.hpp>

void GetInput()

    std::cout << "$ ";
    std::string tline;
    std::getline(std::cin, tline);
    std::cout << "You entered: '" << tline << "'\n";


int main(int argc, char* argv[])

    using namespace boost::python;

    Py_Initialize();

    try
    
        std::cout << "Importing python code" << std::endl;
        object background = import("sto");

        std::cout << "Calling python code" << std::endl;
        background.attr("SimpleTest")(argc);
        object fig = background.attr("PlotData")(argc);

        std::chrono::milliseconds span(100);
        for (int i = 0; i < 10; ++i)
        
            std::cout << "Iteration " << i << std::endl;
            std::future<void> fut = std::async(GetInput);
            while (fut.wait_for(span) == std::future_status::timeout)
                background.attr("Pause")(.2);
        
     catch (error_already_set& e)
    
        PyErr_Print();
    
    Py_Finalize();

    return 0;

以及导入的相应 Python 代码(来自 sto.py):

# Import matplotlib and pyplot and turn on interactive mode
import sys
import matplotlib
import matplotlib.pyplot as plt
plt.ion()
plt.style.use('classic')


def SimpleTest(num):
    print('Python code: you entered ' + str(num))

def PlotData(num):
    """ Create a simple plot and return the figure.
    """
    if len(sys.argv) == 0:
        sys.argv.append('sto.py')

    data_list = [[1,2,3,4],[3,2,4,1],[4,3,2,1]]
    if num < len(data_list):
        data = data_list[num]
    else:
        data = data_list[1]

    f = plt.figure()
    ax = f.gca()

    ax.set_xlabel('x label')
    ax.set_ylabel('y label')

    ax.plot([1,2,3,4],data,'bo-')

    title = 'Data Set ' + str(num+1)
    f.suptitle(title)
    print(title)
    f.show()
    plt.pause(.02)
    return f

if __name__ == '__main__':
    SimpleTest(8)
    PlotData(4)

def Pause1(timeout):
    '''This function isn't great because it brings the 
       figure into focus which is not desired'''
    plt.pause(timeout)

def Pause2(timeout):
    '''This function starts the event loop but does not 
       bring the figure into focus (which is good)'''
    fig = plt.gcf()
    fig.canvas.start_event_loop(timeout)

def Pause(timeout):
    # print('Waiting for ' + str(timeout) + ' seconds')
    Pause2(timeout)
    # print('Done waiting')

【讨论】:

以上是关于无法让 matplotlib 事件处理程序与 Boost.Python 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

Matplotlib 事件处理:他们何时发送 matplotlib 事件以及何时发送 Qt 事件

PyQT5 与 matplotlib 图,事件循环已经在运行

是否必须以不同的方式处理键盘事件才能在 Matplotlib/PyQt5 中选择事件? [复制]

需要一个事件处理程序来处理动态添加的控件,该控件与另一个动态添加的控件交互

EXTJS,无法将事件处理程序绑定到控制器

如何通过让子视图 UIButton 获取新闻事件?