【问题标题】:Exception behaviour in Python __set_name__Python __set_name__ 中的异常行为
【发布时间】:2020-05-27 10:27:02
【问题描述】:

我有一个使用 Python 3.6+ __set_name__ 的子类,以确保拥有的类已经注释了承载子类的字段的类型。如果他们没有引发异常。

但是,引发的任何异常总是被 Python 捕获,并改为引发 RuntimeError

例如:

class Child:
    def __set_name__(self, owner, name):
        raise Exception("OOPS!")

class Owner():
    child = Child()

结果:

Traceback (most recent call last):
  File "<stdin>", line 3, in __set_name__
Exception: OOPS!

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Error calling __set_name__ on 'Child' instance 'child' in 'Owner'

这很可能是预期的行为(找不到对__set_name__ 异常的具体引用),但也可能表明预期__set_name__ 永远不会遭受异常。

我看到的行为不是问题,因为在正确的条件下会发生异常。但是,由于我不能确定引发的异常是我的代码引发的异常,因此测试起来很棘手。

有没有更好的方法来引发一个适合测试的异常,或者确实有一种简单的方法来检查由RuntimeError 包装的异常确实是我的代码引发的那个?

【问题讨论】:

  • 我真的认为这是对 __set_name__ 的滥用,它是描述符协议的一部分...可能有更好的方法来确保注释,例如使用静态分析工具,例如 mypy >
  • 注意,我不认为这是被包装的。基本上,我敢打赌type 中有一个很大的try:... except Exception as e:,如果任何属于类创建的一部分引发错误,它只会引发此运行时错误。我找不到任何官方文档,这可能都是实现细节......
  • 你应该可以访问 RuntimeError 的__cause__ 来测试它是否是你引发的异常。
  • @juanpa.arrivillaga - 你确实可以认为这是对__set_name__ 的滥用,除了没有记录的异常不应该被提出......我正在寻找一个纯Python解决方案,所以在这个阶段不要选择采用 mypy。

标签: python python-3.x exception python-descriptors


【解决方案1】:

所以,既然你得到了整个“上述异常是以下异常的直接原因”,这意味着在type(基本元类)中的某个地方基本上有一些影响:

try:
    descr.__set_name__(A, 'attr')
except Exception as e:
    raise RuntimeError(msg) from e

也就是说,它使用了raise new_exception from original_exception,所以你应该能够反省原来的异常是使用__cause__属性:

所以,观察:

In [1]: class Child:
   ...:     def __set_name__(self, owner, name):
   ...:         raise Exception("OOPS!")
   ...: try:
   ...:     class Owner():
   ...:         child = Child()
   ...: except RuntimeError as e:
   ...:     err = e
   ...:

In [2]: err
Out[2]: RuntimeError("Error calling __set_name__ on 'Child' instance 'child' in 'Owner'")

In [3]: err.__cause__
Out[3]: Exception('OOPS!')

同样,我认为没有任何文档记录,因此您可能依赖于实现细节。

Here is a link to the documentation 更详细地解释了这一点。

【讨论】:

    【解决方案2】:

    您可以访问包装异常上的__cause__ 属性,以检查这是否由于您引发的异常而发生:

    try:
        class Child:
            def __set_name__(self, owner, name):
                raise Exception("OOPS!")
    
        class Owner():
            child = Child()
    
    except RuntimeError as rte:
        assert rte.__cause__.args[0] == "OOPS!"  # or a more appropriate check
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-05-29
      • 2015-07-01
      • 2018-03-26
      • 1970-01-01
      • 2018-05-28
      • 1970-01-01
      • 1970-01-01
      • 2014-10-23
      相关资源
      最近更新 更多