检查是否可以在任何版本中引发某些问题

Check if something is raisable in any version

我正在做一个项目,我们想要验证一个参数是否可以在必要时作为异常实际引发。我们采用以下方法:

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. 如果没有,是否有更好或更安全的方法来处理 "special exceptions",例如KeyboardInterrupt.
  3. 最 Pythonic 我想请求原谅而不是许可,但考虑到我们可以获得两种类型的 TypeError(一种因为有效,另一种因为无效't) 感觉也很奇怪(但为了 2.x 支持,我不得不退回到那个)。

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

这对 raise 很有效。如果无法筹集资金,您将获得 TypeError;否则,您将得到您筹集的资金(或您筹集的资金的一个实例)。这对 2.6(甚至 2.3)和 3.6 一样有效。字符串作为 2.6 中的异常将被引发;不从 3.6 中的 BaseException 继承的类型将不可提升;等等——你会得到正确的结果。无需检查 BaseException 或以不同方式处理 old-style 和 new-style class;就让 raise 做它做的吧。

当然我们确实需要 special-case TypeError,因为它会落在错误的地方。但是因为我们不关心 pre-2.4,所以不需要比 isinstanceissubclass 测试更复杂的东西;除了 return False 之外,没有任何奇怪的对象可以做任何事情了。一个棘手的问题(我最初弄错了;感谢 user2357112 抓住了它)是你必须先进行 isinstance 测试,因为如果对象是 TypeError 实例,issubclass 会提高 TypeError,所以我们需要 short-circuit 和 return True 而不是尝试。

另一个问题是处理我们不想意外捕获的任何特殊异常,例如 KeyboardInterruptSystemError。但幸运的是,these all go back to before 2.6. And both isinstance/issubclass and except clauses(只要您不关心捕获异常值,我们不关心)可以采用在 3.x 中也适用的语法的元组。由于这些情况要求我们 return True,因此我们需要在尝试提出它们之前对其进行测试。但它们都是 BaseException subclasses,所以我们不必担心 classic classes 或类似的东西。

所以:

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 版本 中可提升的对象 为真,而不是在 中可提升的对象任何受支持的版本,即使它们在当前版本中不可升级。您不希望在 3.6 中 is_raisable('spam') 到 return True 然后尝试 raise 'spam' 会失败,对吗?所以,在我的脑海中:

  • not_raisable 测试引发了一个字符串——但这些在 2.6 中是可以引发的。
  • excluded_type 测试引发了一个 class,Python 2.x 可以 通过实例化 class 来处理],但这不是必需的,并且 CPython 2.6 具有将在这种情况下触发的优化。
  • old_style 测试在 3.6 中提高 new-style classes,它们不是 BaseException 的子classes,所以它们是不可筹集。

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

如果你想检测 old-style 类 和实例,只需对它们进行显式检查:

import types

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

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

上失败

您可以引发对象,捕获异常,然后使用 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