Закрытие объекта pyplot в Tkinter во время работы с PdfPages приводит к остановке экземпляра Tkinter.

Прошу прощения за название, но лучшего описания проблемы не придумал. У меня есть программа на основе Tkinter, в которой пользователь может создать отчет в формате PDF, состоящий из обзора, за которым следуют некоторые подробные графики. Я знал, что по какой-то причине вся программа завершит работу после завершения отчета в формате PDF, но я только недавно сел, чтобы точно определить, что вызвало это.

Я обнаружил, что строка plt.close в начальном обзорном графике приводит к закрытию всей программы после написания отчета в формате pdf (это первая часть, которую я не совсем понимаю, если виновата plot.close, почему весь модуль работает до завершения)? Во-вторых, почему это вообще происходит?

Минимальный пример, который мне удалось создать (с бессмысленными данными для графиков), приведен ниже, где, если строка, которой предшествует # THE CULPRIT, закомментирована, экземпляр Tk() остается живым, но если его оставить как есть, экземпляр Tk() закрывается.

import tkinter as tk
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_pdf import PdfPages
from pathlib import Path

class Pdf(object):
    def __init__(self, master):
        self.master = master
        pdf = PdfPages(Path.cwd() / 'demo.pdf')

        self.pdf = pdf

    def plot_initial(self):
        fig = plt.figure(figsize=(8,6))
        fig.add_subplot(111)

        mu, sigma = 0, 0.1
        s = np.random.normal(mu, sigma, 1000)
        count, bins, ignored = plt.hist(s, 30, density=True)
        plt.plot(bins, 1/(sigma * np.sqrt(2 * np.pi)) *
                 np.exp( - (bins - mu)**2 / (2 * sigma**2) ),
                 linewidth=2, color='r')
        plt.title('Overview')
        plt.xlabel('X')
        plt.ylabel('Y')
        self.pdf.savefig(fig)
        # THE CULPRIT
        plt.close(fig)

    def plot_extra(self):
        fig = plt.figure(figsize=(8,6))
        fig.add_subplot(111)

        mu, sigma = 0, 0.1
        s = np.random.normal(mu, sigma, 1000)
        count, bins, ignored = plt.hist(s, 30, density=True)
        plt.plot(bins, 1/(sigma * np.sqrt(2 * np.pi)) *
                 np.exp( - (bins - mu)**2 / (2 * sigma**2) ),
                 linewidth=2, color='r')
        plt.title('Extra')
        plt.xlabel('X')
        plt.ylabel('Y')
        self.pdf.savefig(fig)
        plt.close(fig)

    def close(self):
        self.pdf.close()

class MVE(object):
    @classmethod
    def run(cls):
        root = tk.Tk()
        MVE(root)
        root.mainloop()

    def __init__(self, master):
        self.root = master
        tk.Frame(master)

        menu = tk.Menu(master)
        master.config(menu=menu)

        test_menu = tk.Menu(menu, tearoff=0)
        menu.add_cascade(label='Bug', menu=test_menu)
        test_menu.add_command(label='PDF', command=
                              self.generate_pdf)

    def generate_pdf(self):
        pdf = Pdf(self)

        pdf.plot_initial()
        for i in range(0,3):
            pdf.plot_extra()
        pdf.close()

if __name__ == "__main__":
    MVE.run()

Версии установленных пакетов/базы Python:

  • Питон 3.7.0
  • Ткинтер 8.6
  • Матплотлиб 2.2.3
  • Нампи 1.15.1

Изменить

Я обновился до Matplotlib 3.0.2 в соответствии с предложением @ImportanceOfBeingErnest, однако проблема все еще остается.


person Bas Jansen    schedule 19.02.2019    source источник
comment
Я уже видел, как это происходит в другом месте. Кажется, что plt.close() делает что-то, что завершает сеанс Tk(). Это произойдет только при использовании бэкэнда Tk. Поэтому, если использовать бэкэнд Qt5Agg для pyplot, но оставить приложение tk как есть, вероятно, сработает, но в целом это нежелательно.   -  person ImportanceOfBeingErnest    schedule 19.02.2019
comment
Я быстро рассмотрю использование другого бэкэнда, хотя это не то, что мне действительно хотелось бы делать, особенно если бэкэнд требует установки дополнительного пакета. В качестве альтернативы я должен посмотреть на базовый код plt.close.   -  person Bas Jansen    schedule 19.02.2019
comment
Обратите внимание, что если вы не хотите отображать фигуру на экране, вы можете использовать бэкенд "Agg", который не требует дополнительного набора инструментов QUI.   -  person ImportanceOfBeingErnest    schedule 19.02.2019
comment
К сожалению, программа всегда использует экран, в то время как отчеты в формате PDF являются необязательным выводом, если пользователь желает.   -  person Bas Jansen    schedule 19.02.2019
comment
Хм, я просмотрел список проблем matplotlib github и не нашел ни одной записи и сделал новый отчет об ошибке, можете ли вы поделиться ссылкой на проблему в качестве ответа? так как это ответит на вопрос (а также на конкретную часть почему)   -  person Bas Jansen    schedule 19.02.2019
comment
Ха-ха, я мог бы исправить это сам в 12707. Это объясняет, почему я уже видел эту проблему.   -  person ImportanceOfBeingErnest    schedule 19.02.2019
comment
@ImportanceOfBeingErnest Это не так, возможно, нам следует подумать о переносе этого в чат?   -  person Bas Jansen    schedule 19.02.2019


Ответы (2)


Похоже, что по умолчанию используется бэкенд TkAgg, измените его на неинтерактивный бэкенд, например agg, перед импортом matplotlib.pyplot:

import tkinter as tk
import matplotlib as mpl
mpl.use('agg')
import matplotlib.pyplot as plt
...
person acw1668    schedule 20.02.2019
comment
Хотя это «решает» текущую проблему, на основе взаимодействия с разработчиками я чувствую, что лучше полностью избегать использования pyplot. - person Bas Jansen; 20.02.2019

Проблема возникает из-за: «противоречия в Matplotlib между предоставлением низкоуровневой библиотеки для использования разработчиками приложений и передовым пользовательским интерфейсом. В этом случае тонкости, которые мы должны предоставить конечному пользователю ( ex, выход из основного цикла GUI при закрытии последней фигуры) противоречит тому, как @Tarskin хочет использовать Matplot в качестве низкоуровневой библиотеки.". Полный комментарий см. здесь.

Проблема явно вызвана matplotlib/lib/matplotlib/backends/_backend_tk.py:

def destroy(self, *args): 
    if self.window is not None: 
        #self.toolbar.destroy() 
        if self.canvas._idle_callback: 
            self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) 
        self.window.destroy() 
    if Gcf.get_num_fig_managers() == 0: 
        if self.window is not None: 
            self.window.quit() 
    self.window = None 

который закрывает приложение, когда все фигуры закрыты. Это необходимо для работы plt.show(block=True) (поэтому, когда вы закрываете все графики, мы возвращаем управление терминалу). Для получения исходного кода перейдите здесь.

Было предложено использовать Agg, и хотя это действительно исправляет это на данный момент, было предложено полностью игнорировать pyplot, когда кто-то интегрирует matplotlib в автономный пакет. Поэтому я исправил это, используя только класс matplotlib.figure.Figure, как указано ниже (с немного более бессмысленными данными, чтобы полностью избежать использования pyplot).

import tkinter as tk
from matplotlib.figure import Figure
import numpy as np
from matplotlib.backends.backend_pdf import PdfPages
from pathlib import Path

class Pdf(object):
    def __init__(self, master):
        self.master = master
        pdf = PdfPages(Path.cwd() / 'demo.pdf')
        fig = Figure(figsize=(8,6))
        axes = fig.add_subplot(111)
        axes.set_xlabel('X')
        axes.set_ylabel('Y')

        self.fig = fig
        self.axes = axes
        self.pdf = pdf

    def plot_initial(self):
        mu, sigma = 0, 0.1
        s = np.random.normal(mu, sigma, 1000)

        self.axes.clear()
        self.axes.plot(s)
        self.axes.set_title('Overview')
        self.pdf.savefig(self.fig)

    def plot_extra(self):
        mu, sigma = 0, 0.1
        s = np.random.normal(mu, sigma, 1000)

        self.axes.clear()
        self.axes.plot(s)
        self.axes.set_title('Extra')
        self.pdf.savefig(self.fig)

    def close(self):
        self.pdf.close()

class MVE(object):
    @classmethod
    def run(cls):
        root = tk.Tk()
        MVE(root)
        root.mainloop()

    def __init__(self, master):
        self.root = master
        tk.Frame(master)

        menu = tk.Menu(master)
        master.config(menu=menu)

        test_menu = tk.Menu(menu, tearoff=0)
        menu.add_cascade(label='Fixed', menu=test_menu)
        test_menu.add_command(label='PDF', command=
                              self.generate_pdf)

    def generate_pdf(self):
        pdf = Pdf(self)

        pdf.plot_initial()
        for i in range(0,3):
            pdf.plot_extra()
        pdf.close()

if __name__ == "__main__":
    MVE.run()

Полное обсуждение можно найти в этой ветке проблем github.

person Bas Jansen    schedule 20.02.2019