試驗還是很棒的,但是調(diào)試?就沒那么有趣了。事實是,在 Python 測試代碼之前沒有編譯器來分析你的代碼,因此使的測試成為開發(fā)的一個重要部分。本章的目標是討論一些關于測試、調(diào)試和異常處理的常見問題。但是并不是為測試驅(qū)動開發(fā)或者單元測試模塊做一個簡要的介紹。因此,筆者假定讀者熟悉測試概念。
你的程序中有個方法會輸出到標準輸出中(sys.stdout)。也就是說它會將文本打印到屏幕上面。 你想寫個測試來證明它,給定一個輸入,相應的輸出能正常顯示出來。
使用 unittest.mock 模塊中的 patch() 函數(shù), 使用起來非常簡單,可以為單個測試模擬 sys.stdout 然后回滾, 并且不產(chǎn)生大量的臨時變量或在測試用例直接暴露狀態(tài)變量。
作為一個例子,我們在 mymodule 模塊中定義如下一個函數(shù):
# mymodule.py
def urlprint(protocol, host, domain):
url = '{}://{}.{}'.format(protocol, host, domain)
print(url)
默認情況下內(nèi)置的 print 函數(shù)會將輸出發(fā)送到 sys.stdout 。 為了測試輸出真的在那里,你可以使用一個替身對象來模擬它,然后使用斷言來確認結(jié)果。 使用 unittest.mock 模塊的patch() 方法可以很方便的在測試運行的上下文中替換對象, 并且當測試完成時候自動返回它們的原有狀態(tài)。下面是對 mymodule模塊的測試代碼:
from io import StringIO
from unittest import TestCase
from unittest.mock import patch
import mymodule
class TestURLPrint(TestCase):
def test_url_gets_to_stdout(self):
protocol = 'http'
host = 'www'
domain = 'example.com'
expected_url = '{}://{}.{}\n'.format(protocol, host, domain)
with patch('sys.stdout', new=StringIO()) as fake_out:
mymodule.urlprint(protocol, host, domain)
self.assertEqual(fake_out.getvalue(), expected_url)
urlprint()函數(shù)接受三個參數(shù),測試方法開始會先設置每一個參數(shù)的值。 expected_url 變量被設置成包含期望的輸出的字符串。
unittest.mock.patch()函數(shù)被用作一個上下文管理器,使用StringIO 對象來代替 sys.stdout . fake_out 變量是在該進程中被創(chuàng)建的模擬對象。 在 with 語句中使用它可以執(zhí)行各種檢查。當 with 語句結(jié)束時,patch 會將所有東西恢復到測試開始前的狀態(tài)。 有一點需要注意的是某些對 Python 的 C 擴展可能會忽略掉 sys.stdout 的配置二直接寫入到標準輸出中。 限于篇幅,本節(jié)不會涉及到這方面的講解,它適用于純 Python 代碼。 如果你真的需要在 C 擴展中捕獲 I/O,你可以先打開一個臨時文件,然后將標準輸出重定向到該文件中。 更多關于捕獲以字符串形式捕獲 I/O 和 StringIO 對象請參閱5.6小節(jié)。
你寫的單元測試中需要給指定的對象打補丁, 用來斷言它們在測試中的期望行為(比如,斷言被調(diào)用時的參數(shù)個數(shù),訪問指定的屬性等)。
unittest.mock.patch() 函數(shù)可被用來解決這個問題。patch() 還可被用作一個裝飾器、上下文管理器或單獨使用,盡管并不常見。 例如,下面是一個將它當做裝飾器使用的例子:
from unittest.mock import patch
import example
@patch('example.func')
def test1(x, mock_func):
example.func(x) # Uses patched example.func
mock_func.assert_called_with(x)
它還可以被當做一個上下文管理器:
with patch('example.func') as mock_func:
example.func(x) # Uses patched example.func
mock_func.assert_called_with(x)
最后,你還可以手動的使用它打補丁:
p = patch('example.func')
mock_func = p.start()
example.func(x)
mock_func.assert_called_with(x)
p.stop()
如果可能的話,你能夠疊加裝飾器和上下文管理器來給多個對象打補丁。例如:
@patch('example.func1')
@patch('example.func2')
@patch('example.func3')
def test1(mock1, mock2, mock3):
...
def test2():
with patch('example.patch1') as mock1, \
patch('example.patch2') as mock2, \
patch('example.patch3') as mock3:
...
patch() 接受一個已存在對象的全路徑名,將其替換為一個新的值。 原來的值會在裝飾器函數(shù)或上下文管理器完成后自動恢復回來。 默認情況下,所有值會被 MagicMock 實例替代。例如:
>>> x = 42
>>> with patch('__main__.x'):
... print(x)
...
<MagicMock name='x' id='4314230032'>
>>> x
42
>>>
不過,你可以通過給 patch() 提供第二個參數(shù)來將值替換成任何你想要的:
>>> x
42
>>> with patch('__main__.x', 'patched_value'):
... print(x)
...
patched_value
>>> x
42
>>>
被用來作為替換值的 MagicMock 實例能夠模擬可調(diào)用對象和實例。 他們記錄對象的使用信息并允許你執(zhí)行斷言檢查,例如:
>>> from unittest.mock import MagicMock
>>> m = MagicMock(return_value = 10)
>>> m(1, 2, debug=True)
10
>>> m.assert_called_with(1, 2, debug=True)
>>> m.assert_called_with(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../unittest/mock.py", line 726, in assert_called_with
raise AssertionError(msg)
AssertionError: Expected call: mock(1, 2)
Actual call: mock(1, 2, debug=True)
>>>
>>> m.upper.return_value = 'HELLO'
>>> m.upper('hello')
'HELLO'
>>> assert m.upper.called
>>> m.split.return_value = ['hello', 'world']
>>> m.split('hello world')
['hello', 'world']
>>> m.split.assert_called_with('hello world')
>>>
>>> m['blah']
<MagicMock name='mock.__getitem__()' id='4314412048'>
>>> m.__getitem__.called
True
>>> m.__getitem__.assert_called_with('blah')
>>>
一般來講,這些操作會在一個單元測試中完成。例如,假設你已經(jīng)有了像下面這樣的函數(shù):
# example.py
from urllib.request import urlopen
import csv
def dowprices():
u = urlopen('http://finance.yahoo.com/d/quotes.csv?s=@^DJI&f=sl1')
lines = (line.decode('utf-8') for line in u)
rows = (row for row in csv.reader(lines) if len(row) == 2)
prices = { name:float(price) for name, price in rows }
return prices
正常來講,這個函數(shù)會使用 urlopen() 從 Web 上面獲取數(shù)據(jù)并解析它。 在單元測試中,你可以給它一個預先定義好的數(shù)據(jù)集。下面是使用補丁操作的例子:
import unittest
from unittest.mock import patch
import io
import example
sample_data = io.BytesIO(b'''\
"IBM",91.1\r
"AA",13.25\r
"MSFT",27.72\r
\r
''')
class Tests(unittest.TestCase):
@patch('example.urlopen', return_value=sample_data)
def test_dowprices(self, mock_urlopen):
p = example.dowprices()
self.assertTrue(mock_urlopen.called)
self.assertEqual(p,
{'IBM': 91.1,
'AA': 13.25,
'MSFT' : 27.72})
if __name__ == '__main__':
unittest.main()
本例中,位于 example 模塊中的 urlopen() 函數(shù)被一個模擬對象替代, 該對象會返回一個包含測試數(shù)據(jù)的 ByteIO().
還有一點,在打補丁時我們使用了 example.urlopen 來代替 urllib.request.urlopen 。 當你創(chuàng)建補丁的時候,你必須使用它們在測試代碼中的名稱。 由于測試代碼使用了 from urllib.request import urlopen ,那么 dowprices() 函數(shù) 中使用的 urlopen()函數(shù)實際上就位于 example模塊了。
本節(jié)實際上只是對 unittest.mock模塊的一次淺嘗輒止。 更多更高級的特性,請參考官方文檔
你想寫個測試用例來準確的判斷某個異常是否被拋出。
對于異常的測試可使用 assertRaises() 方法。 例如,如果你想測試某個函數(shù)拋出了 ValueError 異常,像下面這樣寫:
import unittest
# A simple function to illustrate
def parse_int(s):
return int(s)
class TestConversion(unittest.TestCase):
def test_bad_int(self):
self.assertRaises(ValueError, parse_int, 'N/A')
如果你想測試異常的具體值,需要用到另外一種方法:
import errno
class TestIO(unittest.TestCase):
def test_file_not_found(self):
try:
f = open('/file/not/found')
except IOError as e:
self.assertEqual(e.errno, errno.ENOENT)
else:
self.fail('IOError not raised')
assertRaises() 方法為測試異常存在性提供了一個簡便方法。 一個常見的陷阱是手動去進行異常檢測。比如:
class TestConversion(unittest.TestCase):
def test_bad_int(self):
try:
r = parse_int('N/A')
except ValueError as e:
self.assertEqual(type(e), ValueError)
這種方法的問題在于它很容易遺漏其他情況,比如沒有任何異常拋出的時候。 那么你還得需要增加另外的檢測過程,如下面這樣:
class TestConversion(unittest.TestCase):
def test_bad_int(self):
try:
r = parse_int('N/A')
except ValueError as e:
self.assertEqual(type(e), ValueError)
else:
self.fail('ValueError not raised')
assertRaises() 方法會處理所有細節(jié),因此你應該使用它。
assertRaises() 的一個缺點是它測不了異常具體的值是多少。 為了測試異常值,可以使用 assertRaisesRegex()方法, 它可同時測試異常的存在以及通過正則式匹配異常的字符串表示。例如:
class TestConversion(unittest.TestCase):
def test_bad_int(self):
self.assertRaisesRegex(ValueError, 'invalid literal .*',
parse_int, 'N/A')
assertRaises() 和 assertRaisesRegex() 還有一個容易忽略的地方就是它們還能被當做上下文管理器使用:
class TestConversion(unittest.TestCase):
def test_bad_int(self):
with self.assertRaisesRegex(ValueError, 'invalid literal .*'):
r = parse_int('N/A')
但你的測試涉及到多個執(zhí)行步驟的時候這種方法就很有用了。
你希望將單元測試的輸出寫到到某個文件中去,而不是打印到標準輸出。
運行單元測試一個常見技術就是在測試文件底部加入下面這段代碼片段:
import unittest
class MyTest(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()
這樣的話測試文件就是可執(zhí)行的,并且會將運行測試的結(jié)果打印到標準輸出上。 如果你想重定向輸出,就需要像下面這樣修改 main() 函數(shù):
import sys
def main(out=sys.stderr, verbosity=2):
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(sys.modules[__name__])
unittest.TextTestRunner(out,verbosity=verbosity).run(suite)
if __name__ == '__main__':
with open('testing.out', 'w') as f:
main(f)
本節(jié)感興趣的部分并不是將測試結(jié)果重定向到一個文件中, 而是通過這樣做向你展示了 unittest 模塊中一些值得關注的內(nèi)部工作原理。
unittest 模塊首先會組裝一個測試套件。 這個測試套件包含了你定義的各種方法。一旦套件組裝完成,它所包含的測試就可以被執(zhí)行了。
這兩步是分開的,unittest.TestLoader 實例被用來組裝測試套件。 loadTestsFromModule() 是它定義的方法之一,用來收集測試用例。 它會為 TestCase 類掃描某個模塊并將其中的測試方法提取出來。 如果你想進行細粒度的控制, 可以使用 loadTestsFromTestCase() 方法來從某個繼承 TestCase 的類中提取測試方法。 TextTestRunner類是一個測試運行類的例子, 這個類的主要用途是執(zhí)行某個測試套件中包含的測試方法。 這個類跟執(zhí)行 unittest.main() 函數(shù)所使用的測試運行器是一樣的。 不過,我們在這里對它進行了一些列底層配置,包括輸出文件和提升級別。 盡管本節(jié)例子代碼很少,但是能指導你如何對 unittest 框架進行更進一步的自定義。 要想自定義測試套件的裝配方式,你可以對 TestLoader 類執(zhí)行更多的操作。 為了自定義測試運行,你可以構(gòu)造一個自己的測試運行類來模擬 TextTestRunner 的功能。 而這些已經(jīng)超出了本節(jié)的范圍。unittest 模塊的文檔對底層實現(xiàn)原理有更深入的講解,可以去看看。
你想在單元測試中忽略或標記某些測試會按照預期運行失敗。
unittest 模塊有裝飾器可用來控制對指定測試方法的處理,例如:
import unittest
import os
import platform
class Tests(unittest.TestCase):
def test_0(self):
self.assertTrue(True)
@unittest.skip('skipped test')
def test_1(self):
self.fail('should have failed!')
@unittest.skipIf(os.name=='posix', 'Not supported on Unix')
def test_2(self):
import winreg
@unittest.skipUnless(platform.system() == 'Darwin', 'Mac specific test')
def test_3(self):
self.assertTrue(True)
@unittest.expectedFailure
def test_4(self):
self.assertEqual(2+2, 5)
if __name__ == '__main__':
unittest.main()
如果你在 Mac 上運行這段代碼,你會得到如下輸出:
bash % python3 testsample.py -v
test_0 (__main__.Tests) ... ok
test_1 (__main__.Tests) ... skipped 'skipped test'
test_2 (__main__.Tests) ... skipped 'Not supported on Unix'
test_3 (__main__.Tests) ... ok
test_4 (__main__.Tests) ... expected failure
----------------------------------------------------------------------
Ran 5 tests in 0.002s
OK (skipped=2, expected failures=1)
討論
skip()裝飾器能被用來忽略某個你不想運行的測試。 skipIf()和skipUnless() 對于你只想在某個特定平臺或 Python 版本或其他依賴成立時才運行測試的時候非常有用。 使用 @expected 的失敗裝飾器來標記那些確定會失敗的測試,并且對這些測試你不想讓測試框架打印更多信息。
忽略方法的裝飾器還可以被用來裝飾整個測試類,比如:
@unittest.skipUnless(platform.system() == 'Darwin', 'Mac specific tests')
class DarwinTests(unittest.TestCase):
pass
你有一個代碼片段可能會拋出多個不同的異常,怎樣才能不創(chuàng)建大量重復代碼就能處理所有的可能異常呢?
如果你可以用單個代碼塊處理不同的異常,可以將它們放入一個元組中,如下所示:
try:
client_obj.get_url(url)
except (URLError, ValueError, SocketTimeout):
client_obj.remove_url(url)
在這個例子中,元祖中任何一個異常發(fā)生時都會執(zhí)行 remove_url() 方法。 如果你想對其中某個異常進行不同的處理,可以將其放入另外一個 except語句中:
try:
client_obj.get_url(url)
except (URLError, ValueError):
client_obj.remove_url(url)
except SocketTimeout:
client_obj.handle_url_timeout(url)
很多的異常會有層級關系,對于這種情況,你可能使用它們的一個基類來捕獲所有的異常。例如,下面的代碼:
try:
f = open(filename)
except (FileNotFoundError, PermissionError):
pass
可以被重寫為:
try:
f = open(filename)
except OSError:
pass
OSError 是 FileNotFoundError 和 PermissionError異常的基類。
盡管處理多個異常本身并沒什么特殊的,不過你可以使用 as 關鍵字來獲得被拋出異常的引用:
try:
f = open(filename)
except OSError as e:
if e.errno == errno.ENOENT:
logger.error('File not found')
elif e.errno == errno.EACCES:
logger.error('Permission denied')
else:
logger.error('Unexpected error: %d', e.errno)
這個例子中, e變量指向一個被拋出的 OSError異常實例。 這個在你想更進一步分析這個異常的時候會很有用,比如基于某個狀態(tài)碼來處理它。
同時還要注意的時候 except語句是順序檢查的,第一個匹配的會執(zhí)行。 你可以很容易的構(gòu)造多個 except 同時匹配的情形,比如:
>>> f = open('missing')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'missing'
>>> try:
... f = open('missing')
... except OSError:
... print('It failed')
... except FileNotFoundError:
... print('File not found')
...
It failed
>>>
這里的 FileNotFoundError 語句并沒有執(zhí)行的原因是 OSError更一般,它可匹配 FileNotFoundError異常, 于是就是第一個匹配的。 在調(diào)試的時候,如果你對某個特定異常的類成層級關系不是很確定, 你可以通過查看該異常的 __mro__ 屬性來快速瀏覽。比如:
>>> FileNotFoundError.__mro__
(<class 'FileNotFoundError'>, <class 'OSError'>, <class 'Exception'>,
<class 'BaseException'>, <class 'object'>)
>>>
上面列表中任何一個直到 BaseException 的類都能被用于 except 語句。
怎樣捕獲代碼中的所有異常?
想要捕獲所有的異常,可以直接捕獲 Exception 即可:
try:
...
except Exception as e:
...
log('Reason:', e) # Important!
這個將會捕獲除了 SystemExit 、KeyboardInterrupt 和 GeneratorExit 之外的所有異常。 如果你還想捕獲這三個異常,將 Exception 改成 BaseException 即可。
捕獲所有異常通常是由于程序員在某些復雜操作中并不能記住所有可能的異常。 如果你不是很細心的人,這也是編寫不易調(diào)試代碼的一個簡單方法。
正因如此,如果你選擇捕獲所有異常,那么在某個地方(比如日志文件、打印異常到屏幕)打印確切原因就比較重要了。 如果你沒有這樣做,有時候你看到異常打印時可能摸不著頭腦,就像下面這樣:
def parse_int(s):
try:
n = int(v)
except Exception:
print("Couldn't parse")
試著運行這個函數(shù),結(jié)果如下:
>>> parse_int('n/a')
Couldn't parse
>>> parse_int('42')
Couldn't parse
>>>
這時候你就會撓頭想:“這咋回事啊?” 假如你像下面這樣重寫這個函數(shù):
def parse_int(s):
try:
n = int(v)
except Exception as e:
print("Couldn't parse")
print('Reason:', e)
這時候你能獲取如下輸出,指明了有個編程錯誤:
>>> parse_int('42')
Couldn't parse
Reason: global name 'v' is not defined
>>>
很明顯,你應該盡可能將異常處理器定義的精準一些。 不過,要是你必須捕獲所有異常,確保打印正確的診斷信息或?qū)惓鞑コ鋈ィ@樣不會丟失掉異常。
在你構(gòu)建的應用程序中,你想將底層異常包裝成自定義的異常。
創(chuàng)建新的異常很簡單——定義新的類,讓它繼承自 Exception (或者是任何一個已存在的異常類型)。 例如,如果你編寫網(wǎng)絡相關的程序,你可能會定義一些類似如下的異常:
class NetworkError(Exception):
pass
class HostnameError(NetworkError):
pass
class TimeoutError(NetworkError):
pass
class ProtocolError(NetworkError):
pass
然后用戶就可以像通常那樣使用這些異常了,例如:
try:
msg = s.recv()
except TimeoutError as e:
...
except ProtocolError as e:
...
自定義異常類應該總是繼承自內(nèi)置的 Exception 類, 或者是繼承自那些本身就是從 Exception繼承而來的類。 盡管所有類同時也繼承自 BaseException ,但你不應該使用這個基類來定義新的異常。 BaseException是為系統(tǒng)退出異常而保留的,比如 KeyboardInterrupt 或 SystemExit 以及其他那些會給應用發(fā)送信號而退出的異常。 因此,捕獲這些異常本身沒什么意義。 這樣的話,假如你繼承BaseException 可能會導致你的自定義異常不會被捕獲而直接發(fā)送信號退出程序運行。
在程序中引入自定義異??梢允沟媚愕拇a更具可讀性,能清晰顯示誰應該閱讀這個代碼。 還有一種設計是將自定義異常通過繼承組合起來。在復雜應用程序中, 使用基類來分組各種異常類也是很有用的。它可以讓用戶捕獲一個范圍很窄的特定異常,比如下面這樣的:
try:
s.send(msg)
except ProtocolError:
...
你還能捕獲更大范圍的異常,就像下面這樣:
try:
s.send(msg)
except NetworkError:
...
如果你想定義的新異常重寫了 __init__() 方法, 確保你使用所有參數(shù)調(diào)用 Exception.__init__() ,例如:
class CustomError(Exception):
def __init__(self, message, status):
super().__init__(message, status)
self.message = message
self.status = status
看上去有點奇怪,不過 Exception 的默認行為是接受所有傳遞的參數(shù)并將它們以元組形式存儲在 .args 屬性中. 很多其他函數(shù)庫和部分 Python 庫默認所有異常都必須有 .args 屬性, 因此如果你忽略了這一步,你會發(fā)現(xiàn)有些時候你定義的新異常不會按照期望運行。 為了演示 .args 的使用,考慮下下面這個使用內(nèi)置的 RuntimeError 異常的交互會話, 注意看 raise 語句中使用的參數(shù)個數(shù)是怎樣的:
>>> try:
... raise RuntimeError('It failed')
... except RuntimeError as e:
... print(e.args)
...
('It failed',)
>>> try:
... raise RuntimeError('It failed', 42, 'spam')
... except RuntimeError as e:
... print(e.args)
...
('It failed', 42, 'spam')
>>>
關于創(chuàng)建自定義異常的更多信息,請參考 Python 官方文檔
你想捕獲一個異常后拋出另外一個不同的異常,同時還得在異?;厮葜斜A魞蓚€異常的信息。
為了鏈接異常,使用 raise from語句來代替簡單的 raise語句。 它會讓你同時保留兩個異常的信息。例如:
>>> def example():
... try:
... int('N/A')
... except ValueError as e:
... raise RuntimeError('A parsing error occurred') from e
>>>
example()
Traceback (most recent call last):
File "<stdin>", line 3, in example
ValueError: invalid literal for int() with base 10: 'N/A'
上面的異常是下面的異常產(chǎn)生的直接原因:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in example
RuntimeError: A parsing error occurred
>>>
在回溯中科院看到,兩個異常都被捕獲。 要想捕獲這樣的異常,你可以使用一個簡單的 except 語句。 不過,你還可以通過查看異常對象的 __cause__ 屬性來跟蹤異常鏈。例如:
try:
example()
except RuntimeError as e:
print("It didn't work:", e)
if e.__cause__:
print('Cause:', e.__cause__)
當在 except 塊中又有另外的異常被拋出時會導致一個隱藏的異常鏈的出現(xiàn)。例如:
>>> def example2():
... try:
... int('N/A')
... except ValueError as e:
... print("Couldn't parse:", err)
...
>>>
>>> example2()
Traceback (most recent call last):
File "<stdin>", line 3, in example2
ValueError: invalid literal for int() with base 10: 'N/A'
在處理上述異常的時候,另外一個異常發(fā)生了:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in example2
NameError: global name 'err' is not defined
>>>
這個例子中,你同時獲得了兩個異常的信息,但是對異常的解釋不同。 這時候,NameError 異常被作為程序最終異常被拋出,而不是位于解析異常的直接回應中。
如果,你想忽略掉異常鏈,可使用 raise from None :
>>> def example3():
... try:
... int('N/A')
... except ValueError:
... raise RuntimeError('A parsing error occurred') from None...
>>>
example3()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in example3
RuntimeError: A parsing error occurred
>>>
在設計代碼時,在另外一個 except代碼塊中使用 raise語句的時候你要特別小心了。 大多數(shù)情況下,這種 raise 語句都應該被改成raise from語句。也就是說你應該使用下面這種形式:
try:
...
except SomeException as e:
raise DifferentException() from e
這樣做的原因是你應該顯示的將原因鏈接起來。 也就是說,DifferentException 是直接從 SomeException 衍生而來。 這種關系可以從回溯結(jié)果中看出來。
如果你像下面這樣寫代碼,你仍然會得到一個鏈接異常, 不過這個并沒有很清晰的說明這個異常鏈到底是內(nèi)部異常還是某個未知的編程錯誤。
try:
...
except SomeException:
raise DifferentException()
當你使用 raise from 語句的話,就很清楚的表明拋出的是第二個異常。
最后一個例子中隱藏異常鏈信息。 盡管隱藏異常鏈信息不利于回溯,同時它也丟失了很多有用的調(diào)試信息。 不過萬事皆平等,有時候只保留適當?shù)男畔⒁彩呛苡杏玫摹?/p>
你在一個 except 塊中捕獲了一個異常,現(xiàn)在想重新拋出它。
簡單的使用一個單獨的 rasie語句即可,例如:
>>> def example():
... try:
... int('N/A')
... except ValueError:
... print("Didn't work")
... raise
...
>>> example()
Didn't work
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in example
ValueError: invalid literal for int() with base 10: 'N/A'
>>>
這個問題通常是當你需要在捕獲異常后執(zhí)行某個操作(比如記錄日志、清理等),但是之后想將異常傳播下去。 一個很常見的用法是在捕獲所有異常的處理器中:
try:
...
except Exception as e:
# Process exception information in some way
...
# Propagate the exception
raise
你希望自己的程序能生成警告信息(比如廢棄特性或使用問題)。
要輸出一個警告消息,可使用 warning.warn() 函數(shù)。例如:
import warnings
def func(x, y, logfile=None, debug=False):
if logfile is not None:
warnings.warn('logfile argument deprecated', DeprecationWarning)
...
warn()的參數(shù)是一個警告消息和一個警告類,警告類有如下幾種:UserWarning, DeprecationWarning, SyntaxWarning, RuntimeWarning, ResourceWarning, 或 FutureWarning.
對警告的處理取決于你如何運行解釋器以及一些其他配置。 例如,如果你使用 -W all 選項去運行 Python,你會得到如下的輸出:
bash % python3 -W all example.py
example.py:5: DeprecationWarning: logfile argument is deprecated
warnings.warn('logfile argument is deprecated', DeprecationWarning)
通常來講,警告會輸出到標準錯誤上。如果你想講警告轉(zhuǎn)換為異常,可以使用 -W error 選項:
bash % python3 -W error example.py
Traceback (most recent call last):
File "example.py", line 10, in <module>
func(2, 3, logfile='log.txt')
File "example.py", line 5, in func
warnings.warn('logfile argument is deprecated', DeprecationWarning)
DeprecationWarning: logfile argument is deprecated
bash %
在你維護軟件,提示用戶某些信息,但是又不需要將其上升為異常級別,那么輸出警告信息就會很有用了。 例如,假設你準備修改某個函數(shù)庫或框架的功能,你可以先為你要更改的部分輸出警告信息,同時向后兼容一段時間。 你還可以警告用戶一些對代碼有問題的使用方式。
作為另外一個內(nèi)置函數(shù)庫的警告使用例子,下面演示了一個沒有關閉文件就銷毀它時產(chǎn)生的警告消息:
>>> import warnings
>>> warnings.simplefilter('always')
>>> f = open('/etc/passwd')
>>> del f
__main__:1: ResourceWarning: unclosed file <_io.TextIOWrapper name='/etc/passwd'
mode='r' encoding='UTF-8'>
>>>
默認情況下,并不是所有警告消息都會出現(xiàn)。-W選項能控制警告消息的輸出。 -W all 會輸出所有警告消息,-W ignore忽略掉所有警告,-W error 將警告轉(zhuǎn)換成異常。 另外一種選擇,你還可以使用 warnings.simplefilter() 函數(shù)控制輸出。 always 參數(shù)會讓所有警告消息出現(xiàn),`ignore忽略調(diào)所有的警告,error 將警告轉(zhuǎn)換成異常。
對于簡單的生成警告消息的情況這些已經(jīng)足夠了。 warnings 模塊對過濾和警告消息處理提供了大量的更高級的配置選項。 更多信息請參考 Python 文檔
你的程序奔潰后該怎樣去調(diào)試它?
如果你的程序因為某個異常而奔潰,運行 python3 -i someprogram.py 可執(zhí)行簡單的調(diào)試。 -i 選項可讓程序結(jié)束后打開一個交互式 shell。 然后你就能查看環(huán)境,例如,假設你有下面的代碼:
# sample.py
def func(n):
return n + 10
func('Hello')
運行 python3 -i sample.py 會有類似如下的輸出:
bash % python3 -i sample.py
Traceback (most recent call last):
File "sample.py", line 6, in <module>
func('Hello')
File "sample.py", line 4, in func
return n + 10
TypeError: Can't convert 'int' object to str implicitly
>>> func(10)
20
>>>
如果你看不到上面這樣的,可以在程序奔潰后打開 Python 的調(diào)試器。例如:
>>> import pdb
>>> pdb.pm()
> sample.py(4)func()
-> return n + 10
(Pdb) w
sample.py(6)<module>()
-> func('Hello')
> sample.py(4)func()
-> return n + 10
(Pdb) print n
'Hello'
(Pdb) q
>>>
如果你的代碼所在的環(huán)境很難獲取交互 shell(比如在某個服務器上面), 通??梢圆东@異常后自己打印跟蹤信息。例如:
import traceback
import sys
try:
func(arg)
except:
print('**** AN ERROR OCCURRED ****')
traceback.print_exc(file=sys.stderr)
要是你的程序沒有奔潰,而只是產(chǎn)生了一些你看不懂的結(jié)果, 你在感興趣的地方插入一下 print()語句也是個不錯的選擇。 不過,要是你打算這樣做,有一些小技巧可以幫助你。 首先,traceback.print_stack()函數(shù)會你程序運行到那個點的時候創(chuàng)建一個跟蹤棧。例如:
>>> def sample(n):
... if n > 0:
... sample(n-1)
... else:
... traceback.print_stack(file=sys.stderr)
...
>>> sample(5)
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in sample
File "<stdin>", line 3, in sample
File "<stdin>", line 3, in sample
File "<stdin>", line 3, in sample
File "<stdin>", line 3, in sample
File "<stdin>", line 5, in sample
>>>
另外,你還可以像下面這樣使用 pdb.set_trace() 在任何地方手動的啟動調(diào)試器:
import pdb
def func(arg):
...
pdb.set_trace()
...
當程序比較大二你想調(diào)試控制流程以及函數(shù)參數(shù)的時候這個就比較有用了。 例如,一旦調(diào)試器開始運行,你就能夠使用 print 來觀測變量值或敲擊某個命令比如 w 來獲取追蹤信息。
不要將調(diào)試弄的過于復雜化。一些簡單的錯誤只需要觀察程序堆棧信息就能知道了, 實際的錯誤一般是堆棧的最后一行。 你在開發(fā)的時候,也可以在你需要調(diào)試的地方插入一下 print() 函數(shù)來診斷信息(只需要最后發(fā)布的時候刪除這些打印語句即可)。
調(diào)試器的一個常見用法是觀測某個已經(jīng)奔潰的函數(shù)中的變量。 知道怎樣在函數(shù)奔潰后進入調(diào)試器是一個很有用的技能。
當你想解剖一個非常復雜的程序,底層的控制邏輯你不是很清楚的時候, 插入 pdb.set_trace()這樣的語句就很有用了。
實際上,程序會一直運行到碰到 set_trace() 語句位置,然后立馬進入調(diào)試器。 然后你就可以做更多的事了。
如果你使用 IDE 來做 Python 開發(fā),通常 IDE 都會提供自己的調(diào)試器來替代 pdb。 更多這方面的信息可以參考你使用的 IDE 手冊。
你想測試你的程序運行所花費的時間并做性能測試。
如果你只是簡單的想測試下你的程序整體花費的時間, 通常使用 Unix 時間函數(shù)就行了,比如:
bash % time python3 someprogram.py
real 0m13.937s
user 0m12.162s
sys 0m0.098s
bash %
如果你還需要一個程序各個細節(jié)的詳細報告,可以使用 cProfile 模塊:
bash % python3 -m cProfile someprogram.py
859647 function calls in 16.016 CPU seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
263169 0.080 0.000 0.080 0.000 someprogram.py:16(frange)
513 0.001 0.000 0.002 0.000 someprogram.py:30(generate_mandel)
262656 0.194 0.000 15.295 0.000 someprogram.py:32(<genexpr>)
1 0.036 0.036 16.077 16.077 someprogram.py:4(<module>)
262144 15.021 0.000 15.021 0.000 someprogram.py:4(in_mandelbrot)
1 0.000 0.000 0.000 0.000 os.py:746(urandom)
1 0.000 0.000 0.000 0.000 png.py:1056(_readable)
1 0.000 0.000 0.000 0.000 png.py:1073(Reader)
1 0.227 0.227 0.438 0.438 png.py:163(<module>)
512 0.010 0.000 0.010 0.000 png.py:200(group)
...
bash %
不過通常情況是介于這兩個極端之間。比如你已經(jīng)知道代碼運行時在少數(shù)幾個函數(shù)中花費了絕大部分時間。 對于這些函數(shù)的性能測試,可以使用一個簡單的裝飾器:
# timethis.py
import time
from functools import wraps
def timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
r = func(*args, **kwargs)
end = time.perf_counter()
print('{}.{} : {}'.format(func.__module__, func.__name__, end - start))
return r
return wrapper
要使用這個裝飾器,只需要將其放置在你要進行性能測試的函數(shù)定義前即可,比如:
>>> @timethis
... def countdown(n):
... while n > 0:
... n -= 1
...
>>> countdown(10000000)
__main__.countdown : 0.803001880645752
>>>
要測試某個代碼塊運行時間,你可以定義一個上下文管理器,例如:
from contextlib import contextmanager
@contextmanager
def timeblock(label):
start = time.perf_counter()
try:
yield
finally:
end = time.perf_counter()
print('{} : {}'.format(label, end - start))
下面是使用這個上下文管理器的例子:
>>> with timeblock('counting'):
... n = 10000000
... while n > 0:
... n -= 1
...
counting : 1.5551159381866455
>>>
對于測試很小的代碼片段運行性能,使用 timeit 模塊會很方便,例如:
>>> from timeit import timeit
>>> timeit('math.sqrt(2)', 'import math')
0.1432319980012835
>>> timeit('sqrt(2)', 'from math import sqrt')
0.10836604500218527
>>>
timeit 會執(zhí)行第一個參數(shù)中語句100萬次并計算運行時間。 第二個參數(shù)是運行測試之前配置環(huán)境。如果你想改變循環(huán)執(zhí)行次數(shù), 可以像下面這樣設置 number參數(shù)的值:
>>> timeit('math.sqrt(2)', 'import math', number=10000000)
1.434852126003534
>>> timeit('sqrt(2)', 'from math import sqrt', number=10000000)
1.0270336690009572
>>>
當執(zhí)行性能測試的時候,需要注意的是你獲取的結(jié)果都是近似值。 time.perf_counter()函數(shù)會在給定平臺上獲取最高精度的計時值。 不過,它仍然還是基于時鐘時間,很多因素會影響到它的精確度,比如機器負載。 如果你對于執(zhí)行時間更感興趣,使用 time.process_time() 來代替它。例如:
from functools import wraps
def timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.process_time()
r = func(*args, **kwargs)
end = time.process_time()
print('{}.{} : {}'.format(func.__module__, func.__name__, end - start))
return r
return wrapper
最后,如果你想進行更深入的性能分析,那么你需要詳細閱讀 time 、timeit和其他相關模塊的文檔。 這樣你可以理解和平臺相關的差異以及一些其他陷阱。 還可以參考13.13小節(jié)中相關的一個創(chuàng)建計時器類的例子。
你的程序運行太慢,你想在不使用復雜技術比如 C 擴展或 JIT 編譯器的情況下加快程序運行速度。
關于程序優(yōu)化的第一個準則是“不要優(yōu)化”,第二個準則是“不要優(yōu)化那些無關緊要的部分”。 如果你的程序運行緩慢,首先你得使用14.13小節(jié)的技術先對它進行性能測試找到問題所在。
通常來講你會發(fā)現(xiàn)你得程序在少數(shù)幾個熱點地方花費了大量時間, 不然內(nèi)存的數(shù)據(jù)處理循環(huán)。一旦你定位到這些點,你就可以使用下面這些實用技術來加速程序運行。
很多程序員剛開始會使用 Python 語言寫一些簡單腳本。 當編寫腳本的時候,通常習慣了寫毫無結(jié)構(gòu)的代碼,比如:
# somescript.py
import sys
import csv
with open(sys.argv[1]) as f:
for row in csv.reader(f):
# Some kind of processing
pass
很少有人知道,像這樣定義在全局范圍的代碼運行起來要比定義在函數(shù)中運行慢的多。 這種速度差異是由于局部變量和全局變量的實現(xiàn)方式(使用局部變量要更快些)。 因此,如果你想讓程序運行更快些,只需要將腳本語句放入函數(shù)中即可:
# somescript.py
import sys
import csv
def main(filename):
with open(filename) as f:
for row in csv.reader(f):
# Some kind of processing
pass
main(sys.argv[1])
速度的差異取決于實際運行的程序,不過根據(jù)經(jīng)驗,使用函數(shù)帶來15-30%的性能提升是很常見的。
每一次使用點(.)操作符來訪問屬性的時候會帶來額外的開銷。 它會觸發(fā)特定的方法,比如 __getattribute__() 和 __getattr__() ,這些方法會進行字典操作操作。
通常你可以使用from module import name 這樣的導入形式,以及使用綁定的方法。 假設你有如下的代碼片段:
import math
def compute_roots(nums):
result = []
for n in nums:
result.append(math.sqrt(n))
return result
# Test
nums = range(1000000)
for n in range(100):
r = compute_roots(nums)
在我們機器上面測試的時候,這個程序花費了大概40秒?,F(xiàn)在我們修改compute_roots()函數(shù)如下:
from math import sqrt
def compute_roots(nums):
result = []
result_append = result.append
for n in nums:
result_append(sqrt(n))
return result
修改后的版本運行時間大概是29秒。唯一不同之處就是消除了屬性訪問。 用sqrt() 代替了 math.sqrt() 。 The result.append() 方法被賦給一個局部變量 result_append,然后在內(nèi)部循環(huán)中使用它。
不過,這些改變只有在大量重復代碼中才有意義,比如循環(huán)。 因此,這些優(yōu)化也只是在某些特定地方才應該被使用。
之前提過,局部變量會比全局變量運行速度快。 對于頻繁訪問的名稱,通過將這些名稱變成局部變量可以加速程序運行。 例如,看下之前對于 compute_roots()函數(shù)進行修改后的版本:
import math
def compute_roots(nums):
sqrt = math.sqrt
result = []
result_append = result.append
for n in nums:
result_append(sqrt(n))
return result
在這個版本中,sqrt 從 match模塊被拿出并放入了一個局部變量中。 如果你運行這個代碼,大概花費25秒(對于之前29秒又是一個改進)。 這個額外的加速原因是因為對于局部變量 sqrt 的查找要快于全局變量sqrt