【问题标题】:Check if something is raisable in any version检查是否在任何版本中都可以提高
【发布时间】:2018-03-21 02:26:51
【问题描述】:

我正在从事一个项目,我们希望验证是否可以在必要时将参数作为异常引发。我们选择了以下内容:

def is_raisable(exception):
    funcs = (isinstance, issubclass)
    return any(f(exception, BaseException) for f in funcs)

这处理以下用例,满足我们的需求(目前):

is_raisable(KeyError) # the exception type, which can be raised
is_raisable(KeyError("key")) # an exception instance, which can be raised

但是,对于在旧版本 (2.x) 中可以引发的旧样式类,它会失败。然后我们尝试以这种方式解决它:

IGNORED_EXCEPTIONS = [
    KeyboardInterrupt,
    MemoryError,
    StopIteration,
    SystemError,
    SystemExit,
    GeneratorExit
]
try:
    IGNORED_EXCEPTIONS.append(StopAsyncIteration)
except NameError:
    pass
IGNORED_EXCEPTIONS = tuple(IGNORED_EXCEPTIONS)

def is_raisable(exception, exceptions_to_exclude=IGNORED_EXCEPTIONS):

    funcs_to_try = (isinstance, issubclass)
    can_raise = False

    try:
        can_raise = issubclass(exception, BaseException)
    except TypeError:
        # issubclass doesn't like when the first parameter isn't a type
        pass

    if can_raise or isinstance(exception, BaseException):
        return True

    # Handle old-style classes
    try:
        raise exception
    except TypeError as e:
        # It either couldn't be raised, or was a TypeError that wasn't 
        # detected before this (impossible?)
        return exception is e or isinstance(exception, TypeError)
    except exceptions_to_exclude as e:
        # These are errors that are unlikely to be explicitly tested here,
        # and if they were we would have caught them before, so percolate up
        raise
    except:
        # Must be bare, otherwise no way to reliably catch an instance of an
        # old-style class
        return True

这通过了我们所有的测试,但它不是很漂亮,如果我们正在考虑一些我们不希望用户传递的东西,但它仍然会让人觉得很笨拙,但无论如何可能会被扔进去原因。

def test_is_raisable_exception(self):
    """Test that an exception is raisable."""

    self.assertTrue(is_raisable(Exception))

def test_is_raisable_instance(self):
    """Test that an instance of an exception is raisable."""

    self.assertTrue(is_raisable(Exception()))

def test_is_raisable_old_style_class(self):
    """Test that an old style class is raisable."""

    class A: pass

    self.assertTrue(is_raisable(A))

def test_is_raisable_old_style_class_instance(self):
    """Test that an old style class instance is raisable."""

    class A: pass

    self.assertTrue(is_raisable(A()))

def test_is_raisable_excluded_type_background(self):
    """Test that an exception we want to ignore isn't caught."""

    class BadCustomException:
        def __init__(self):
            raise KeyboardInterrupt

    self.assertRaises(KeyboardInterrupt, is_raisable, BadCustomException)

def test_is_raisable_excluded_type_we_want(self):
    """Test that an exception we normally want to ignore can be not
    ignored."""

    class BadCustomException:
        def __init__(self):
            raise KeyboardInterrupt

    self.assertTrue(is_raisable(BadCustomException, exceptions_to_exclude=()))

def test_is_raisable_not_raisable(self):
    """Test that something not raisable isn't considered rasiable."""

    self.assertFalse(is_raisable("test"))

不幸的是,我们需要继续支持 Python 2.6+(很快将只支持 Python 2.7,所以如果您有一个在 2.6 中不起作用的解决方案,那很好,但并不理想)和 Python 3.x。理想情况下,我希望在没有明确测试版本的情况下这样做,但如果没有其他方法可以做到这一点。

最后,我的问题是:

  1. 有没有更简单的方法可以做到这一点并支持所有列出的版本?
  2. 如果没有,是否有更好或更安全的方法来处理“特殊异常”,例如KeyboardInterrupt
  3. 作为最 Pythonic 的人,我想请求宽恕而不是许可,但考虑到我们可以获得两种类型的 TypeError(一种是因为它有效,一种是因为它没有),这也让人感觉很奇怪(但无论如何我都必须依靠它来获得 2.x 支持)。

【问题讨论】:

  • "old-style classes [...] are raisable" 不在任何版本中,只有旧版本。
  • @IgnacioVazquez-Abrams 我知道,但不清楚。我会编辑。
  • 无论如何,我不会担心它不是BaseException的后代;当代码移植到 3.x 时,它必须是 1。
  • @Dannnno 啊,在问题中,我只是没有仔细阅读。那好吧。我认为我的回答的精神——你不需要所有那些仔细的 isinstance/issubclass 检查——仍然是正确的,但我可能在不相关的细节上花费了更多的精力而不是重要的东西。当我再次来到电脑前时,我会检查一下。对此感到抱歉。
  • 顺便说一句,我喜欢关于异常的 2.7 参考文档为您提供了有关 1.4 和 1.5 之间更改的所有详细信息,但除了 try/ 之外,没有告诉您所有后续更改何时发生除了/最终在 2.5 中合并。何时禁止引发非类型异常?介于 1.0 和 2.6 之间,我猜你只需要知道这些……

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


【解决方案1】:

你在 Python 中测试大多数东西的方式是 try 然后看看你是否得到了异常。

这适用于raise。如果某些东西不能被提升,你会得到一个TypeError;否则,您将得到您提出的(或您提出的实例)。这适用于 2.6(甚至 2.3)和 3.6。在 2.6 中作为异常的字符串将是可引发的;在 3.6 中不继承自 BaseException 的类型将不可引发;等等——你会得到所有事情的正确结果。无需检查BaseException 或以不同方式处理旧式和新式类;让raise 做它该做的事情。

当然,我们确实需要特例 TypeError,因为它会出现在错误的位置。但是由于我们不关心 pre-2.4,所以没有必要比isinstanceissubclass 测试更复杂;除了返回 False 之外,没有任何奇怪的对象可以做任何事情。一个棘手的问题(我最初弄错了;感谢 user2357112 抓住它)是您必须先进行 isinstance 测试,因为如果对象是 TypeError 实例,issubclass 将引发 TypeError ,所以我们需要短路并返回 True 而不要尝试。

另一个问题是处理我们不想意外捕获的任何特殊异常,例如KeyboardInterruptSystemError。但幸运的是,these all go back to before 2.6isinstance/issubclassexcept clauses(只要你不关心捕获异常值,我们不关心)都可以使用在 3.x 中也可以使用的语法获取元组。由于要求我们为这些情况返回True,因此我们需要在尝试提高它们之前对其进行测试。但它们都是BaseException 的子类,所以我们不必担心经典类或类似的东西。

所以:

def is_raisable(ex, exceptions_to_exclude=IGNORED_EXCEPTIONS):
    try:
        if isinstance(ex, TypeError) or issubclass(ex, TypeError):
            return True
    except TypeError:
        pass
    try:
        if isinstance(ex, exceptions_to_exclude) or issubclass(ex, exceptions_to_exclude):
            return True
    except TypeError:
        pass
    try:
        raise ex
    except exceptions_to_exclude:
        raise
    except TypeError:
        return False
    except:
        return True

这并没有通过您编写的测试套件,但我认为这是因为您的某些测试不正确。我假设您希望 is_raisable 对于在当前 Python 版本中可提升的对象为真,而不是在任何支持的版本中可提升的对象 即使它们是'在当前版本中不可提升。您不希望 is_raisable('spam') 在 3.6 中返回 True 然后尝试 raise 'spam' 会失败,对吧?所以,在我脑海中浮现:

  • not_raisable 测试引发了一个字符串 - 但这些在 2.6 中可以引发。
  • excluded_type 测试引发了一个类,Python 2.x 可以通过实例化该类来处理该类,但这不是必需的,CPython 2.6 具有将在这种情况下触发的优化.
  • old_style 测试在 3.6 中引发了新式类,它们不是 BaseException 的子类,因此它们不可引发。

如果不为 2.6、3.x 甚至 2.7 甚至可能为两个 2.x 版本的不同实现编写单独的测试,我不确定如何编写正确的测试(尽管您可能没有有没有用户,比如 Jython?)。

【讨论】:

  • 除非你在错误的时间得到了键盘中断。然后事情变得很奇怪。
  • 获得类似KeyboardInterrupt 的东西是我不喜欢裸露的except 的原因,我认为其中一些额外的复杂性出现了。编辑:忍者!
  • 我不知道特殊情况类型 TypeError 实例!吓人!
  • @Dannnno 重写之后,它与您的代码并没有我预期的那么不同。尽管我认为不需要任何BaseException 测试或专门处理旧式类,它仍然更简单且不那么hacky。它可以正确处理字符串。
  • issubclass(ex, TypeError) 将在ex 不是类including when ex is an instance of TypeError 时立即引发异常。 issubclass(ex, exceptions_to_exclude) 也有类似的问题。
【解决方案2】:

您可以引发对象,捕获异常,然后使用is 关键字检查引发的异常是对象还是对象的实例。如果引发了其他任何问题,则为TypeError,表示该对象不可引发。

此外,要处理任何可提升的对象,我们可以使用sys.exc_info。这也将捕获诸如KeyboardInterrupt 之类的异常,但如果与参数的比较不确定,我们可以重新引发它们。

import sys

def is_raisable(obj):
    try:
        raise obj
    except:
        exc_type, exc = sys.exc_info()[:2]

        if exc is obj or exc_type is obj:
            return True
        elif exc_type is TypeError:
            return False
        else:
            # We reraise exceptions such as KeyboardInterrupt that originated from outside
            raise

is_raisable(ValueError) # True
is_raisable(KeyboardInterrupt) # True
is_raisable(1) # False

【讨论】:

  • 您的文字显示“如果有人提出其他问题,那就是ValueError”您确定这是对的吗? (如果是这样,我认为你的代码是错误的——我的也是。)
  • 无论如何,您似乎可以安全地跳过对TypeError 的预检查,通过这种方式翻转逻辑,这是一个很好的简化。
  • 上面的文字可能需要改写。但我不确定这里没有涵盖什么情况。我们要么提出了我们的对象/类,要么得到了 TypeError,要么得到了外部的异常(KeyboardInterrupt 等)。我错过了什么吗?
  • 当我在 2.6 或 3.6 中 raise 3 时,我得到的是 TypeError,而不是 ValueError。 (而且我认为对于任何其他不可提升的值都是如此,在 2.7 中也是如此,并且由文档保证。)您的代码似乎假设它将是 TypeError,而不是 ValueError。我的代码肯定是这样假设的。
  • 糟糕!这是我的错。我真的是说 TypeError,这甚至是我在代码中写的
【解决方案3】:

如果您想检测旧式类和实例,只需对它们进行显式检查:

import types

if isinstance(thing, (types.ClassType, types.InstanceType)):
    ...

您可能希望将其包装在某种版本检查中,这样它就不会在 Python 3 上失败。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-26
    • 1970-01-01
    • 2022-08-13
    相关资源
    最近更新 更多