如何阻止 Python 从其他库打印出忽略的异常?

How to stop Python from printing out ignored Exceptions from other libraries?

我对 Python3 处理特定异常的方式有点疑问,应该忽略该异常,但无论如何它都会在执行结束时连同 Traceback 打印出来。

具体来说,我使用 graph-tool 库来启动交互式 window,如下所示:

graph_tool.draw.interactive_window(self.graphtool_graph, vertex_text=self.v_label, vertex_font_size=6,geometry=(1920, 1080))

如果我关闭 打开的 window,然后 它完成绘制和布置图形本身,就会出现我的问题。然后,当我的代码执行完成时,我得到这个打印输出:

Exception ignored in: <bound method GraphWindow.__del__ of <gtk_draw.GraphWindow object at 0x7f221ccf3750 (graph_tool+draw+gtk_draw+GraphWindow at 0x4c662a0)>>
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/graph_tool/draw/gtk_draw.py", line 1183, in __del__
  File "/usr/lib/python3/dist-packages/graph_tool/draw/gtk_draw.py", line 375, in cleanup
  File "/usr/lib/python3/dist-packages/gi/overrides/__init__.py", line 68, in __get__
TypeError: 'NoneType' object is not callable

这显然不是很好看,尤其是当最终用户与我的项目进行交互时。

我试图将对 draw.interactive_window 的调用包含在 try-catch 语句中,如下所示:

        try:
            graph_tool.draw.interactive_window(self.graphtool_graph, vertex_text=self.v_label, vertex_font_size=6,
                                               geometry=(1920, 1080))
        except TypeError:
            # Interactive Window closed before it finished drawing the graph. It would throw an exception that is
            # quite ugly to see. Let's ignore it.
            return

但我遇到了同样的问题。我什至尝试不指定 TypeError 异常并使用毯子 except,但无济于事。

有谁知道阻止Python打印出这个异常的方法吗?

P.S.: 我在 Python 错误跟踪器上发现了这个 issue,这似乎是相关的。在随附的讨论中,这被描述为一个功能™而不是一个错误,但我仍然想了解是否有可能以任何方式阻止打印此异常,尤其是当我明确地试图捕获它并忽略它时。

您的问题来自 __del__ 方法调用中发生的异常。作为 documented here

Due to the precarious circumstances under which del() methods are invoked, exceptions that occur during their execution are ignored, and a warning is printed to sys.stderr instead

顺便说一下,这就是为什么你的 except 块不起作用的原因...

你可以用这个 mcve 验证这一点:

# warntest.py

class Foo(object):
    def __del__(self):
        raise ValueError("YADDA")

f = Foo()

然后

 $ python3 warntest.py 
Exception ignored in: <bound method Foo.__del__ of <__main__.Foo object at 0x7efcb61dc898>>
Traceback (most recent call last):
  File "warntest.py", line 3, in __del__
ValueError: YADDA

尽管 "a warning is printed" 可能会提出什么建议,但令我非常沮丧的是,只是询问 Python 到 silence warnings 并没有改变任何东西 - python3 -Wignore warntest.py 表现相同,并在脚本中手动设置 "ignore" 过滤器并没有做更多。

IOW,恐怕这里没有简单而干净的解决方案。这给您留下了三个可能的选择:

1/ 从源头上解决问题。 graph-tool 是 OSS,您可以通过改进这些 __del__ 方法来做出贡献,这样它们就不会引发任何异常。

2/ 使用 hack。一个可能的方法是用 contextlib.redirect_stderr 管理器包装这个调用——我试过了,它按预期工作,但它绝对会阻止任何东西在这个调用期间到达 stderr。

3/ 接受它...

编辑

I tried looking into the source of g-t and cannot find where this exception is raised

它在您发布的回溯中以纯文本形式写入...实际上,异常本身在 gi/overrides/__init__.py 中引发,但这不是重点 - 您想要的是编辑 GraphWindow.__del__(并且GraphWidget.__del__ 太 FWIW) 包装 self.graph.cleanup() ( GraphWidget 中的 self.cleanup() 调用最粗略的 try/except 块。下面的 MCVE 重现了这个问题:

class Sub(object):
    def cleanup(self):
        raise ValueError("YADDA")

    def __del__(self):
        self.cleanup()

class Main(object):
    def __init__(self):
        self.sub = Sub()

    def __del__(self):
        self.sub.cleanup()

Main()

修复非常简单:

class Sub(object):
    def cleanup(self):
        raise ValueError("YADDA")

    def __del__(self):
        # yes, a bare except clause and a pass...
        # this is exactly what you're NOT supposed to do,
        # never ever, because it's BAD... but here it's
        # ok - provided you double-checked what the code
        # in the `try` block really do, of course.
        try:
            self.cleanup()
        except:
            pass

class Main(object):
    def __init__(self):
        self.sub = Sub()

    def __del__(self):
        try:
            self.sub.cleanup()
        except:
             pass

重要说明:这种异常处理程序(一个空的 except 子句后跟一个 pass正是什么人们 永远、永远 都不应该这样做。这是最可怕的异常处理反模式。但是__del__确实是个特例,而且在现在的情况下the cleanup() method only tries to unregister a callback function所以真的是无伤大雅。

but even when redirecting the output to a file

请注意,您要重定向 stderr,而不是 stdout。以下代码段对我有用:

import contextlib

class Foo(object):
    def __del__(self):
        raise ValueError("YADDA")

def test():    
    Foo()

with contextlib.redirect_stderr(io.StringIO()):
    test()