การปิดวัตถุ 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()

เวอร์ชันของแพ็คเกจ/ฐานหลามที่ติดตั้ง:

  • หลาม 3.7.0
  • ทีคินเตอร์ 8.6
  • Matplotlib 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 ระหว่างการจัดหาไลบรารีระดับต่ำที่นักพัฒนาแอปพลิเคชันใช้และการเป็นส่วนติดต่อผู้ใช้ระดับแนวหน้า ในกรณีนี้ ความดีที่เราจำเป็นต้องจัดเตรียมสำหรับผู้ใช้ปลายทาง ( เช่น การออกจากลูปหลักของ 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