Rumah > Artikel > pembangunan bahagian belakang > Bagaimana untuk menyelesaikan pengecualian ralat Python
Sekiranya anda tidak biasa dengan pengecualian, mari mulakan dengan definisi umum...
pengecualian:( Pengkomputeran ) Gangguan yang berlaku semasa pemprosesan biasa, biasanya disebabkan oleh keadaan ralat, yang boleh dikendalikan oleh bahagian lain program.
Mari kita lihat contoh mudah:
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)rrree
Jelas sekali, ini adalah satu kesilapan. Kami tidak mahu program kami tiba-tiba ranap kerana pengguna memasukkan sesuatu yang pelik. Seperti jenaka berikut...
Seorang jurutera QA masuk ke sebuah bar. Dia memesan bir. Dia memesan lima biji bir. Dia memesan -1 bir. Dia memesan seekor cicak.
Kami mahu menghalang input pelik. Dalam kes ini, hanya terdapat satu titik kegagalan yang penting: fungsi int()
. Ia menjangkakan hujah yang boleh ditukar kepada integer, dan jika ia tidak mendapatnya, ia melemparkan pengecualian ValueError
. Untuk mengendalikan perkara ini dengan betul, kami membalut kod yang mungkin gagal dalam blok try...except
.
>>> 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'
Apabila kami menguji kod kami sekali lagi, kami tidak akan menghadapi ralat kegagalan ini. Jika kami tidak boleh mendapatkan maklumat yang kami perlukan daripada pengguna, kami hanya akan menetapkan code=0
. Sudah tentu, kami boleh menulis semula fungsi initiate_security_protocol()
kami untuk mengendalikan 0
kod yang berbeza, tetapi saya tidak akan menunjukkannya di sini, hanya untuk menjimatkan masa.
NOTA: Atas sebab apa pun, sebagai pengaturcara polyglot, saya sering terlupa menggunakan except
dalam Python dan menggunakan catch
Pernyataan. Saya telah salah menaip tiga kali dalam siaran ini (dan kemudian segera membetulkannya). Ini hanyalah titik ingatan. Syukurlah, Python tidak mempunyai kata kunci catch
, jadi ralat sintaks akan menonjol. Jika anda berbilang bahasa, jangan panik jika anda keliru. Dalam python ia adalah except
, bukan catch
.
Sebelum kita menyelami beberapa butiran yang lebih mendalam tentang pernyataan try...except
ini, mari semak kenyataan ralat sekali lagi. Lagipun, apa gunanya artikel mengenai pengendalian ralat jika kita tidak membincangkan mesej ralat? Dalam Python, kami memanggilnya Traceback kerana ia mengesan asal ralat dari baris pertama kod yang terlibat ke baris terakhir. Dalam banyak bahasa lain, ini akan dipanggil jejak tindanan. try:
code = int(input("Enter security protocol code: "))
except ValueError:
code = 0
initiate_security_protocol(code)
Saya mempunyai tabiat membaca maklumat ini dari bawah ke atas kerana ia membantu saya mendapatkan maklumat yang paling penting terlebih dahulu. Jika anda melihat baris terakhir, anda akan melihat
tidak boleh ditukar kepada integer menggunakan . Kami juga mengetahui bahawa ia cuba menukar kepada integer dengan asas ValueError
10'seven one two'
, yang mungkin maklumat berguna dalam senario lain. Bayangkan, sebagai contoh, jika baris itu ditukar kepada...int()
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'Adalah mustahil jika kita terlupa untuk menentukan asas 16 untuk penukaran , bukannya lalai (asas 10) . Ringkasnya, anda hendaklah sentiasa membaca dan memahami baris terakhir mesej ralat dengan teliti!
Terlalu banyak kali saya telah membaca separuh daripada siaran dan menghabiskan setengah jam mengejar pepijat, hanya untuk menyedari bahawa saya terlupa parameter atau menggunakan fungsi yang salah. int('5bff', 16)
Di atas mesej ralat ialah baris kod tempat ralat itu datang ( ). Di atas ialah laluan mutlak ke fail (
. Pernyataan code = int(input("Enter security protocol code: "))
bermaksud kod berada di luar sebarang fungsi. Dalam contoh ini, panggilan balik hanya satu langkah, jadi mari kita lihat sesuatu yang lebih kompleks. Saya telah menukar dan melanjutkan kod sebelumnya. security_protocols.py
ValueError: invalid literal for int() with base 10: '5bff'
7
Kami menghadapi ralat yang sama seperti sebelum ini - kami cuba menukar rentetan kepada integer tetapi ia tidak berfungsi. Baris kedua hingga terakhir menunjukkan kepada kita kod yang gagal cukup pasti, in <module>
ini disebut. Menurut baris di atas, kod bermasalah terletak di dalam fungsi pada baris 2 fail ! Kami boleh mencarinya dengan segera dan membungkusnya dengan int()
Adakah anda faham mengapa membaca dari bawah ke atas menjimatkan masa? security_protocols.py
然而,让我们想象它并不那么简单。也许我们没有修改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 自己处理基础异常。
这就是我们需要做的所有事情,尽管像往常一样,我们可以用这个做更多的技巧。自定义异常不仅仅是一个漂亮的名字;我们可以使用它们来处理各种不寻常的错误场景,尽管这显然超出了本指南的范围。
Atas ialah kandungan terperinci Bagaimana untuk menyelesaikan pengecualian ralat Python. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!