我正在开发一个项目,我们想要验证参数实际上可以在必要时作为异常引发.我们选择了以下内容:
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
/issubclass
和except
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?).