今天要介紹 Python 3 引入的 raise ... from ...
述句。相信大家都知道 raise
述句是用來拋出一個例外。那 raise ... from ...
述句有何不同呢?
#!/usr/bin/env python3
import traceback
class TestError(ValueError):
pass
def test(buf):
try:
return (buf[2], buf[3])
except IndexError as e:
raise TestError() from e
try:
test(b'')
except Exception as e:
print('cause:', repr(e.__cause__))
print('context:', repr(e.__context__))
print('suppress:', repr(e.__suppress_context__))
traceback.print_exc()
這段程式碼會輸出:
cause: IndexError('index out of range',)
context: IndexError('index out of range',)
suppress: True
Traceback (most recent call last):
File "test.py", line 9, in test
return (buf[2], buf[3])
IndexError: index out of range
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 14, in <module>
test(b'')
File "test.py", line 11, in test
raise TestError() from e
TestError
這段輸出有二個部分:上半部是最原始的 IndexError
的 traceback,下半部是 TestError
的 traceback。raise ... from ...
會把 from
指定的例外當成新的例外的「肇因(__cause__
)」,讓開發者得以一路追回錯誤的源頭。
如果我們把 from e
拿掉呢?
#!/usr/bin/env python3
import traceback
class TestError(ValueError):
pass
def test(buf):
try:
return (buf[2], buf[3])
except IndexError as e:
raise TestError()
try:
test(b'')
except Exception as e:
print('cause:', repr(e.__cause__))
print('context:', repr(e.__context__))
print('suppress:', repr(e.__suppress_context__))
traceback.print_exc()
輸出會變成:
cause: None
context: IndexError('index out of range',)
suppress: False
Traceback (most recent call last):
File "test.py", line 9, in test
return (buf[0], buf[1], buf[2], buf[3])
IndexError: index out of range
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test.py", line 14, in <module>
test(b'')
File "test.py", line 11, in test
raise TestError()
TestError
和先前的輸出相比有兩個差異。第一個差異是:
cause: IndexError('index out of range',)
context: IndexError('index out of range',)
suppress: True
變成:
cause: None
context: IndexError('index out of range',)
suppress: False
第二個差異是:
The above exception was the direct cause of the following exception:
被換成:
During handling of the above exception, another exception occurred:
這是因為如果在 Python 3 的 except
子句或 finally
子句中,再度拋出一個例外,則「處理中的例外」會隱含的成為新例外的「上下文(__context__
)」,但是不會是新例外的「肇因(__cause__
)」。
和 raise ... from ...
相比,兩者的 traceback 輸出結果幾乎沒有差異。他們之間的差異主要還是在於開發者的意圖。如果開發者的意圖是重新打包例外再拋出,則應該使用 raise ... from ...
。因為當 except
或 finally
子句拋出非預期的例外時,也會有類似的 traceback,使用者沒辦法知道 __context__
記錄的例外是否是新例外的肇因,還是處理舊例外時再度產生新例外。例如:
#!/usr/bin/env python3
import traceback
class TestError(ValueError):
pass
def test(buf):
try:
return (buf[2], buf[3])
except IndexError as e:
return (buf[0], b'\x00')
try:
test(b'')
except Exception as e:
print('cause:', repr(e.__cause__))
print('context:', repr(e.__context__))
print('suppress:', repr(e.__suppress_context__))
traceback.print_exc()
這個例子中,開發者沒有考慮到 len(buf)
為零的情況,而導致處理舊例外的過程中又產生新例外。雖然他們先後發生,但是各自代表不同的錯誤。
那如果我們不想印出「處理中的例外」呢?如果不想要顯示「處理中的例外」,我們可以使用 raise ... from None
。
舉例來說,如果我們寫了一個指令對照表,我們希望在找不到指令時拋出 CommandError
而不是 KeyError
,我們就可以寫成:
#!/usr/bin/env python3
import traceback
class CommandError(ValueError):
pass
_commands = { 'hello': lambda: print('hello') }
def run_cmd(name):
try:
_commands[name]()
except KeyError:
raise CommandError(name) from None
try:
run_cmd('hello')
run_cmd('world')
except:
traceback.print_exc()
輸出結果會變成:
hello
Traceback (most recent call last):
File "test2.py", line 17, in <module>
run_cmd('world')
File "test2.py", line 13, in run_cmd
raise CommandError(name) from None
CommandError: world
最後值得一提的是 raise ... from ...
述句的 from ...
不一定要丟出接到的例外。你也可以當場建構一個新的例外。
雖然我想不到什麼好例子,但可以參考以下程式碼:
#!/usr/bin/env python3
class ListError(Exception):
pass
class ListNode(object):
def __init__(self, value=None, next=None):
self.value = value
self.next = None
class List(object):
def __init__(self):
self.head = None
self.size = 0
def prepend(self, value):
self.size += 1
self.head = ListNode(value, self.head)
def __getitem__(self, n):
if n < 0 or n >= self.size:
raise ListError() from IndexError(n)
p = self.head
for i in range(n):
p = p.next
return p.value
print(List()[1])
這段程式碼會在真的存取資料之前做錯誤檢查,如果發現使用者輸入的索引大於串列的節點個數,就會拋出一個 ListError
例外。與此同時,我們也建構一個 IndexError
作為例外的成因。