Menutup objek pyplot di Tkinter saat bekerja dengan PdfPages menyebabkan instance Tkinter berhenti

Saya minta maaf atas judulnya tetapi saya tidak dapat memikirkan deskripsi masalahnya yang lebih baik. Saya memiliki program berbasis Tkinter yang memiliki opsi bagi pengguna untuk menghasilkan laporan PDF yang terdiri dari ikhtisar, diikuti oleh beberapa plot detail. Saya tahu bahwa karena alasan tertentu seluruh program akan dimatikan setelah laporan PDF diselesaikan, namun baru-baru ini saya duduk untuk benar-benar mengidentifikasi penyebabnya.

Saya menemukan bahwa baris plt.close pada plot ikhtisar awal, menyebabkan seluruh program ditutup setelah laporan pdf ditulis (yang merupakan bagian pertama yang saya tidak mengerti pastinya, jika plot.close yang harus disalahkan mengapa seluruh modul dijalankan sampai selesai)? Kedua, mengapa hal ini bisa terjadi?

Contoh minimum yang dapat saya hasilkan (dengan data yang tidak masuk akal untuk plotnya) tercantum di bawah ini di mana jika baris yang diawali dengan # THE CULPRIT dikomentari, instance Tk() tetap hidup tetapi jika dibiarkan apa adanya, instance Tk() ditutup.

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()

Versi paket/basis python yang diinstal:

  • Python 3.7.0
  • Tkinter 8.6
  • Matplotlib 2.2.3
  • Angka 1.15.1

Edit

Saya telah meningkatkan ke Matplotlib 3.0.2 sesuai saran @ImportanceOfBeingErnest, namun masalahnya masih ada.


person Bas Jansen    schedule 19.02.2019    source sumber
comment
Saya sudah melihat hal ini terjadi di tempat lain. Tampaknya plt.close() melakukan sesuatu yang mengakhiri sesi Tk(). Ini hanya akan terjadi jika menggunakan backend Tk. Jadi jika menggunakan backend Qt5Agg untuk pyplot, tetapi mempertahankan aplikasi tk sebagaimana adanya mungkin akan berhasil - tetapi secara umum tidak diinginkan.   -  person ImportanceOfBeingErnest    schedule 19.02.2019
comment
Saya akan melihat sekilas penggunaan backend yang berbeda, meskipun itu bukan sesuatu yang benar-benar ingin saya lakukan terutama jika backend memerlukan instalasi paket tambahan. Alternatifnya, saya harus melihat kode plt.close yang mendasarinya.   -  person Bas Jansen    schedule 19.02.2019
comment
Perhatikan bahwa jika Anda tidak ingin menampilkan gambar di layar, Anda dapat menggunakan backend "Agg", yang tidak memerlukan perangkat QUI tambahan   -  person ImportanceOfBeingErnest    schedule 19.02.2019
comment
Tercatat, sayangnya program ini selalu menggunakan layar sedangkan laporan pdf adalah keluaran opsional jika diinginkan pengguna.   -  person Bas Jansen    schedule 19.02.2019
comment
Hmm, saya mencari daftar masalah matplotlib github dan tidak menemukan entri apa pun dan membuat laporan bug baru, dapatkah Anda membagikan tautan ke masalah tersebut sebagai jawabannya? karena itu akan menjawab pertanyaan (juga bagian mengapa secara spesifik)   -  person Bas Jansen    schedule 19.02.2019
comment
Haha, saya mungkin sudah memperbaikinya sendiri di 12707. Yang akan menjelaskan mengapa saya sudah melihat masalah itu.   -  person ImportanceOfBeingErnest    schedule 19.02.2019
comment
@ImportanceOfBeingErnest Belum, mungkin kita harus mempertimbangkan untuk memindahkan ini ke obrolan?   -  person Bas Jansen    schedule 19.02.2019


Jawaban (2)


Sepertinya backend default yang digunakan adalah TkAgg, ubah ke backend non-interaktif, seperti agg, sebelum mengimpor 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
Meskipun hal ini 'menyelesaikan' masalah saat ini, berdasarkan interaksi dengan pengembang, saya merasa lebih baik menghindari penggunaan pyplot sepenuhnya. - person Bas Jansen; 20.02.2019

Masalah muncul karena: "ketegangan di Matplotlib antara menyediakan perpustakaan tingkat rendah untuk digunakan oleh pengembang aplikasi dan menjadi antarmuka pengguna garis depan. Dalam hal ini, kenyamanan yang perlu kita sediakan untuk pengguna akhir ( misalnya, keluar dari loop utama GUI ketika gambar terakhir ditutup) bertentangan dengan cara @Tarskin ingin menggunakan Matplot sebagai perpustakaan tingkat rendah.". Untuk komentar selengkapnya, lihat di sini.

Masalahnya tampaknya disebabkan oleh 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 

yang keluar dari aplikasi ketika semua angka ditutup. Ini penting agar plt.show(block=True) berfungsi (jadi ketika Anda menutup semua plot, kami mengembalikan kendali ke terminal). Untuk sumbernya, buka di sini.

Saran dibuat untuk menggunakan Agg dan meskipun ini memang memperbaikinya untuk saat ini, saran dibuat untuk mengabaikan pyplot sepenuhnya, ketika seseorang mengintegrasikan matplotlib ke dalam paket yang berdiri sendiri. Oleh karena itu, saya sekarang telah memperbaikinya hanya dengan menggunakan kelas matplotlib.figure.Figure, seperti yang tercantum di bawah ini (dengan lebih banyak data yang tidak masuk akal, untuk menghindari penggunaan pyplot sepenuhnya).

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()

Diskusi selengkapnya dapat ditemukan di ini thread masalah github.

person Bas Jansen    schedule 20.02.2019