在Python Tkinter中处理异常

6 投票
4 回答
22773 浏览
提问于 2025-04-17 18:07

我写了一个用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对话框中抛出异常。

谢谢你的帮助!!

4 个回答

3

我不太确定我是否理解你的意思,但这段简单的代码可以让你控制当找不到目录时的情况:

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

补充说明: 由于你想要一个通用的方法,而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()

补充说明: 如果你想包含堆栈信息

include traceback

并在except块中:

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

A.Rodas提出的解决方案更干净、更完整,但理解起来更复杂。

17

我看到你有一个非面向对象的例子,所以我来展示两种解决捕获异常问题的方法。

关键在于 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
5

你可以重写Tkinter中的CallWrapper。为了做到这一点,你需要使用命名导入的方式来引入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()

撰写回答