为什么“except: pass”是一种不好的编程习惯?
- 2024-11-28 08:37:00
- admin 原创
- 225
问题描述:
我经常看到其他 Stack Overflow 问题上关于不鼓励使用的评论except: pass
。为什么这样不好?有时我只是不关心错误是什么,只想继续写代码。
try:
something
except:
pass
为什么使用except: pass
块不好?什么让它变得不好?是因为我pass
犯了错误还是我犯了except
任何错误?
解决方案 1:
正如您正确猜测的那样,它有两个方面:通过在之后不指定任何异常类型来捕获任何except
错误,然后简单地传递它而不采取任何操作。
我的解释“有点”长——所以 tl;dr 可以分解为以下几点:
不要捕获任何错误。始终指定您准备恢复的异常,并且只捕获那些异常。
尽量避免传入 except 块。除非明确需要,否则这通常不是一个好兆头。
但让我们详细讨论一下:
不要捕捉任何错误
使用try
块时,您通常会这样做,因为您知道有可能会抛出异常。因此,您也已经大致了解了什么可能中断以及可能抛出什么异常。在这种情况下,您会捕获异常,因为您可以从中积极恢复。这意味着您已为异常做好准备,并且有在发生该异常时要遵循的一些替代计划。
例如,当您要求用户输入一个数字时,您可以使用 转换输入,int()
这可能会引发ValueError
。您只需要求用户再试一次就可以轻松恢复,因此捕获ValueError
并再次提示用户将是一个合适的计划。另一个例子是,如果您想从文件中读取一些配置,而该文件恰好不存在。因为它是一个配置文件,所以您可能有一些默认配置作为后备,所以该文件不是必需的。所以捕获FileNotFoundError
并简单地应用默认配置在这里是个好计划。现在在这两种情况下,我们都有一个非常具体的异常,我们期望并且有一个同样具体的计划来从中恢复。因此,在每种情况下,我们都只明确指出except
那个特定的异常。
然而,如果我们捕获所有异常,那么除了我们准备恢复的异常之外,还可能遇到意想不到的异常,而这些异常我们确实无法恢复;或者不应该恢复。
让我们以上面的配置文件示例为例。如果文件丢失,我们只需应用默认配置,并可能在稍后决定自动保存配置(因此下次文件就会存在)。现在假设我们得到的是IsADirectoryError
,或PermissionError
。在这种情况下,我们可能不想继续;我们仍然可以应用默认配置,但稍后将无法保存文件。并且用户很可能也想要自定义配置,因此可能不希望使用默认值。因此,我们希望立即告知用户,并且可能也会中止程序执行。但这不是我们想在某个小代码部分深处做的事情;这是应用程序级别的重要内容,因此应该在顶部处理 - 因此让异常冒泡。
Python 2 习惯用法文档中还提到了另一个简单的例子。这里,代码中存在一个简单的拼写错误,导致它中断。因为我们捕获了每个异常,所以我们也捕获了NameError
s和SyntaxError
s。这两个错误都是我们在编程时都会犯的错误,也是我们在交付代码时绝对不想犯的错误。但是因为我们也捕获了它们,所以我们甚至不知道它们发生在那里,并且失去了正确调试它的任何帮助。
但也有更危险的异常,我们不太可能做好准备。例如,SystemError通常很少发生,我们无法真正计划;这意味着发生了一些更复杂的事情,可能会阻止我们继续当前任务。
无论如何,您不太可能在代码的一小部分中为所有事情做好准备,所以您实际上应该只捕获您准备好的异常。有些人建议至少捕获,Exception
因为它不会包括像SystemExit
和KeyboardInterrupt
这样设计用来终止应用程序的内容,但我认为这仍然太不具体了。只有一个地方我个人接受捕获Exception
任何异常,那就是在单个全局应用程序级异常处理程序中,该处理程序的唯一目的是记录我们没有准备好的任何异常。这样,我们仍然可以保留尽可能多的有关意外异常的信息,然后我们可以用这些信息扩展我们的代码以明确处理这些异常(如果我们可以从中恢复)或者 - 在出现错误的情况下 - 创建测试用例以确保它不会再次发生。但当然,这只有当我们只捕获我们已经预料到的异常时才有效,所以我们没有预料到的异常自然会冒出来。
尽量避免传入 except 块
当明确捕获一小部分特定异常时,很多情况下我们什么都不做就没问题了。在这种情况下,只要有就except SomeSpecificException: pass
行了。但大多数情况下情况并非如此,因为我们可能需要一些与恢复过程相关的代码(如上所述)。例如,这可以是再次重试操作,或者设置默认值。
但如果不是这样,例如,因为我们的代码已经构造为重复直到成功,那么只需通过就足够了。以上面的例子为例,我们可能想要求用户输入一个数字。因为我们知道用户不喜欢我们要求他们做的事情,所以我们可能首先将其放入循环中,因此它可能看起来像这样:
def askForNumber ():
while True:
try:
return int(input('Please enter a number: '))
except ValueError:
pass
因为我们一直尝试直到没有异常抛出,所以我们不需要在 except 块中做任何特殊的事情,所以这没问题。但当然,有人可能会说我们至少想向用户显示一些错误消息,告诉他为什么必须重复输入。
不过,在许多其他情况下,仅仅传入一个异常except
就表明我们并没有真正为捕获的异常做好准备。除非这些异常很简单(例如ValueError
或TypeError
),并且我们可以通过的原因很明显,否则请尽量避免直接通过。如果真的没什么可做的(并且您对此非常确定),那么请考虑添加注释来说明为什么会这样;否则,请扩展 except 块以实际包含一些恢复代码。
except: pass
然而,最糟糕的情况是两者兼而有之。这意味着我们愿意捕获任何错误,尽管我们绝对没有为此做好准备,而且我们也不会对此采取任何措施。您至少希望记录错误,并且可能重新引发它以终止应用程序(在发生 MemoryError 之后,您不太可能像往常一样继续运行)。只是通过它不仅会使应用程序保持某种程度的活力(当然取决于您在哪里捕获),而且还会丢弃所有信息,从而无法发现错误 — 如果您不是发现错误的人,情况尤其如此。
因此,底线是:只捕获您真正期望并准备恢复的异常;所有其他异常都可能是您应该修复的错误,或者您无论如何都没有准备好。如果您真的不需要对它们做些什么,传递特定的异常是可以的。在所有其他情况下,这只是一种假设和懒惰的表现。你肯定想解决这个问题。
解决方案 2:
这里的主要问题是它忽略了所有错误:内存不足、CPU 超载、用户想要停止、程序想要退出、Jabberwocky 正在杀死用户。
这太过分了。你脑子里想着“我想忽略这个网络错误”。如果出现意外错误,那么你的代码会默默继续运行,并以完全不可预测的方式中断,没有人可以调试。
这就是为什么你应该限制自己只忽略某些错误,而让其余的错误通过。
解决方案 3:
执行伪代码实际上甚至不会给出任何错误:
try:
something
except:
pass
好像它是一段完全有效的代码,而不是抛出一个NameError
。我希望这不是你想要的。
解决方案 4:
为什么“except: pass”是一种不好的编程习惯?
这有什么不好吗?
try: something except: pass
这会捕获所有可能的异常,包括GeneratorExit
、KeyboardInterrupt
和SystemExit
- 这些都是您可能不想捕获的异常。它与捕获相同BaseException
。
try:
something
except BaseException:
pass
旧版本的文档这样说:
由于 Python 中的每个错误都会引发异常,使用
except:
会使许多编程错误看起来像运行时问题,从而阻碍调试过程。
Python 异常层次结构
如果你捕获了一个父异常类,你也会捕获它们的所有子类。只捕获你准备处理的异常会更优雅。
这是 Python 3异常层次结构- 您真的想捕获它们全部吗?:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
+-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
不要这样做
如果您使用这种形式的异常处理:
try:
something
except: # don't just do a bare except!
pass
那么你将无法something
使用 Ctrl-C 中断你的代码块。你的程序将忽略代码块内所有可能的异常try
。
这是另一个具有相同不良行为的示例:
except BaseException as e: # don't do this either - same as bare!
logging.info(e)
相反,尝试仅捕获您知道正在寻找的特定异常。例如,如果您知道在转换时可能会出现值错误:
try:
foo = operation_that_includes_int(foo)
except ValueError as e:
if fatal_condition(): # You can raise the exception if it's bad,
logging.info(e) # but if it's fatal every time,
raise # you probably should just not catch it.
else: # Only catch exceptions you are prepared to handle.
foo = 0 # Here we simply assign foo to 0 and continue.
用另一个例子进一步解释
您这样做可能是因为您一直在进行网页抓取并得到一个结果UnicodeError
,但是因为您使用了最广泛的异常捕获,所以您的代码(可能存在其他根本缺陷)将尝试运行至完成,从而浪费带宽、处理时间、设备磨损、内存耗尽、收集垃圾数据等。
如果其他人要求你完成工作以便他们能够依赖你的代码,我理解你不得不处理所有事情的感觉。但是如果你愿意在开发过程中大声失败,你就有机会纠正可能只是间歇性出现的问题,但从长远来看,这将是代价高昂的错误。
通过更精确的错误处理,您的代码可以更加健壮。
解决方案 5:
>>> import this
《Python 之禅》,作者:Tim Peters
美丽胜过丑陋。
明确胜过隐晦。
简单胜过复杂。
复杂胜过繁琐。
扁平胜过嵌套。稀疏胜过
密集。
可读性很重要。
特殊情况不足以特殊到打破规则。尽管
实用性胜过纯粹性。
错误永远不应该默默无闻。
除非明确地被压制。
面对歧义,拒绝猜测的诱惑。
应该有一种——最好只有一种——明显的方法来做到这一点。
尽管除非你是荷兰人,否则这种方法一开始可能并不明显。
现在总比没有好。
尽管没有往往比现在更好。如果
实现很难解释,那不是一个好主意。
如果实现很容易解释,那可能是一个好主意。
命名空间是一个非常棒的主意——让我们多做些这样的事情吧!
所以,这是我的观点。每当你发现错误时,你应该做一些事情来处理它,即把它写在日志文件或其他东西中。至少,它会告诉你曾经有一个错误。
解决方案 6:
您至少应该使用except Exception:
以避免捕获诸如SystemExit
或 之类的系统异常KeyboardInterrupt
。这是文档链接。
通常,您应该明确定义要捕获的异常,以避免捕获不需要的异常。您应该知道要忽略哪些异常。
解决方案 7:
首先,它违反了Python 之禅的两个原则:
明确优于隐含
错误不应该悄无声息地发生
它的意思是,你故意让错误悄无声息地通过。此外,你甚至不知道到底发生了哪个错误,因为except: pass
会捕获任何异常。
其次,如果我们试图脱离 Python 的禅宗,只从理智的角度来谈论,你应该知道,使用except:pass
会让你对系统一无所知,无法控制。经验法则是,如果发生错误,就引发异常,并采取适当的措施。如果你事先不知道应该采取什么措施,至少在某处记录错误(最好重新引发异常):
try:
something
except:
logger.exception('Something happened')
但是,通常情况下,如果您尝试捕获任何异常,那么您可能做错了什么!
解决方案 8:
第一个原因已经说明过了——它隐藏了您没有预料到的错误。
(#2)-它使其他人难以阅读和理解您的代码。如果您在尝试读取文件时捕获了 FileNotFoundException,那么对于其他开发人员来说,“catch”块应该具有什么功能就非常明显了。如果您没有指定异常,那么您需要额外的注释来解释该块应该做什么。
(#3)-它展示了懒惰的编程。如果你使用通用的 try/catch,这表明你不了解程序中可能出现的运行时错误,或者你不知道 Python 中可能出现哪些异常。捕获特定错误表明你既了解程序,也了解 Python 抛出的错误范围。这更有可能让其他开发人员和代码审阅者信任你的工作。
解决方案 9:
那么,此代码产生什么输出?
fruits = [ 'apple', 'pear', 'carrot', 'banana' ]
found = False
try:
for i in range(len(fruit)):
if fruits[i] == 'apple':
found = true
except:
pass
if found:
print "Found an apple"
else:
print "No apples in list"
现在想象一下try
-except
代码块包含数百行对复杂对象层次结构的调用,并且本身在大型程序的调用树中间被调用。当程序出错时,您从哪里开始查找?
解决方案 10:
该except:pass
构造本质上会忽略块中涵盖的代码try:
运行时出现的任何和所有异常情况。
这种做法之所以不好,是因为它通常不是您真正想要的。更常见的情况是,一些特定的情况即将出现,而您想将其隐藏起来,但except:pass
这种做法太过生硬。虽然它可以完成工作,但也会掩盖您可能没有预料到的其他错误情况,但您可能非常希望以其他方式处理这些错误情况。
这在 Python 中尤为重要,因为根据这种语言的习惯用法,异常不一定是错误。当然,就像在大多数语言中一样,它们通常以这种方式使用。但 Python 尤其偶尔使用它们来实现某些代码任务的替代退出路径,这些路径实际上不是正常运行情况的一部分,但已知会不时出现,甚至在大多数情况下是预期的。SystemExit
已经作为一个老例子提到,但现在最常见的例子可能是StopIteration
。以这种方式使用异常引起了很多争议,特别是当迭代器和生成器首次引入 Python 时,但最终这个想法占了上风。
解决方案 11:
一般来说,你可以将任何错误/异常归为以下三类之一:
致命错误:不是你的错,你无法阻止它们,也无法从中恢复。你当然不应该忽略它们并继续,让你的程序处于未知状态。只能让错误终止你的程序,你什么也做不了。
愚蠢:你自己的错,很可能是由于疏忽、错误或编程错误。你应该修复错误。同样,你绝对不应该忽视并继续。
外生性:在异常情况下可能会出现这些错误,例如文件未找到或连接终止。您应该明确处理这些错误,并且只处理这些错误。
在所有情况下except: pass
都只会使您的程序处于未知状态,从而可能造成更大的损害。
解决方案 12:
到目前为止提出的所有意见都是有效的。在可能的情况下,您需要指定要忽略的异常。在可能的情况下,您需要分析导致异常的原因,并且只忽略您想要忽略的异常,而不是其他异常。如果异常导致应用程序“严重崩溃”,那就这样吧,因为知道意外发生的时间比隐瞒问题是否发生更重要。
尽管如此,不要把任何编程实践视为最重要的事情。这是愚蠢的。总是有时间和地点去做忽略所有异常的块。
另一个愚蠢至上的例子是goto
运算符的使用。当我还在上学的时候,我们的教授教我们goto
运算符只是为了提醒我们永远不要使用它。不要相信有人告诉你永远不要使用 xyz,也不可能存在它有用的场景。总是有的。
解决方案 13:
简单来说,如果抛出了异常或错误,那就说明出了问题。这可能不是什么大问题,但仅仅为了使用 goto 语句而创建、抛出和捕获错误和异常并不是一个好主意,而且很少这样做。99% 的时间里,某个地方都出了问题。
问题需要解决。就像生活中一样,在编程中,如果你只是置问题于不理,并试图忽略它们,它们很多时候不会自行消失;相反,它们会变得越来越大,不断增加。为了防止问题不断增长并在以后再次出现,你要么 1) 消除它,然后清理混乱,要么 2) 遏制它,然后清理混乱。
忽略异常和错误并让它们保持原样是体验内存泄漏、未完成的数据库连接、文件权限不必要的锁定等的好方法。
在极少数情况下,问题非常小,微不足道,而且除了需要 try...catch 块之外,它是自包含的,事后真的没有什么乱七八糟需要清理。这些是这种最佳实践不一定适用的唯一情况。根据我的经验,这通常意味着代码所做的任何事情基本上都是微不足道的,可以忽略不计,而重试尝试或特殊消息之类的东西既不值得增加复杂性,也不值得让线程停滞不前。
在我的公司,规则是几乎总是在 catch 块中执行某些操作,如果您不执行任何操作,则必须始终添加注释并给出不执行该操作的充分理由。当需要执行任何操作时,您绝不能跳过或留下空的 catch 块。
解决方案 14:
在我看来,错误出现是有原因的,听起来很愚蠢,但事实就是如此。好的编程只有在必须处理错误时才会引发错误。另外,正如我前段时间读到的,“pass 语句是一条显示稍后将插入代码的语句”,因此,如果您想要一个空的 except 语句,请随意这样做,但对于一个好的程序来说,会缺少一部分。因为您没有处理您应该处理的事情。出现的异常让您有机会纠正输入数据或更改数据结构,以便这些异常不会再次发生(但在大多数情况下(网络异常、一般输入异常)异常表示程序的下一部分将无法很好地执行。例如,NetworkException 可能表示网络连接中断,程序无法在下一个程序步骤中发送/接收数据。
但是仅对一个执行块使用传递块是有效的,因为您仍然区分异常的类型,因此如果将所有异常块放在一个块中,它就不是空的:
try:
#code here
except Error1:
#exception handle1
except Error2:
#exception handle2
#and so on
可以这样重写:
try:
#code here
except BaseException as e:
if isinstance(e, Error1):
#exception handle1
elif isinstance(e, Error2):
#exception handle2
...
else:
raise
因此,即使带有 pass 语句的多个 except 块也会产生代码,其结构可以处理特殊类型的异常。
解决方案 15:
由于尚未提及,因此最好使用以下样式contextlib.suppress
:
with suppress(FileNotFoundError):
os.remove('somefile.tmp')
在这个例子中,在该代码块执行后且没有引发任何异常(除了被抑制的)somefile.tmp
之后,将不存在。FileNotFoundError
解决方案 16:
在编程中处理错误非常重要。你确实需要向用户展示出了什么问题。在极少数情况下,你可以忽略错误。这是非常糟糕的编程习惯。
解决方案 17:
我正在构建一个将在数据中心运行的应用程序。它不应生成任何错误或引发任何异常。我的数据中心有一个网络监控系统,其中包括一个 SNMP 陷阱接收器。
try:
main()
except as e:
log(str(e))
send_snmp_trap(str(e))
raise
除了加注不会发生任何事情因为它是任何可能留下的堆栈的底部。
顺便说一句,这绝不是万能药。有些异常无法捕获。SNMP 不保证交付。YMMV。
解决方案 18:
如果这是不好的做法,“通过”将不是一个选择。如果你有一个从许多地方接收信息的资产,即表单或用户输入,它就会派上用场。
variable = False
try:
if request.form['variable'] == '1':
variable = True
except:
pass
解决方案 19:
我个人更喜欢这个解决方案:
except ValueError as error:
print(error.args)
pass
error.args
给我一行代码,不会太分散我的注意力,但确实有助于代码审查,特别是当错误有多种原因时,例如
(ValueError('year 0 is out of range'),)
(ValueError('month must be in 1..12'),)
(ValueError('day is out of range for month'),)
当处理中的时间段时pandas
。