检查任何版本

时间:2018-03-21 02:26:51

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

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

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支持)。

3 个答案:

答案 0 :(得分:3)

你在Python中测试大多数东西的方法是try,然后看看你是否得到了例外。

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

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

另一个问题是处理我们不想意外捕获的任何特殊例外,例如TrueKeyboardInterrupt。但幸运的是,these all go back to before 2.6。并且isinstance/issubclassexcept clauses(只要你不关心捕获异常值,我们没有)可以使用在3.x中工作的语法的元组。由于我们需要为这些情况返回SystemError,因此我们需要在尝试提升它们之前对其进行测试。但它们都是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 对于当前Python版本中可升级的对象是真的,而不是任何支持版本中可升级的对象它们在当前版本中不会升级。您不希望is_raisable在3.6中返回is_raisable('spam'),然后尝试True会失败,对吧?所以,脱离我的头脑:

  • raise 'spam'测试会引发一个字符串 - 但这些字符串在2.6中可以升级。
  • not_raisable测试引发了一个类,Python 2.x 可以通过实例化类来处理它,但它不是必需的,并且CPython 2.6具有将触发的优化这种情况。
  • excluded_type测试会在3.6中引发新样式的类,而它们不是old_style的子类,所以它们不会升级。

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

答案 1 :(得分: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

答案 2 :(得分:1)

如果要检测旧式类和实例,只需对它们进行明确检查:

import types

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

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