如何实现PySide6中Plotly绘图窗口不阻塞数据分析脚本运行?
实现非阻塞的Plotly独立绘图窗口(类似Matplotlib)
我运行数据分析脚本时更偏好Plotly而非Matplotlib,想实现类似Matplotlib的非阻塞绘图窗口效果,不会阻塞脚本运行。
编写了如下代码,通过PySide6的WebEngineWidget显示离线Plotly图表:
import numpy as np import plotly.graph_objs as go import plotly.offline fig = go.Figure() fig.add_scatter(x=np.random.rand(100), y=np.random.rand(100), mode='markers', marker={'size': 30, 'color': np.random.rand(100), 'opacity': 0.6, 'colorscale': 'Viridis'}); import os, sys from PySide6.QtWidgets import QApplication from PySide6.QtCore import QUrl from PySide6 import QtWebEngineWidgets class PlotlyViewer(QtWebEngineWidgets.QWebEngineView): def __init__(self, fig, exec=True): # 创建QApplication实例或复用已有实例 self.app = QApplication.instance() if QApplication.instance() else QApplication(sys.argv) super().__init__() self.file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "temp.html")) plotly.offline.plot(fig, filename=self.file_path, auto_open=False) self.load(QUrl.fromLocalFile(self.file_path)) self.setWindowTitle("Plotly Viewer") self.show() if exec: self.app.exec() def closeEvent(self, event): os.remove(self.file_path) win = PlotlyViewer(fig)
但执行以下代码时,win2要等第一个窗口关闭后才会弹出:
win = PlotlyViewer(fig) win2 = PlotlyViewer(fig2) win3 = PlotlyViewer(fig3)
我知道可以制作包含多图表的部件,但有时只需查看单个图表,有时需要多个,想知道能不能通过进程实现脚本持续执行,或者这是否和Python GIL有关?
问题原因
核心问题是**QApplication.exec()会阻塞主线程**:每次创建PlotlyViewer实例时,如果exec=True,就会启动Qt的事件循环并卡住主线程,直到窗口关闭才会继续执行后续代码。这和GIL无关,是Qt事件循环的运行机制导致的。
解决方法
方法1:统一管理Qt事件循环(推荐)
修改PlotlyViewer类,不在实例化时调用app.exec(),而是在所有窗口创建完成后统一启动事件循环,这样所有窗口会同时显示,且脚本在事件循环启动前不会被阻塞。
修改后的代码:
import numpy as np import plotly.graph_objs as go import plotly.offline import os, sys from PySide6.QtWidgets import QApplication from PySide6.QtCore import QUrl from PySide6 import QtWebEngineWidgets class PlotlyViewer(QtWebEngineWidgets.QWebEngineView): def __init__(self, fig): self.app = QApplication.instance() if QApplication.instance() else QApplication(sys.argv) super().__init__() # 为每个窗口生成独立临时HTML文件,避免互相覆盖 self.file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"temp_{id(self)}.html")) plotly.offline.plot(fig, filename=self.file_path, auto_open=False) self.load(QUrl.fromLocalFile(self.file_path)) self.setWindowTitle("Plotly Viewer") self.show() def closeEvent(self, event): os.remove(self.file_path) # 创建多个测试图表 fig = go.Figure() fig.add_scatter(x=np.random.rand(100), y=np.random.rand(100), mode='markers', marker={'size':30, 'color':np.random.rand(100), 'opacity':0.6, 'colorscale':'Viridis'}) fig2 = go.Figure() fig2.add_bar(x=[1,2,3], y=[4,5,6]) fig3 = go.Figure() fig3.add_pie(values=[30,20,50]) # 一次性创建所有窗口 win = PlotlyViewer(fig) win2 = PlotlyViewer(fig2) win3 = PlotlyViewer(fig3) # 统一启动事件循环(这一步会阻塞,但所有窗口已完成创建) QApplication.instance().exec()
方法2:多进程实现完全非阻塞
如果需要脚本在显示窗口后继续执行其他任务(无需等待窗口关闭),可以用multiprocessing模块为每个窗口启动独立进程,每个窗口的Qt事件循环在各自进程中运行,完全不阻塞主脚本。
示例代码:
import numpy as np import plotly.graph_objs as go import plotly.offline import os, sys from PySide6.QtWidgets import QApplication from PySide6.QtCore import QUrl from PySide6 import QtWebEngineWidgets import multiprocessing def show_plot(fig): app = QApplication(sys.argv) viewer = QtWebEngineWidgets.QWebEngineView() # 用进程ID生成唯一临时文件 file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), f"temp_{multiprocessing.current_process().pid}.html")) plotly.offline.plot(fig, filename=file_path, auto_open=False) viewer.load(QUrl.fromLocalFile(file_path)) viewer.setWindowTitle(f"Plotly Viewer - PID {multiprocessing.current_process().pid}") viewer.show() # 程序退出时清理临时文件 def cleanup(): os.remove(file_path) app.aboutToQuit.connect(cleanup) app.exec() # 创建测试图表 fig = go.Figure() fig.add_scatter(x=np.random.rand(100), y=np.random.rand(100), mode='markers', marker={'size':30, 'color':np.random.rand(100), 'opacity':0.6, 'colorscale':'Viridis'}) fig2 = go.Figure() fig2.add_bar(x=[1,2,3], y=[4,5,6]) fig3 = go.Figure() fig3.add_pie(values=[30,20,50]) # 启动多进程显示窗口 processes = [] for f in [fig, fig2, fig3]: p = multiprocessing.Process(target=show_plot, args=(f,)) p.start() processes.append(p) # 主脚本继续执行其他任务(示例:打印进度) print("主脚本继续运行,不会被窗口阻塞") for i in range(10): print(f"主脚本执行任务 {i+1}/10") import time time.sleep(1) # 可选:等待所有窗口关闭后结束主脚本 # for p in processes: # p.join()
关于GIL的说明
这个问题和Python GIL无关。GIL主要影响多线程中CPU密集型任务的并行执行,而你的问题是Qt事件循环阻塞主线程导致的。多进程方案之所以有效,是因为每个进程有独立的Python解释器和GIL,完全隔离了各个窗口的事件循环。
内容的提问来源于stack exchange,提问作者yankeefan11
相关产品推荐
相关产品推荐

