Обработка исключения в python tkinter

Я написал приложение на Python Tkinter. Недавно я заметил, что для одной из операций она иногда закрывается (без каких-либо ошибок), если эта операция не удалась. Я написал небольшую программу, чтобы проиллюстрировать проблему: -

import os
from Tkinter import *

def copydir():
    src = "D:\\a\\x\\y"
    dest = "D:\\a\\x\\z"
    os.rename(src,dest)

master = Tk()

def callback():
    global master
    master.after(1, callback)
    copydir()
    print "click!"

b = Button(master, text="OK", command=copydir)
b.pack()

master.after(100, callback)

mainloop()

Чтобы воспроизвести проблему, откройте папку, которую она переименует, в «командной строке ms», чтобы ее переименование вызвало исключение из кода Tkinter.

Мой исходный код использует многопоточность, а также выполняет другие задачи, поэтому я попытался сделать операции в этом тестовом сценарии как можно более похожими.

Теперь, если я запущу этот код, дважды щелкнув его, программа просто закроется без каких-либо ошибок. Но если бы я запускал этот скрипт из консоли, то сообщения об исключениях сбрасывались бы на консоль, и, по крайней мере, я знал бы, что что-то не так.

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

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

2) Могу ли я предотвратить выход приложения tkinter из-за такой ошибки и выдать любое исключение в каком-либо диалоговом окне TK.

Спасибо за помощь!!


person sarbjit    schedule 06.03.2013    source источник
comment
Просто обратите внимание, вы должны объявить master глобальным, только если вы переназначите его, но вы просто вызываете его метод, поэтому объявление не нужно.   -  person unddoch    schedule 06.03.2013
comment
Да вы правы. Нет необходимости объявлять мастер как глобальный объект.   -  person sarbjit    schedule 06.03.2013
comment
Опубликуйте свое решение как ответ, а не добавляйте его к вопросу.   -  person Honest Abe    schedule 06.03.2013


Ответы (4)


Вы можете переопределить CallWrapper Tkinter. Для этого необходимо использовать именованный импорт Tkinter вместо импорта подстановочных знаков:

import Tkinter as tk
import traceback

class Catcher: 
    def __init__(self, func, subst, widget):
        self.func = func 
        self.subst = subst
        self.widget = widget
    def __call__(self, *args):
        try:
            if self.subst:
                args = apply(self.subst, args)
            return apply(self.func, args)
        except SystemExit, msg:
            raise SystemExit, msg
        except:
            traceback.print_exc(file=open('test.log', 'a'))

# ...
tk.CallWrapper = Catcher
b = tk.Button(master, text="OK", command=copydir)
b.pack()
master.mainloop()
person A. Rodas    schedule 06.03.2013
comment
Я включил это в свой код, но почему-то это не работает. При двойном щелчке по коду он просто выходит без записи какого-либо файла. Смотрите мой обновленный код. - person sarbjit; 06.03.2013
comment
@sarbjit Я не пробовал код выше, но, похоже, он работает, только если импорт назван. Я обновил ответ. - person A. Rodas; 06.03.2013
comment
С именованным импортом все работает нормально. Хотя мне пришлось добавить некоторый код, чтобы отменить все ожидающие события Tk after и явно закрыть основной цикл. - person sarbjit; 07.03.2013

Я вижу, у вас есть не объектно-ориентированный пример, поэтому я покажу два варианта решения проблемы перехвата исключений.

Ключ находится в файле tkinter\__init__.py. Видно, что есть задокументированный метод report_callback_exception класса Tk. Вот его описание:

report_callback_exception()

Сообщить об исключении обратного вызова в sys.stderr.

Приложения могут захотеть переопределить эту внутреннюю функцию и должны, если sys.stderr имеет значение None.

Итак, как мы видим, предполагается переопределить этот метод, давайте сделаем это!

Необъектно-ориентированное решение

import tkinter as tk
from tkinter.messagebox import showerror


if __name__ == '__main__':

    def bad():
        raise Exception("I'm Bad!")

    # any name as accepted but not signature
    def report_callback_exception(self, exc, val, tb):
        showerror("Error", message=str(val))

    tk.Tk.report_callback_exception = report_callback_exception
    # now method is overridden

    app = tk.Tk()
    tk.Button(master=app, text="bad", command=bad).pack()
    app.mainloop()

Объектно-ориентированное решение

import tkinter as tk
from tkinter.messagebox import showerror


class Bad(tk.Tk):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # or tk.Tk.__init__(*args, **kwargs)

        def bad():
            raise Exception("I'm Bad!")
        tk.Button(self, text="bad", command=bad).pack()

    def report_callback_exception(self, exc, val, tb):
        showerror("Error", message=str(val))

if __name__ == '__main__':

    app = Bad()
    app.mainloop()

Результат

Моя среда:

Python 3.5.1 |Anaconda 2.4.1 (64-bit)| (default, Dec  7 2015, 15:00:12) [MSC  
v.1900 64 bit (AMD64)] on win32

tkinter.TkVersion
8.6

tkinter.TclVersion
8.6
person Maxim Kochurov    schedule 28.01.2016
comment
Если вы хотите сообщить о полной трассировке ошибки, используйте message = traceback.format_exc() (заслуга Джеймса) - person Stevoisiak; 02.03.2018

Я не очень уверен, правильно ли я вас понял, но этот простой код дает вам контроль над случаем, когда каталог не может быть найден:

import os
from Tkinter import *

def copydir():
    src = "D:\\troll"
    dest = "D:\\trollo"

    try:
        os.rename(src, dest)
    except:
        print 'Sorry, I couldnt rename'
        # optionally: raise YourCustomException
        # or use a Tkinter popup to let the user know

master = Tk()

b = Button(master, text="OK", command=copydir)
b.pack()

mainloop()

EDIT: поскольку вам нужен общий метод, а Tkinter не распространяет исключения, вам нужно его запрограммировать. Есть два способа:

1) Жестко закодировать его в функции, как я сделал в примере выше (ужасно)

2) Используйте декоратор, чтобы добавить блок try-except.

import os
from Tkinter import *


class ProvideException(object):
    def __init__(self, func):
        self._func = func

    def __call__(self, *args):

        try:
            return self._func(*args)

        except Exception, e:
            print 'Exception was thrown', str(e)
            # Optionally raise your own exceptions, popups etc


@ProvideException
def copydir():
    src = "D:\\troll"
    dest = "D:\\trollo"

    os.rename(src, dest)

master = Tk()

b = Button(master, text="OK", command=copydir)
b.pack()

mainloop()

EDIT: Если вы хотите включить стек

include traceback

и в блоке кроме:

except Exception, e:
    print 'Exception was thrown', str(e)
    print traceback.print_stack()

Решение, предложенное А.Родасом, чище и полнее, но сложнее для понимания.

person bgusach    schedule 06.03.2013
comment
На самом деле я ищу универсальное решение для обработки исключений в случае приложения Tkinter. Как и в этом случае, я знаю об этом решении. Что я хочу знать, так это то, что если есть способ, с помощью которого любое исключение, вызванное в приложении Python Tkinter, будет выгружено в текстовый файл. - person sarbjit; 06.03.2013
comment
@sarbjit Я расширил свое решение. Проверьте это, теперь я понимаю, что вы хотели, и я почти уверен, что это поможет. - person bgusach; 06.03.2013
comment
Спасибо за решение, это то, что я ищу. Поскольку я изучаю Python, у меня мало сомнений, и я надеюсь, что вы поможете прояснить: 1) Когда вы использовали @ProvideException в коде, что это означает, как мы называем это в Python (я хочу прочитать об этом). 2) Можно ли с помощью этого метода сбросить полную трассировку стека. Поэтому я хочу, чтобы всякий раз, когда возникает какое-либо исключение, оно должно быть сброшено в текстовый файл. На данный момент он показывает только строковое сообщение об ошибке. Кроме того, это будет обрабатывать все виды исключений, верно? - person sarbjit; 06.03.2013
comment
@ProvideException — это декоратор, который состоит из отправки вашей функции в другую функцию и работы с результатом этой (которая является другой функцией). В этом случае copydir отправляется в класс ProvideException и сохраняется как внутренняя переменная, а затем вызывается, что запускает метод call, но добавляет блок try-except. Я обновлю ответ. - person bgusach; 06.03.2013
comment
Если я использую декоратор в своем коде, будет ли он применим ко всем функциям в программе (каждая функция проходит через этот декортар) или только к функции, над которой используется этот декоратор? - person sarbjit; 06.03.2013
comment
В этом смысл использования декораторов. Вам просто нужно добавить @DecoratorName перед каждым определением подпрограммы, и все готово =). Нет необходимости жестко кодировать каждую функцию. - person bgusach; 06.03.2013

Вы можете установить глобальный обработчик исключений с помощью exclude-hook(). Пример можно найти здесь.

person krase    schedule 06.03.2013