Python中流控制的例外是最佳实践吗?


15

我正在阅读“学习Python”,发现了以下内容:

用户定义的异常也可以表示非错误情况。例如,当找到匹配项时,可以对搜索例程进行编码以引发异常,而不是返回状态标志以供调用方解释。在下面,try / except / else异常处理程序执行if / else返回值测试器的工作:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

因为Python是动态类型化的,并且是内核的多态形式,所以通常使用异常而不是哨兵返回值来表示这种情况。

我已经在各种论坛上多次讨论过这种问题,并且使用StopIteration结束循环对Python进行了引用,但是在官方样式指南中找不到很多(PEP 8附带提供了一个流程控制异常引用)或开发人员的声明。有没有官方声明这是Python的最佳做法?

这(作为控制流的异常是否被认为是严重的反模式?如果是,为什么?)也有几个注释者指出此样式是Pythonic。这是基于什么?

TIA

Answers:


25

普遍的共识是“不要使用例外!” 大部分来自其他语言,甚至有时已经过时。

  • 在C ++中,由于“堆栈展开” ,抛出异常的代价非常高。每个局部变量声明都像withPython中的语句,该变量中的对象可能运行析构函数。这些析构函数在引发异常时以及从函数返回时执行。这种“ RAII习惯用法”是不可或缺的语言功能,对于编写健壮,正确的代码非常重要-因此,RAII与廉价异常之间的平衡是C ++决定对RAII进行的权衡。

  • 在早期的C ++中,许多代码不是以异常安全的方式编写的:除非您实际使用RAII,否则很容易泄漏内存和其他资源。因此,抛出异常会使该代码不正确。这不再合理,因为即使C ++标准库也使用异常:您不能假装不存在异常。但是,将C代码与C ++结合使用时,异常仍然是一个问题。

  • 在Java中,每个异常都有一个关联的堆栈跟踪。堆栈跟踪在调试错误时非常有用,但是在从不打印异常的情况下会浪费很多精力,例如,因为它仅用于控制流程。

因此,在这些语言中,异常太昂贵而无法用作控制流。在Python中,这不再是一个问题,而且异常情况便宜很多。此外,Python语言已经遭受了一些开销,与其他控制流结构相比,这使得异常的代价不明显:例如,使用显式成员资格测试if key in the_dict: ...来检查dict条目是否存在通常与访问该条目the_dict[key]; ...并检查您是否进行检查一样快。得到一个KeyError。一些整体语言功能(例如生成器)是根据异常进行设计的。

因此,尽管没有技术上的理由专门避免使用Python中的异常,但仍然存在一个问题,即是否应使用它们而不是返回值。具有例外的设计级问题是:

  • 他们一点都不明显。您不能轻易地查看一个函数并查看它可能引发哪些异常,因此您并不总是知道要捕获什么。返回值往往定义得更好。

  • 异常是非本地控制流,会使您的代码复杂化。引发异常时,您不知道控制流将从何处恢复。对于无法立即处理的错误,这可能是一个好主意,当将情况通知给呼叫者时,这完全没有必要。

一般而言,Python文化倾向于使用异常,但很容易过分习惯。想象一下一个list_contains(the_list, item)函数,该函数检查列表是否包含与该项目相同的项目。如果通过绝对令人讨厌的异常传达结果,因为我们必须这样称呼它:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

返回布尔值会更清楚:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

如果已经假定该函数返回一个值,则在特殊情况下返回一个特殊值很容易出错,因为人们会忘记检查该值(这可能是C语言中1/3问题的原因)。例外通常更正确。

一个很好的例子是一个pos = find_string(haystack, needle)函数,该函数搜索needlehaystack字符串中字符串的第一个匹配项,然后返回起始位置。但是,如果他们的干草堆弦不包含针弦怎么办?

C并由Python模仿的解决方案是返回一个特殊值。在C中,这是一个空指针,在Python中,这是-1。当将位置用作字符串索引而不进行检查时,这将导致令人惊讶的结果,尤其-1是在Python中有效的索引时。在C语言中,您的NULL指针至少会给您一个段错误。

在PHP中,将返回不同类型的特殊值:布尔值FALSE而不是整数。事实证明,由于语言的隐式转换规则,这实际上并没有任何改善(但请注意,在Python中,布尔值也可以用作整数!)。不返回一致类型的函数通常被认为非常混乱。

一个更健壮的变体是在找不到字符串时引发异常,这确保了在正常控制流程中不可能意外地使用特殊值代替普通值:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

另外,也可以使用总是返回不能直接使用但必须首先解开的类型,例如,结果布尔元组,其中布尔值指示是否发生了异常或结果是否可用。然后:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

这迫使您立即处理问题,但是很快就会很烦人。它还会阻止您轻松链接功能。现在,每个函数调用都需要三行代码。Golang是一种认为这种滋扰值得安全的语言。

综上所述,异常并不是完全没有问题,可以明确地被过度使用,特别是当它们替换“正常”返回值时。但是,当用于表示特殊情况(不一定只是错误)时,异常可以帮助您开发干净,直观,易于使用且难以滥用的API。


1
感谢您的深入回答。我来自Java之类的其他语言,其中“例外仅用于特殊条件”,因此这对我来说是新的。是否有Python核心开发人员曾经用EAFP,EIBTI等其他Python准则(如邮件列表,博客文章等)用这种方式陈述过这些信息?另一个开发者/老板问。谢谢!
JB

通过重载您的自定义异常类的fillInStackTrace方法以立即返回,您可以使其筹集起来非常便宜,因为这是栈遍历的代价,这就是完成的地方。
约翰·科万(John Cowan)

一开始,您的简介中的第一个项目符号令人困惑。也许您可以将其更改为“现代C ++中的异常处理代码的成本为零;但是抛出异常的成本却很高”?(对于潜行者:stackoverflow.com/questions/13835817/…)[编辑:建议编辑]
phresnel

请注意,对于使用a collections.defaultdict或进行字典查找的操作my_dict.get(key, default),其代码要比try: my_dict[key] except: return default
Steve Barnes

11

没有!- 并非一般而言 -异常不被认为是良好的流控制实践,除了单类代码外。生成器或迭代器操作是将异常视为一种合理的甚至更好的信号发送条件的方式。这些操作可以返回任何可能的值作为有效结果,因此需要一种机制来发出结束信号。

考虑一次读取一个流的二进制文件一个字节-绝对任何值都是潜在的有效结果,但是我们仍然需要发信号通知文件结束。因此,我们可以选择每次返回两个值(字节值和有效标志),或者在没有其他事情要做时引发异常。在这两种情况下,使用代码如下所示:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

或者:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

但是,自从实施PEP 343并向后移植以来,所有这些都被整齐地包装在with声明中。上面的代码非常pythonic:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

在python3中,它变成了:

for val in open(source, 'rb').read():
     processbyte(val)

我强烈建议您阅读PEP 343,其中提供了背景,基本原理,示例等。

当使用生成器函数来表示完成时,通常也使用异常来表示处理结束。

我想补充一点,您的搜索器示例几乎可以肯定是向后的,此类函数应该是生成器,在第一次调用时返回第一个匹配项,然后在替换调用中返回下一个匹配项,并NotFound在没有更多匹配项时引发异常。


您说:“不!-总体上来说-异常不被视为良好的流控制实践,唯一的一类代码除外”。这种强调性陈述的理由在哪里?这个答案似乎只专注于迭代情况。在许多其他情况下,人们可能会考虑使用异常。在其他情况下,这样做有什么问题?
Daniel Waltrip

@DanielWaltrip我看到在各种编程语言中使用了例外的代码太多了,而使用简单例外if会更好,这些例外通常使用范围广泛。当涉及调试和测试甚至是代码行为的推理时,最好不要依赖异常-它们应该是异常。在生产代码中,我已经看到了通过抛出/捕获而不是简单返回返回的计算,这意味着当计算遇到被零除的错误时,它将返回一个随机值,而不是崩溃,从而导致错误发生数小时。调试。
史蒂夫·巴恩斯

嘿@SteveBarnes,除零错误捕获示例听起来像是编程不良(具有太笼统的捕获,不会重新引发异常)。
wTyeRogers

您的答案似乎可以归结为:A)“不!” B)有有效的例子说明通过异常的控制流在哪里比替代方法更好地工作C)对问题代码的更正以使用生成器(使用异常作为控制流,并且做得很好)。尽管您的回答通常是有益的,但其中似乎没有论点支持A项,因为它以粗体显示,似乎是主要声明,我希望得到一些支持。如果支持,我不会拒绝您的回答。
las
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.