条件检查与异常处理
Posted
技术标签:
【中文标题】条件检查与异常处理【英文标题】:Condition checking vs. Exception handling [duplicate] 【发布时间】:2011-02-13 22:31:16 【问题描述】:什么时候异常处理比条件检查更可取?在很多情况下,我可以选择使用其中的一种。
例如,这是一个使用自定义异常的求和函数:
# 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"
这是同一个函数,避免使用异常
# 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 的正确案例是什么?为什么?
【问题讨论】:
就像上面 gimel 的链接一样,EAFP 总是比测试类型更可取,因此在编写良好的 Python 中您将很少看到type(a) == type(b)
比较(尽管它们在 C++ 中很常见)。如果 sum 的调用者使用了不兼容的类型,她应该得到一个 TypeError 异常。
【参考方案1】:
当参数包含意外值时,您应该抛出异常。
对于您的示例,我建议在两个参数的类型不同时抛出异常。
抛出异常是一种优雅的中止服务的方式,不会弄乱你的代码。
【讨论】:
【参考方案2】:通常,您希望对可以理解、预期和能够处理的情况使用条件检查。对于不连贯或无法处理的情况,您可以使用例外。
所以,如果您想到您的“添加”功能。它永远不应该返回 null。这不是添加两个东西的连贯结果。在这种情况下,传入的参数中存在错误,并且函数不应不试图假装一切正常。这是抛出异常的完美案例。
如果您处于常规或正常执行情况,您可能希望使用条件检查并返回 null。例如,IsEqual
可能是使用条件的好例子,如果您的条件之一失败,则返回 false。即。
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。这意味着消费者(调用方)无法判断比较是否失败或对象只是不相等。如果需要区分这些情况,则应使用异常而不是条件。
最终,您想问问自己,消费者是否能够专门处理您遇到的失败案例。如果你的方法/函数不能做它需要做的,那么你可能想抛出一个异常。
【讨论】:
您关于可以处理的事情不适合异常的建议似乎是错误的:这就是我们有异常处理的原因。在 Python 中尤其如此,其中使用了大量异常,包括非常预期的情况,例如指示在循环中到达可迭代的结尾。 我的意思是“处理 within 方法而不是不处理”。某些事情可以在“黑匣子”内无缝处理。这是我在“IsEqual”方法中提供的示例,它处理了异常情况,因为用户不一定关心输入的有效性,只关心它们的相等性。当当前上下文不能或不应该处理它时应该抛出异常。 我很清楚存在异常处理。这里的主要区别是 WHERE。在这两种情况下,您都在处理“异常”,因为它是一种意外情况,但是,如果您使用比较,则您对用户隐藏了它。他们不会收到“异常”的通知,他们会收到“null”或“error”响应代码,这是重载返回参数。重载返回参数几乎总是是个坏主意,因为这样你就迫使调用方在调用你的函数后进行检查。 这与抛出异常有着根本的不同,因为他们可以选择处理或不处理(他们可以让异常冒泡)。当使用异常时,用户知道它成功完成而不做任何额外的事情,当你在内部处理它时,你说它成功完成总是。如果你总能产生一个有效的响应,那么你应该使用比较。 @Mike Graham 对不起,我已经很多年没用过python了,所以我实际上有点生疏了。一般来说,例外都会增加性能税,我不知道这在python中是否属实。【参考方案3】:异常更易于管理,因为它们定义了可能出错的一般系列。
在您的示例中,只有一个可能的问题,因此使用异常没有任何优势。但是,如果您有另一个进行除法的类,那么它需要表明您不能除以零。简单地返回 None
将不再有效。
另一方面,异常可以被子类化,并且您可以捕获特定异常,具体取决于您对潜在问题的关心程度。例如,您可能有DoesntCompute
基本异常和InvalidType
和InvalidArgument
等子类。如果您只想要一个结果,您可以将所有计算包装在一个捕获DoesntCompute
的块中,但您仍然可以同样简单地进行非常具体的错误处理。
【讨论】:
【参考方案4】:如果您要问,您可能应该使用异常。异常用于表示异常情况,即事情与其他情况不同的特定情况。几乎所有错误和许多其他事情都是这种情况。
在sum_
的第二个实现中,用户必须每次检查该值是多少。这让人想起 C/Fortran/其他语言的样板文件(以及常见的错误来源),其中错误代码未经检查而我们避免。您必须在所有级别编写这样的代码才能传播错误。它会变得混乱,尤其是在 Python 中避免使用。
其他几点说明:
您通常不需要自己设置例外。在许多情况下,像ValueError
和 TypeError
这样的内置异常是合适的。
当我创建一个非常有用的新异常时,我经常尝试将比Exception
更具体的东西子类化。内置的异常层次结构是here。
我永远不会实现像sum_
这样的函数,因为类型检查会降低您的代码的灵活性、可维护性和惯用性。
我会简单地编写函数
def sum_(a, b):
return a + b
如果对象兼容,这将起作用,如果不兼容,它已经抛出异常,每个人都习惯看到的TypeError
。考虑一下我的实现是如何工作的
>>> 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_ 函数只是一个虚拟示例,用于说明处理结果的两种可能方式。通常我不会有这样的功能。 我知道这只是一个愚蠢的例子,但我注意到类型检查是人们认为他们应该做的一种常见的条件检查类型,但在好的 Python 代码中你不会做很多事情。 【参考方案5】:我更喜欢异常而不是状态返回的主要原因是考虑如果程序员忘记做他的工作会发生什么。对于异常,您可能会忽略捕获异常。在这种情况下,您的系统将明显失败,您将有机会考虑在哪里添加捕获。对于状态返回,如果您忘记检查返回,它将被默默地忽略,并且您的代码将继续运行,稍后可能会以一种神秘的方式失败。比起看不见的失败,我更喜欢看得见的失败。
还有其他原因,我在这里解释过:Exceptions vs. Status Returns。
【讨论】:
【参考方案6】:也许sum_
单独看起来不错。如果你知道,它真的被使用了怎么办?
#foo.py
def sum_(a, b):
if type(a) == type(b):
return a + b
#egg.py
from foo import sum_:
def egg(c = 5):
return sum_(3, c)
#bar.py
from egg import egg
def bar():
return len(egg("2"))
if __name__ == "__main__":
print bar()
如果你运行bar.py
,你会得到:
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()
请看——通常调用一个函数是为了对其输出采取行动。如果您只是“吞下”异常并返回一个虚拟值,那么使用您的代码的人将很难进行故障排除。首先,回溯是完全没用的。仅此一项就足够了。
想要修复此错误的人必须首先仔细检查bar.py
,然后分析egg.py
,试图找出None 的确切来源。在阅读egg.py
之后,他们将不得不阅读sum_.py
并希望注意到None
的隐含返回;只有这样他们才明白问题所在:由于为他们输入了参数egg.py
,他们未能通过类型检查。
在其中加入一些实际的复杂性,事情会很快变得丑陋。
与 C 不同,Python 在编写时牢记Easier to Ask Forgiveness than Permission 原则:如果出现问题,我会得到异常。如果你给我一个 None
我期望一个实际值的地方,事情将会中断,异常将发生在远离实际导致它的行的地方,人们会用二十种不同的语言朝你的大方向诅咒,然后更改代码以引发合适的异常 (TypeError("incompatible operand type")
)。
【讨论】:
【参考方案7】:其实使用异常的问题出在业务逻辑上。如果情况是异常(即根本不应该发生),则可以使用异常。但是,如果从业务逻辑的角度来看这种情况是可能的,那么什么时候应该通过条件检查来处理,即使这个条件看起来要复杂得多。
例如,这是我在准备好的语句中遇到的代码,当开发人员设置参数值时(Java,而不是 Python):
// Variant A
try
ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId());
catch (Exception e)
ps.setNull(1, Types.INTEGER);
使用条件检查,这将是这样写的:
// 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 乍一看似乎要复杂得多,但是,它是正确的,因为从业务角度来看,这种情况是可能的(可能未指定国家/地区)。使用异常会导致性能问题,并且会导致对代码的误解,因为不清楚,国家为空是否可以接受。
可以通过使用 EnterpriseBean 中的辅助函数来改进变体 B,该函数将立即返回地区和国家:
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可以改写如下:
// Variant C
if (enterprise != null && enterprise.getCountry() != null)
ps.setInt(1, enterprise.getCountry().getId());
else
ps.setNull(1, Types.INTEGER);
另外,请阅读Joel article,了解为什么不应过度使用异常。还有 Raymon Chen 的 essay。
【讨论】:
以上是关于条件检查与异常处理的主要内容,如果未能解决你的问题,请参考以下文章