【问题标题】:Altering traceback of a non-callable module更改不可调用模块的回溯
【发布时间】:2018-12-07 18:28:42
【问题描述】:

我是一个包的小贡献者,人们应该这样做(Foo.Bar.Bar 是一个类):

>>> from Foo.Bar import Bar
>>> s = Bar('a')

有时人们会错误地这样做(Foo.Bar 是一个模块):

>>> from Foo import Bar   
>>> s = Bar('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'module' object is not callable

这可能看起来很简单,但用户仍然无法调试它,我想让它更容易。我无法更改 Foo 或 Bar 的名称,但我想添加一个信息更丰富的回溯,例如:

TypeError("'module' object is not callable, perhaps you meant to call 'Bar.Bar()'")

我阅读了Callable modules 问答,我知道我不能将__call__ 方法添加到模块中(而且我不想仅仅为此将整个模块包装在一个类中)。无论如何,我不希望模块是可调用的,我只想要一个自定义回溯。是否有适用于 Python 3.x 和 2.7+ 的干净解决方案?

【问题讨论】:

  • 我知道您说过您不能重命名模块,但这确实是最佳解决方案。将Foo 重命名为foo 并将Bar(模块)重命名为bar 将在很大程度上表明它们不是类,并且人们不太可能将bar 与Bar 混淆。
  • @Aran-Fey 是的,但这里的遗产很重要,如果我更改名称,很多代码会中断,我很确定这是不允许的
  • 您可以重命名它并在一段时间内提供一个已弃用的向后兼容解决方案。这样你就可以修复你的教程并让新咖啡更明显,而不会在没有警告的情况下破坏旧代码。尽管没有可调用的方法,但不知道在这里更改消息。
  • 虽然我知道这不是你想要的,也可能没有帮助,但我支持@Aran-Fey 和@Voo。 Foo 和 Bar 的命名带来了混淆,它们应该在 PEP8 之后的 snake_case 中。尝试调用小写名称来实例化对象会立即让您觉得有点奇怪,至少对于 Python 的非初学者而言。

标签: python python-3.x python-2.7 exception module


【解决方案1】:

您想要的是在向用户显示时更改错误消息。一种方法是定义自己的excepthook。

你自己的函数可以:

  • 在回溯对象中搜索调用框架(其中包含有关TypeError 异常的信息以及执行此操作的函数),
  • 在局部变量中搜索Bar对象,
  • 如果对象是模块而不是类或函数,则更改错误消息。

在Foo.__init__.py 你可以安装一个你的excepthook

import inspect
import sys


def _install_foo_excepthook():
    _sys_excepthook = sys.excepthook

    def _foo_excepthook(exc_type, exc_value, exc_traceback):
        if exc_type is TypeError:
            # -- find the last frame (source of the exception)
            tb_frame = exc_traceback
            while tb_frame.tb_next is not None:
                tb_frame = tb_frame.tb_next

            # -- search 'Bar' in the local variable
            f_locals = tb_frame.tb_frame.f_locals
            if 'Bar' in f_locals:
                obj = f_locals['Bar']
                if inspect.ismodule(obj):
                    # -- change the error message
                    exc_value.args = ("'module' object is not callable, perhaps you meant to call 'Foo.Bar.Bar()'",)

        _sys_excepthook(exc_type, exc_value, exc_traceback)

    sys.excepthook = _foo_excepthook


_install_foo_excepthook()

当然,你需要强制执行这个算法……

使用以下演示:

# coding: utf-8

from Foo import Bar

s = Bar('a')

你得到:

Traceback (most recent call last):
  File "/path/to/demo_bad.py", line 5, in <module>
    s = Bar('a')
TypeError: 'module' object is not callable, perhaps you meant to call 'Foo.Bar.Bar()'

【讨论】:

  • 谢谢,这是有道理的,尽管对于潜在问题来说感觉有点沉重。是否需要指定编码?
  • @Chris_Rands:不,这是复制/粘贴错误。在 Python 2.7 上它可能很有用,但 Python Exception 不能很好地支持 unicode 字符串:选择 ASCII 格式的消息。
【解决方案2】:

将此添加到Bar.py 的顶部:(基于this question)

import sys

this_module = sys.modules[__name__]
class MyModule(sys.modules[__name__].__class__):
    def __call__(self, *a, **k):  # module callable
        raise TypeError("'module' object is not callable, perhaps you meant to call 'Bar.Bar()'")
    def __getattribute__(self, name):
        return this_module.__getattribute__(name)

sys.modules[__name__] = MyModule(__name__)


# the rest of file
class Bar:
    pass

注意:使用 python3.6 和 python2.7 测试。

【讨论】:

  • 这仅适用于 Python 3.5 及更高版本,因为它依赖于在 3.5 中添加的 special case,该 special case 将模块单选为在 C 中实现的唯一可以重新分配其 __class__ 的对象。跨度>
  • 现在,这段代码也适用于python2.7。 :) @user2357112
  • @user2357112 你会推荐一个特定的解决方案吗?
  • @Chris_Rands:我会接受默认的错误消息。有很多方法可以更改错误消息,但每种方法都有微妙的副作用和额外的混乱来源。例如,如果在包的__init__.py 中使用,此答案中的(当前)代码会破坏子模块。您当前的问题很容易理解和解释。尝试更改错误消息给自己带来的问题可能更难理解和解释。
  • @Chris_Rands:Laurent LAPORTE 的答案中的异常钩子非常有问题,只检查局部变量,并没有尝试检查 Bar 模块是否实际上是作为函数调用的,而不是其他触发类型错误。此外,它可能会干扰其他 excepthook 替换,并且与 IPython 不兼容。
【解决方案3】:

有很多方法可以获得不同的错误消息,但它们都有奇怪的警告和副作用。

  • 将模块的 __class__ 替换为 types.ModuleType 子类可能是最简洁的选择,但它仅适用于 Python 3.5+。

    除了 3.5+ 的限制之外,我认为此选项的主要奇怪副作用是模块将被报告为可被 callable 函数调用,并且重新加载模块将再次替换其类,除非你'请注意避免这种双重替换。

  • 用不同的对象替换模块对象适用于 3.5 之前的 Python 版本,但要完全正确非常棘手。

    子模块、重新加载、全局变量、除自定义错误消息之外的任何模块功能......如果您错过了实现的一些微妙方面,所有这些都可能会中断。此外,该模块将被报告为可由callable 调用,就像__class__ 替换一样。

  • 在引发异常后尝试修改异常消息是可能的,例如在sys.excepthook 中,但是没有一个好的方法来判断任何特定的TypeError 来自尝试将您的模块调用为函数。

    你能做的最好的可能是在命名空间中检查 TypeError 和 'module' object is not callable 消息,在该命名空间中,你的模块看起来很可能被调用 - 例如,如果 Bar 名称被绑定到框架的局部变量或全局变量中的Foo.Bar 模块 - 但这仍然会有大量的误报和误报。此外,sys.excepthook 替换与 IPython 不兼容,您使用的任何机制都可能与某些东西发生冲突。

现在,您遇到的问题很容易理解和解释。任何尝试更改错误消息时遇到的问题可能更难以理解和解释。这可能不是一个值得的权衡。

【讨论】:

    猜你喜欢
    • 2012-12-22
    • 1970-01-01
    • 2013-01-22
    • 1970-01-01
    • 2018-05-04
    • 1970-01-01
    • 2020-05-02
    • 2021-12-28
    • 2020-01-31
    相关资源
    最近更新 更多