Home > Article > Backend Development > How to solve Python error exceptions
Just in case you're not familiar with exceptions, let's start with the general definition...
exception:( Computing) An interruption that occurs during normal processing, usually caused by an error condition, that can be handled by another part of the program.
Let's look at a simple example:
def initiate_security_protocol(code): if code == 1: print("Returning onboard companion to home location...") if code == 712: print("Dematerializing to preset location...") code = int(input("Enter security protocol code: ")) initiate_security_protocol(code)
>>> Enter security protocol code: 712 Dematerializing to preset location... >>> Enter security protocol code: seven one two Traceback (most recent call last): File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/security_protocols.py", line 7, in <module> code = int(input("Enter security protocol code: ")) ValueError: invalid literal for int() with base 10: 'seven one two'
Obviously, this is a mistake. We don't want our program to suddenly crash because the user entered something weird. As the following joke goes...
A QA engineer walks into a bar. He ordered a beer. He ordered five beers. He ordered -1 beer. He ordered a lizard.
We want to prevent strange input. In this case, there is only one important point of failure: the int()
function. It expects an argument that can be converted to an integer, and if it doesn't get it, it throws a ValueError
exception. To handle this properly, we wrap possible failing code in a try...except
block.
try: code = int(input("Enter security protocol code: ")) except ValueError: code = 0 initiate_security_protocol(code)
When we test our code again, we will not encounter this failure error. If we can't get the information we need from the user, we will just set code=0
. Of course, we could rewrite our initiate_security_protocol()
function to handle 0
different code, but I won't show it here, just to save time.
NOTE: For whatever reason, as a polyglot programmer I often forget to use except
in Python and in most other languages The catch
statement used. I’ve typed it wrong three times in this post (and then immediately fixed it). This is just a memory point. Thankfully, Python does not have the catch
keyword, so syntax errors will stand out. If you're multilingual, don't panic if you get confused. In python, it is except, not
catch.
try...except statement, let's review the error statement again. After all, what good is an article on error handling if we don't discuss error messages? In Python, we call this
Traceback because it traces the origin of the error from the first line of code involved to the last line. In many other languages, this would be called a stack trace.
Traceback (most recent call last): File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/security_protocols.py", line 7, in <module> code = int(input("Enter security protocol code: ")) ValueError: invalid literal for int() with base 10: 'seven one two'I have a habit of reading this information from bottom to top because it helps me get the most important information first. If you look at the last line, you'll see ValueError, which is the specific exception that was thrown. The exact details are as follows; in this case, you cannot use . to convert the string
'seven one two' to an integer with
int(). We also learned that it is trying to convert to a base
10 integer, which may be useful information in other scenarios. Imagine, for example, if that line was changed to...<pre class="brush:php;toolbar:false">ValueError: invalid literal for int() with base 10: '5bff'</pre>
What if we forgot to specify base 16 for int('5bff', 16)conversion instead of the default ( (base 10), this is entirely possible. In short,
Too many times I’ve read half of a post and spent half an hour chasing down the wrong bug, only to realize that I forgot a parameter or used the wrong function. Above the error message is the line of code where the error comes from ( code = int(input("Enter security protocol code: "))). Above is the absolute path to the file (
) and the line number 7
. The statement in <module>
means that the code is outside any function. In this example, the callback is only one step, so let's look at something a little more complex. I've changed and extended the previous code. <pre class="brush:php;toolbar:false">Traceback (most recent call last):
File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/databank.py", line 6, in <module>
decode_message("Bad Wolf")
File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/databank.py", line 4, in decode_message
initiate_security_protocol(message)
File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/security_protocols.py", line 2, in initiate_security_protocol
code = int(code)
ValueError: invalid literal for int() with base 10: 'Bad Wolf'</pre>
We ran into a similar error as before - we were trying to convert a string to an integer, but it didn't work. The penultimate line shows us the failing code; sure enough, there is a reference to int()
. According to the above line, the problematic code is located on line 2 of the
file inside the initiate_security_protocol()
function! We can find it right away and wrap it in a try...except
. Do you understand why reading from bottom to top saves time? <p data-id="p838747a-3lkFvvs1">然而,让我们想象它并不那么简单。也许我们没有修改<code>security_protocols.py
的选项,所以我们需要在执行该模块之前防止出现问题。如果我们查看下一行,我们会看到在databank.py
第 4 行,在decode_message()
函数内部,我们调用的initiate_security_protocol()
函数有问题。是由于在databank.py
第6行被调用,这就是我们将参数传递"Bad Wolf"
给它的地方。
数据输入不是问题,因为我们要解码消息“Bad Wolf”。但是,为什么我们要将我们试图解码的消息传递给安全协议呢?也许我们需要改写那个函数(或者除了其他更改之外?)。如你所见,Traceback对于了解错误的来源非常重要。养成仔细阅读的习惯;许多有用的信息可能隐藏在意想不到的地方。
顺便说一句,第一行每次都是一样的,但是如果你忘记如何阅读这些消息,它会非常有用。最近执行的代码列在最后。因此,正如我之前所说,你应该从下往上阅读它们。
“请求宽恕比获得许可更容易。” -海军少将格蕾丝·霍珀
这句话最初是关于主动的;如果你相信一个想法,请尽力去尝试它,而不是等待其他人的许可来追求它。然而,在这种情况下,它很好地描述了 Python 的错误处理哲学:如果某些事情经常以一种或多种特定方式失败,通常最好使用<strong>try...except</strong>
语句来处理这些情况。
这种哲学被正式命名为“请求宽恕比许可更容易”,或EAFP。
这有点抽象,所以让我们考虑另一个例子。假设我们希望能够在字典中查找信息。
datafile_index = { # Omitted for brevity. # Just assume there's a lot of data in here. } def get_datafile_id(subject): id = datafile_index[subject] print(f"See datafile {id}.") get_datafile_id("Clara Oswald") get_datafile_id("Ashildir")
See datafile 6035215751266852927. Traceback (most recent call last): File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/databank.py", line 30, in <module> get_datafile_id("Ashildir") File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/databank.py", line 26, in get_datafile_id id = datafile_index[subject] KeyError: 'Ashildir'
第一个函数调用是对的。我们在字典database_index
中搜索存在的键"Clara Oswald"
,因此我们返回与其关联的值 (6035215751266852927
),并在我们格式化语句print()
中打印出该数据。但是,第二个函数调用失败。引发异常KeyError
,因为"Ashildir"
它不是字典中的键。
技术说明: Python 为collections.defaultdict
这个确切问题提供了另一种解决方案;尝试访问不存在的键将使用一些默认值在字典中创建键/值对。但是,由于这是演示错误处理的示例,因此我没有使用它。
由于不能合理地期望我们知道或记住字典中的所有键,尤其是在现实世界的场景中,我们需要一些方法来处理尝试访问不存在的键的常见情况。你的第一直觉可能是在尝试访问字典键之前检查它......
def get_datafile_id(subject): if subject in datafile_index: id = datafile_index[subject] print(f"See datafile {id}.") else: print(f"Datafile not found on {subject})
在 Python 文化中,这种方法被称为“跳前看”[LBYL]。
但这不是最有效的方法!“宽恕,而不是许可”在这里发挥作用:我们不是先测试,而是使用<strong>try...except</strong>
.
def get_datafile_id(subject): try: id = datafile_index[subject] print(f"See datafile {id}.") except KeyError: print(f"Datafile not found on {subject}")
这背后的逻辑很简单:我们不是通过两次获取键,而是只访问一次,并使用实际exception作为逻辑分支的手段。
在 Python 中,我们不认为异常是可以避免的。事实上,try...except
它是许多 Python 设计模式和算法的常规部分。不要害怕引发和捕获异常!事实上,即使是键盘中断也是通过KeyboardInterrupt
异常处理的。
注意: try...except
是一个强大的工具,但它并不适用于一切。例如,None
从函数返回通常被认为比引发异常更好。仅在发生最好由调用者处理的实际错误时抛出异常。
迟早,每个 Python 开发人员都会发现这是可行的:
try: someScaryFunction() except: print("An error occured. Moving on!")
一个单except
语句允许你在一个中捕获所有异常。这个被称为反面模式,这是一个非常非常糟糕的想法。总结一下……
...实际错误的所有上下文放在了一起抛出,永远看不到问题跟踪器的内部。当发生“大量”异常时,堆栈跟踪指向发生次要错误的位置,而不是 try 块内的实际失败位置。
长话短说,你应该始终明确地捕获特定的异常类型。任何你无法预见的失败都可能与一些需要解决的错误有关;例如,当你的超级复杂搜索功能突然开始提出 anOSError
而不是预期的KeyError
或者TypeError
时。
像往常一样,The Zen Python 对此有话要说……
错误永远不应该悄无声息地过去。
除非明确沉默。
换句话说,这不是口袋妖怪 - 你不应该抓住他们!
我不会一下子就捕捉到所有异常。那么,如何处理多个可能的故障呢?
你要知道 Python 的try...except
工具比它最初展示的要多得多。
class SonicScrewdriver: def __init__(self): self.memory = 0 def perform_division(self, lhs, rhs): try: result = float(lhs)/float(rhs) except ZeroDivisionError: print("Wibbly wobbly, timey wimey.") result = "Infinity" except (ValueError, UnicodeError): print("Oy! Don't diss the sonic!") result = "Cannot Calculate" else: self.memory = result finally: print(f"Calculation Result: {result}\n") sonic = SonicScrewdriver() sonic.perform_division(8, 4) sonic.perform_division(4, 0) sonic.perform_division(4, "zero") print(f"Memory Is: {sonic.memory}")
在我向你展示输出之前,请仔细查看代码。你认为这三个sonic.perform_division()
函数调用中的每一个都会打印出什么?最终存储sonic.memory
的是什么?看看你能不能弄明白。
如果你已经有答案?让我们看看你是否正确。
Calculation Result: 2.0 Wibbly wobbly, timey wimey. Calculation Result: Infinity Oy! Don't diss the sonic! Calculation Result: Cannot Calculate Memory Is: 2.0
你是惊讶,还是你做对了?让我们分析一下。
try:
当然,是我们试图运行的代码,它可能会也可能不会引发异常。
except ZeroDivisionError:
当我们试图除以零时发生。在这种情况下,我们说该值"Infinity"
是计算的结果,并打印出一条关于除以0的提示信息。
except (ValueError, UnicodeError):
只要引发这两个异常之一,就会抛出异常。ValueError
是每当我们传递的任何参数都不能被强制转换float()
时,就会发生这种错误,而UnicodeError
是如果编码或解码 Unicode 有问题,就会发生这种报错。实际上,第二个只是为了说明一点。对于ValueError
所有无法将参数转换为浮点数的可信场景,这已经足够了。无论哪种情况,我们都将值"Cannot Calculate"
作为我们的结果,并提醒用户不要对硬件提出不合理的要求。
这就是事情变得有趣的地方。仅在未引发异常时else:
运行。在这种情况下,如果我们有一个有效的除法计算结果,我们实际上希望将它存储在内存中;相反,如果我们得到“无穷大”或“无法计算”作为我们的结果,我们不会存储它。
无论如何,该finally:
部分都会运行。在这种情况下,我们打印出我们的计算结果。
顺序确实很重要。我们必须遵循模式try...except...else...finally
,else
如果存在,必须在所有except
语句之后。finally
总是最后的。
最初很容易混淆else
和finally
,因此请确保你了解其中的区别。<strong>else</strong>
仅在未引发异常时运行;<strong>finally</strong>
每次运行。
你希望以下代码实现什么?
class SonicScrewdriver: def __init__(self): self.memory = 0 def perform_division(self, lhs, rhs): try: result = float(lhs)/float(rhs) except ZeroDivisionError: print("Wibbly wobbly, timey wimey.") result = "Infinity" except (ValueError, UnicodeError): print("Oy! Don't diss the sonic!") result = "Cannot Calculate" else: self.memory = result return result finally: print(f"Calculation Result: {result}\n") result = -1 sonic = SonicScrewdriver() print(sonic.perform_division(8, 4))
下面的那return
句话else
应该是事情的结束了吧?其实,不!如果我们运行该代码...
Calculation Result: 2.0 2.0
有两个重要的观察结果:
finally
正在运行,即使在我们的return
声明之后。该函数不会像通常那样退出。
该return
语句确实在finally
块执行之前运行。我们知道这一点是因为结果result
输出是2.0
,而不是我们在语句finally
中分配的-1
。
<strong>finally</strong>
每次都会运行,即使你<strong>return</strong>
在<strong>try...except</strong>
结构中的其他地方有。
但是,我也用一个os.abort()
代替测试了上面的return result
,在这种情况下,finally
块永远不会运行;该程序彻底中止。你可以在任何地方直接停止程序执行,Python 只会放弃它正在做的事情并退出。该规则是不变的,即使是不寻常的finally
行为。
所以,我们可以用try...except
捕获异常. 但是如果我们只是想主动抛出一个呢?
在 Python 术语中,我们说我们引发了异常,并且与该语言中的大多数事情一样,实现这一点很明显:只需使用raise
关键字:
class Tardis: def __init__(self): pass def camouflage(self): raise NotImplementedError('Chameleon circuits are stuck.') tardis = Tardis() tardis.camouflage()
当我们执行该代码时,我们会看到我们引发的异常。
Traceback (most recent call last): File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/tardis.py", line 10, in <module> tardis.camoflague() File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/tardis.py", line 7, in camoflague raise NotImplementedError('Chameleon circuits are stuck.') NotImplementedError: Chameleon circuits are stuck.
这样就能知道我们在哪儿出现了异常错误。
注意:异常NotImplementedError是Python 中的内置异常之一,有时用于指示一个函数不应该使用,因为它还没有完成(但总有一天会完成)。它不能与NotImplementedvalue互换。请参阅文档以了解何时使用它们。
显然,关键代码是raise NotImplementedError('Chameleon circuits are stuck.')
. 在raise
关键字之后,我们给出要引发的异常对象的名称。在大多数情况下,我们从 Exception 类创建一个新对象,从括号的使用可以看出。所有异常都接受字符串作为消息的第一个参数。一些例外接受或需要更多参数,因此请参阅官方文档。
有时我们需要在捕捉到异常后对其进行处理。我们有一些非常简单的方法来做到这一点。
最明显的是从异常中打印消息。为此,我们需要能够处理我们捕获的异常对象。让我们将except
语句更改为except NotImplementedError as e:
,其中e
是我们“绑定”到异常对象的名称。然后,我们可以e
直接作为对象使用。
tardis = Tardis() try: tardis.camouflage() except NotImplementedError as e: print(e)
异常类已经定义了它的__str__()
函数来返回异常消息,所以如果我们将它转换成一个字符串(str()
),这就是我们将得到的。你可能还记得上一篇文章print()
自动将其参数转换为字符串。当我们运行这段代码时,我们得到...
Chameleon circuits are stuck.
是不是很容易!
现在,如果我们想再次引发异常怎么办?
等等,什么?我们刚刚捕获了那个东西。为什么还要再次引发异常?
一个示例是,如果你需要在幕后进行一些清理工作,但最终仍希望调用者必须处理异常。这是一个例子......
class Byzantium: def __init__(self): self.power = 0 def gravity_field(self): if self.power <= 0: raise SystemError("Gravity Failing") def grab_handle(): pass byzantium = Byzantium() try: byzantium.gravity_field() except SystemError: grab_handle() print("Night night") raise
在上面的示例中,我们只是想捕获一些实体 ( grab_handle()
) 并打印一条附加消息,然后让异常继续raise
抛出. 当我们重新引发异常时,我们说它冒泡了。
Night night Traceback (most recent call last): File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/byzantium.py", line 18, in <module> byzantium.gravity_field() File "/home/jason/Code/FiringRanges/PyFiringRange/Sandbox/byzantium.py", line 8, in gravity_field raise SystemError("Gravity Failing") SystemError: Gravity Failing
注意:你可能认为我们需要说except SystemError as e:
或者说raise e
什么,但那是矫枉过正。对于冒泡的异常,我们只需要自己调用<strong>raise</strong>
。
现在,如果我们想在冒泡异常的同时添加一些额外的信息怎么办?你的第一个猜测可能只是完全引发一个新异常,但这会带来一些问题。为了演示,我将在执行顺序中添加另一层。请注意,当我处理这个问题时SystemError
,我会提出一个新的RuntimeError
。我在第二个try...except
区块中发现了这个新异常。
byzantium = Byzantium() def test(): try: byzantium.gravity_field() except SystemError: grab_handle() raise RuntimeError("Night night") try: test() except RuntimeError as e: print(e) print(e.__cause__)
当我们运行它时,我们得到以下输出。
Night night None
当我们捕获到这个新异常时,我们完全没有关于它是什么原因的上下文。为了解决这个问题,Python 3在PEP 3134中引入了显式异常链接。实现它很容易。看看我们的新函数test()
,这是我与上一个示例相比唯一更改的部分。
byzantium = Byzantium() def test(): try: byzantium.gravity_field() except SystemError as e: grab_handle() raise RuntimeError("Night night") from e try: test() except RuntimeError as e: print(e) print(e.__cause__)
你有没有发现我在那儿做什么?在except
声明中,我将名称绑定e
到我们捕获的原始异常。然后,在引发新RuntimeError
异常时,我将其链接到上一个异常,并使用from e
. 我们现在的输出...
Night night Gravity Failing
当我们运行它时,我们的新异常会记住它是从哪里来的——前一个异常存储在它的__cause__
属性中(打印在输出的第二行)。这对于日志记录特别有用。
你可以使用异常类执行许多其他技巧,尤其是在引入 PEP 3134 时。像往常一样,我建议你阅读文档,我在文章末尾链接到该文档。
Python 有一大堆异常,它们的使用有据可查。当我为工作选择合适的异常时,我经常参考这个异常列表。然而,有时,我们只需要更多……定制的东西。
所有错误类型的异常都是从Exception
类派生的,而类又是从BaseException
类派生的。这种双重层次结构的原因是你可以捕获所有错误Exceptions
,而无需对特殊的、非系统退出的异常(如KeyboardInterrupt
. 当然,这在实践中对你来说并不重要,因为except Exception
实际上总是我之前提到的反模式的另一种形式。无论如何,不建议你直接派生自BaseException
——只要知道它存在即可。
在进行自定义异常时,你实际上可以从任何你喜欢的异常类派生。有时,最好从与你正在自定义的异常最接近的异常中获取。但是,如果你不知所措,你可以从Exception
派生.
让我们自定义一个,好吗?
class SpacetimeError(Exception): def __init__(self, message): super().__init__(message) class Tardis(): def __init__(self): self._destination = "" self._timestream = [] def cloister_bell(self): print("(Ominous bell tolling)") def dematerialize(self): self._timestream.append(self._destination) print("(Nifty whirring sound)") def set_destination(self, dest): if dest in self._timestream: self.cloister_bell() self._destination = dest def engage(self): if self._destination in self._timestream: raise SpacetimeError("You should not cross your own timestream!") else: self.dematerialize() tardis = Tardis() # Should be fine tardis.set_destination("7775/349x10,012/acorn") tardis.engage() # Also fine tardis.set_destination("5136/161x298,58/delta") tardis.engage() # The TARDIS is not going to like this... tardis.set_destination("7775/349x10,012/acorn") tardis.engage()
显然,最后一个将导致我们的SpacetimeError
异常被引发。
让我们再看看那个异常类声明。
class SpacetimeError(Exception): def __init__(self, message): super().__init__(message)
这实际上非常容易编写。如果你还记得我们之前对类的探索,super().__init__()
就是在基类上调用初始化函数,Exception
在这种情况下就是这样。我们将消息传递给SpacetimeError
异常构造函数,并将其交给基类初始化函数。
事实上,如果我唯一要做的就是将 传递message
给super()
, 类,我可以让这更简单:
class SpacetimeError(Exception): pass
Python 自己处理基础异常。
这就是我们需要做的所有事情,尽管像往常一样,我们可以用这个做更多的技巧。自定义异常不仅仅是一个漂亮的名字;我们可以使用它们来处理各种不寻常的错误场景,尽管这显然超出了本指南的范围。
The above is the detailed content of How to solve Python error exceptions. For more information, please follow other related articles on the PHP Chinese website!