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

Can't get matplotlib event handler working with Boost.Python

本文关键字:Boost Python 一起 工作 程序 matplotlib 事件处理      更新时间:2023-10-16

我正在开发一个带有解释器(用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 quitn";
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;
}

如果有人偶然发现这篇文章并感兴趣,我找到了合适的前进方式。 我放弃了 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')