关于python:条件检查与异常处理

Condition checking vs. Exception handling

本问题已经有最佳答案,请猛点这里访问。

什么时候处理异常比检查条件更可取?在很多情况下,我可以选择使用其中一个。

例如,这是一个使用自定义异常的求和函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# module mylibrary
class WrongSummand(Exception):
    pass

def sum_(a, b):
   """ returns the sum of two summands of the same type"""
    if type(a) != type(b):
        raise WrongSummand("given arguments are not of the same type")
    return a + b


# module application using mylibrary
from mylibrary import sum_, WrongSummand

try:
    print sum_("A", 5)
except WrongSummand:
    print"wrong arguments"

这是相同的函数,避免使用异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# module mylibrary
def sum_(a, b):
   """ returns the sum of two summands if they are both of the same type"""
    if type(a) == type(b):
        return a + b


# module application using mylibrary
from mylibrary import sum_

c = sum_("A", 5)
if c is not None:
    print c
else:
    print"wrong arguments"

我认为使用条件总是更易于阅读和管理。还是我错了?定义引发异常的API的适当情况是什么?为什么?


例外是更容易管理的,因为它们定义了可能出错的事情的一般系列。在您的示例中,只有一个可能的问题,因此使用异常没有优势。但是如果你有另一个类做除法,那么它需要发出信号,你不能被零除。仅仅返回EDOCX1[1]就不再有效了。

另一方面,异常可以被子类化,您可以捕获特定的异常,这取决于您对底层问题的关注程度。例如,您可以有一个DoesntCompute基异常和类似InvalidTypeInvalidArgument的子类。如果您只想得到一个结果,您可以将所有计算包装在一个捕获DoesntCompute的块中,但您仍然可以轻松地执行非常具体的错误处理。


通常,您希望使用条件检查来检查可以理解、预期和能够处理的情况。对于不连贯或不可处理的情况,您将使用异常。

所以,如果你想到你的"添加"功能。它不应该返回空值。这不是添加两个东西的一致结果。在这种情况下,传入的参数中存在错误,函数不应试图假装一切正常。这是抛出异常的完美案例。

如果在常规或正常的执行情况下,您将希望使用条件检查并返回空值。例如,IsEqual是使用条件的一个很好的例子,如果您的某个条件失败,则返回false。即。

1
2
3
4
5
6
7
8
9
10
function bool IsEqual(obj a, obj b)
{
   if(a is null) return false;
   if(b is null) return false;
   if(a.Type != b.Type) return false;

   bool result = false;
   //Do custom IsEqual comparison code
   return result;
}

在这种情况下,对于异常情况和"对象大小写不相等"都返回false。这意味着使用者(调用方)无法判断比较是否失败或对象是否不相等。如果需要区分这些情况,那么应该使用异常而不是条件。

最后,您想问问自己,消费者是否能够专门处理您遇到的故障案例。如果您的方法/函数不能做它需要做的事情,那么您可能想要抛出一个异常。


实际上,使用异常的问题在于业务逻辑。如果情况是例外(即根本不应该发生),则可以使用例外。但是,如果从业务逻辑的角度来看情况是可能的,那么在应该通过条件检查进行处理时,即使这种情况看起来更复杂。

例如,这里是我在准备语句中遇到的代码,开发人员在设置参数值(Java,而不是Python):

1
2
3
4
5
6
// Variant A
try {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId());
} catch (Exception e) {
  ps.setNull(1, Types.INTEGER);
}

通过条件检查,可以这样写:

1
2
3
4
5
6
7
8
// Variant B
if (enterprise != null && enterprise.getSubRegion() != null
  && enterprise.getSubRegion().getRegion() != null
  && enterprise.getSubRegion().getRegion().getCountry() != null) {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId());
} else {
  ps.setNull(1, Types.INTEGER);
}

从第一眼看,变体B似乎要复杂得多,但是,它是正确的,因为从商业的角度来看,这种情况是可能的(可能没有指定国家)。使用异常会导致性能问题,并且会导致对代码的误解,因为不清楚,国家是否可以接受为空。

在企业中使用辅助功能可以改进变量B,企业将立即返回地区和国家:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public RegionBean getRegion() {
  if (getSubRegion() != null) {  
    return getSubRegion().getRegion();
  } else {
    return null;
  }
}

public CountryBean getCountry() {
  if (getRegion() != null) {
    return getRegion().getCountry();
  } else {
    return null;
  }
}

这段代码使用了类似于链接的方法,并且每个get方法看起来足够简单,并且只使用了一个前置任务。因此,变量B可以改写如下:

1
2
3
4
5
6
// Variant C
if (enterprise != null && enterprise.getCountry() != null) {
  ps.setInt(1, enterprise.getCountry().getId());
} else {
  ps.setNull(1, Types.INTEGER);
}

另外,请阅读这篇Joel文章,了解为什么不应过度使用异常。还有陈瑞蒙的一篇文章。


我选择异常状态返回的主要原因是考虑如果程序员忘记做他的工作会发生什么。除了例外,您可能会忽略捕获异常。在这种情况下,您的系统将明显失败,您将有机会考虑在哪里添加捕获。对于状态返回,如果您忘记检查返回,它将被静默忽略,并且您的代码将继续运行,稍后可能会以一种神秘的方式失败。比起看不见的失败,我更喜欢看不见的失败。

还有其他原因,我在这里解释过:异常与状态返回。


如果你在问,你可能应该使用例外。例外是用来表示特殊情况的,这是一种特殊情况,在这种情况下,事情的工作方式与其他情况不同。这是一个很好的例子,几乎所有的错误,以及许多其他的事情。

在您的第二个sum_实现中,用户必须每次检查值是什么。这让人想起C/Fortran/其他语言样板文件(以及常见的错误源),其中错误代码未经检查是我们所避免的。您必须在所有级别编写这样的代码才能传播错误。它会变得混乱,尤其是在Python中避免。

其他几个注意事项:

  • 你通常不需要自己做例外。在许多情况下,像ValueErrorTypeError这样的内置异常是适当的。
  • 当我确实创建了一个新的异常,这非常有用,我经常尝试将比Exception更具体的子类化。这里是内置的异常层次结构。
  • 我绝不会实现像sum_这样的函数,因为类型检查会降低代码的灵活性、可维护性和惯用性。

    我只写函数

    1
    2
    def sum_(a, b):
        return a + b

    如果对象是兼容的,如果不是兼容的话,它就会抛出一个例外,每个人都习惯看到的TypeError。考虑我的实现是如何工作的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> sum_(1, 4)
    5
    >>> sum_(4.5, 5.0)
    9.5
    >>> sum_([1, 2], [3, 4])
    [1, 2, 3, 4]
    >>> sum_(3.5, 5) # This operation makes perfect sense, but would fail for you
    8.5
    >>> sum_("cat", 7) # This makes no sense and already is an error.
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
      File"<stdin>", line 1, in sum_
    TypeError: cannot concatenate 'str' and 'int' objects

    我的代码更短、更简单,但比您的代码更健壮、更灵活。这就是为什么我们避免在python中进行类型检查。


当参数包含意外值时,应引发异常。

在您的示例中,我建议在两个参数的类型不同时抛出异常。

抛出异常是一种很好的方法,可以在不弄乱代码的情况下中止服务。


也许单凭sum_看起来不错。如果,你知道,它真的被使用了呢?

1
2
3
4
#foo.py
def sum_(a, b):
    if type(a) == type(b):
        return a + b
1
2
3
4
#egg.py
from foo import sum_:
def egg(c = 5):
  return sum_(3, c)
1
2
3
4
5
6
#bar.py
from egg import egg
def bar():
  return len(egg("2"))
if __name__ =="__main__":
  print bar()

如果你运行bar.py,你会得到:

1
2
3
4
5
6
Traceback (most recent call last):
  File"bar.py", line 6, in <module>
    print bar()
  File"bar.py", line 4, in bar
    return len(egg("2"))
TypeError: object of type 'NoneType' has no len()

参见——通常人们调用一个函数的目的是对其输出进行操作。如果您只是"吞下"异常并返回一个虚拟值,那么谁使用您的代码将很难进行故障排除。首先,回溯是完全无用的。仅此一点就足够了。

谁想修复这个bug,首先要仔细检查bar.py,然后分析egg.py,试图找出其中的一个究竟来自哪里。在读了egg.py之后,他们将不得不读sum_.py并希望注意到None的隐式返回;只有这样他们才能理解问题:由于为他们输入了参数egg.py而使类型检查失败。

加上一点实际的复杂性,事情会很快变得丑陋。

与C不同的是,python在编写时考虑到请求宽恕比许可原则更容易:如果出了问题,我会得到一个例外。如果你给我一个None,我期望一个实际的值,事情就会破裂,异常会发生在远离实际引起它的那条线的地方,人们会用20种不同的语言在你的总的方向上诅咒你,然后改变代码来抛出一个合适的异常(TypeError("incompatible operand type"))。