重定向sys.stdout到Tkinter.Text小部件时段错误

7 投票
2 回答
5623 浏览
提问于 2025-04-15 23:11

我正在用Python和Tkinter开发一个图形界面的应用程序,这个程序是基于现有的Python bdb模块的。在这个应用中,我想把控制台的所有输出(包括正常输出和错误输出)都静音,然后把它们重定向到我的图形界面里。为此,我写了一个特别的Tkinter.Text对象(代码在帖子最后)。

基本的想法是,当有东西写入sys.stdout时,它会以黑色的形式显示在“Text”中。如果有东西写入sys.stderr,它会以红色的形式显示在“Text”中。每当有新内容写入时,Text会自动向下滚动,以便查看最新的内容。

目前我使用的是Python 2.6.1。在Mac OS X 10.5上,这个功能运行得很好,我没有遇到任何问题。然而,在RedHat Enterprise Linux 5上,我在运行脚本时经常会遇到“段错误”。这个段错误并不总是在同一个地方出现,但几乎总是会发生。如果我把代码中的sys.stdout=sys.stderr=那两行注释掉,段错误似乎就消失了。

我相信还有其他解决办法,我可能需要尝试,但有没有人能指出我这里做错了什么,导致这些段错误的发生?这让我很头疼。谢谢!

附言 - 我知道把sys.stderr重定向到图形界面可能不是个好主意,但即使我只重定向sys.stdout而不重定向sys.stderr,仍然会出现段错误。我也意识到目前我让Text对象无限增长。

class ConsoleText(tk.Text):
    '''A Tkinter Text widget that provides a scrolling display of console
    stderr and stdout.'''

    class IORedirector(object):
        '''A general class for redirecting I/O to this Text widget.'''
        def __init__(self,text_area):
            self.text_area = text_area

    class StdoutRedirector(IORedirector):
        '''A class for redirecting stdout to this Text widget.'''
        def write(self,str):
            self.text_area.write(str,False)

    class StderrRedirector(IORedirector):
        '''A class for redirecting stderr to this Text widget.'''
        def write(self,str):
            self.text_area.write(str,True)

    def __init__(self, master=None, cnf={}, **kw):
        '''See the __init__ for Tkinter.Text for most of this stuff.'''

        tk.Text.__init__(self, master, cnf, **kw)

        self.started = False
        self.write_lock = threading.Lock()

        self.tag_configure('STDOUT',background='white',foreground='black')
        self.tag_configure('STDERR',background='white',foreground='red')

        self.config(state=tk.DISABLED)

    def start(self):

        if self.started:
            return

        self.started = True

        self.original_stdout = sys.stdout
        self.original_stderr = sys.stderr

        stdout_redirector = ConsoleText.StdoutRedirector(self)
        stderr_redirector = ConsoleText.StderrRedirector(self)

        sys.stdout = stdout_redirector
        sys.stderr = stderr_redirector

    def stop(self):

        if not self.started:
            return

        self.started = False

        sys.stdout = self.original_stdout
        sys.stderr = self.original_stderr

    def write(self,val,is_stderr=False):

        #Fun Fact:  The way Tkinter Text objects work is that if they're disabled,
        #you can't write into them AT ALL (via the GUI or programatically).  Since we want them
        #disabled for the user, we have to set them to NORMAL (a.k.a. ENABLED), write to them,
        #then set their state back to DISABLED.

        self.write_lock.acquire()
        self.config(state=tk.NORMAL)

        self.insert('end',val,'STDERR' if is_stderr else 'STDOUT')
        self.see('end')

        self.config(state=tk.DISABLED)
        self.write_lock.release()

2 个回答

4

我假设这是一个更大、更复杂的程序的一部分。

与其使用锁,不如让你的代码写入一个线程安全的队列对象。然后,在你的主线程中,你可以定期检查这个队列,并将内容写入文本小部件。你可以通过事件循环来进行这个检查(而不是自己写一个循环),方法是运行一个检查任务,然后让它在几毫秒后自动重新安排自己运行(几百毫秒就足够了)。

4

好的,我终于找到了问题所在。我在最初开发代码的Mac OS X 10.5.8上从来没有遇到过这个问题。这个分段错误似乎只发生在RedHat Enterprise Linux 5上。

结果发现,这段代码是罪魁祸首:

def write(self,val,is_stderr=False):

        #Fun Fact:  The way Tkinter Text objects work is that if they're disabled,
        #you can't write into them AT ALL (via the GUI or programatically).  Since we want them
        #disabled for the user, we have to set them to NORMAL (a.k.a. ENABLED), write to them,
        #then set their state back to DISABLED.

        self.write_lock.acquire()
        self.config(state=tk.NORMAL)

        self.insert('end',val,'STDERR' if is_stderr else 'STDOUT')
        self.see('end')

        self.config(state=tk.DISABLED)
        self.write_lock.release()

我希望能解释一下为什么会出现分段错误,但我发现不断启用和禁用Text对象就是问题所在。如果我把上面的代码改成这样:

def write(self,val,is_stderr=False):

        self.write_lock.acquire()

        self.insert('end',val,'STDERR' if is_stderr else 'STDOUT')
        self.see('end')

        self.write_lock.release()

当我去掉self.config(state=...)的调用时,分段错误就消失了。调用self.config(state=...)的初衷是为了让用户无法编辑Text字段。不过,当Text字段处于tk.DISABLED状态时,调用self.insert(...)也无法正常工作。

我想出的解决办法是保持Text字段可用,但让它忽略所有键盘输入(这样用户尝试使用键盘时就会有只读的感觉)。实现这个的最简单方法是把__init__方法改成这样(把状态改为tk.NORMAL,并更改<Key>事件的绑定):

def __init__(self, master=None, cnf={}, **kw):
        '''See the __init__ for Tkinter.Text for most of this stuff.'''

        tk.Text.__init__(self, master, cnf, **kw)

        self.started = False
        self.write_lock = threading.Lock()

        self.tag_configure('STDOUT',background='white',foreground='black')
        self.tag_configure('STDERR',background='white',foreground='red')

        self.config(state=tk.NORMAL)
        self.bind('<Key>',lambda e: 'break') #ignore all key presses

希望这能帮助到遇到同样问题的人。

撰写回答