python – 检查任何版本中的某些东西是否可以升级

我正在开发一个项目,我们想要验证参数实际上可以在必要时作为异常引发.我们选择了以下内容:

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.理想情况下,我想在没有对版本进行明确测试的情况下这样做,但如果没有办法做到这一点,那么那就没关系了.

最终,我的问题是:

>有没有更简单的方法来支持所有列出的版本?
>如果没有,是否有更好或更安全的方式来处理“特殊例外”,例如:一个KeyboardInterrupt.
>要成为大多数Pythonic我想请求宽恕而不是许可,但考虑到我们可以得到两种类型的TypeError(一种因为它起作用,一种因为它没有),感觉也很奇怪(但我必须无论如何都要支持2.x支持).

最佳答案 在Python中测试大多数东西的方法是尝试然后看看你是否得到了异常.

这适用于加薪.如果某些东西不可升级,你会得到一个TypeError;否则,你会得到你所筹集的东西(或你所筹集的东西的实例).这将适用于2.6(甚至2.3)和3.6.作为2.6中的例外的字符串将是可升级的;不在3.6中从BaseException继承的类型将不可升级;等等 – 你得到了一切正确的结果.无需检查BaseException或以不同方式处理旧式和新式类;只是让加油做它做的事情.

当然我们需要特殊情况下的TypeError,因为它会落在错误的地方.但由于我们不关心2.4之前的版本,因此不需要比isinstance和issubclass测试更复杂的东西;除了返回False之外,没有任何奇怪的对象可以执行任何操作.一个棘手的位(我最初错了;感谢user2357112用于捕获它)是你必须首先进行isinstance测试,因为如果对象是TypeError实例,则issubclass会引发TypeError,所以我们需要短路并且在不尝试的情况下返回True.

另一个问题是处理我们不想意外捕获的任何特殊异常,例如KeyboardInterrupt和SystemError.但幸运的是,these all go back to before 2.6.isinstance/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,然后尝试引发’垃圾邮件’会失败,对吗?所以,在我的头顶:

> 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?).

点赞