程序能一次寫完并正常運行的概率很小,基本不超過1%。總會有各種各樣的bug需要修正。有的bug很簡單,看看錯誤信息就知道,有的bug很復雜,我們需要知道出錯時,哪些變量的值是正確的,哪些變量的值是錯誤的,因此,需要一整套調試程序的手段來修復bug。
第一種方法簡單直接粗暴有效,就是用`print()`把可能有問題的變量打印出來看看:
~~~
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
~~~
執行后在輸出中查找打印的變量值:
~~~
$ python3 err.py
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
~~~
用`print()`最大的壞處是將來還得刪掉它,想想程序里到處都是`print()`,運行結果也會包含很多垃圾信息。所以,我們又有第二種方法。
### 斷言
凡是用`print()`來輔助查看的地方,都可以用斷言(assert)來替代:
~~~
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
~~~
`assert`的意思是,表達式`n != 0`應該是`True`,否則,根據程序運行的邏輯,后面的代碼肯定會出錯。
如果斷言失敗,`assert`語句本身就會拋出`AssertionError`:
~~~
$ python3 err.py
Traceback (most recent call last):
...
AssertionError: n is zero!
~~~
程序中如果到處充斥著`assert`,和`print()`相比也好不到哪去。不過,啟動Python解釋器時可以用`-O`參數來關閉`assert`:
~~~
$ python3 -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
~~~
關閉后,你可以把所有的`assert`語句當成`pass`來看。
### logging
把`print()`替換為`logging`是第3種方式,和`assert`比,`logging`不會拋出錯誤,而且可以輸出到文件:
~~~
import logging
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
~~~
`logging.info()`就可以輸出一段文本。運行,發現除了`ZeroDivisionError`,沒有任何信息。怎么回事?
別急,在`import logging`之后添加一行配置再試試:
~~~
import logging
logging.basicConfig(level=logging.INFO)
~~~
看到輸出了:
~~~
$ python3 err.py
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print 10 / n
ZeroDivisionError: division by zero
~~~
這就是`logging`的好處,它允許你指定記錄信息的級別,有`debug`,`info`,`warning`,`error`等幾個級別,當我們指定`level=INFO`時,`logging.debug`就不起作用了。同理,指定`level=WARNING`后,`debug`和`info`就不起作用了。這樣一來,你可以放心地輸出不同級別的信息,也不用刪除,最后統一控制輸出哪個級別的信息。
`logging`的另一個好處是通過簡單的配置,一條語句可以同時輸出到不同的地方,比如console和文件。
### pdb
第4種方式是啟動Python的調試器pdb,讓程序以單步方式運行,可以隨時查看運行狀態。我們先準備好程序:
~~~
# err.py
s = '0'
n = int(s)
print 10 / n
~~~
然后啟動:
~~~
$ python3 -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'
~~~
以參數`-m pdb`啟動后,pdb定位到下一步要執行的代碼`-> s = '0'`。輸入命令`l`來查看代碼:
~~~
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print 10 / n
~~~
輸入命令`n`可以單步執行代碼:
~~~
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print 10 / n
~~~
任何時候都可以輸入命令`p 變量名`來查看變量:
~~~
(Pdb) p s
'0'
(Pdb) p n
0
~~~
輸入命令`q`結束調試,退出程序:
~~~
(Pdb) q
~~~
這種通過pdb在命令行調試的方法理論上是萬能的,但實在是太麻煩了,如果有一千行代碼,要運行到第999行得敲多少命令啊。還好,我們還有另一種調試方法。
### pdb.set_trace()
這個方法也是用pdb,但是不需要單步執行,我們只需要`import pdb`,然后,在可能出錯的地方放一個`pdb.set_trace()`,就可以設置一個斷點:
~~~
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 運行到這里會自動暫停
print 10 / n
~~~
運行代碼,程序會自動在`pdb.set_trace()`暫停并進入pdb調試環境,可以用命令`p`查看變量,或者用命令`c`繼續運行:
~~~
$ python3 err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print 10 / n
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File "err.py", line 7, in <module>
print 10 / n
ZeroDivisionError: division by zero
~~~
這個方式比直接啟動pdb單步調試效率要高很多,但也高不到哪去。
### IDE
如果要比較爽地設置斷點、單步執行,就需要一個支持調試功能的IDE。目前比較好的Python IDE有PyCharm:
[http://www.jetbrains.com/pycharm/](http://www.jetbrains.com/pycharm/)
另外,[Eclipse](http://eclipse.org/)加上[pydev](http://pydev.org/)插件也可以調試Python程序。
### 小結
寫程序最痛苦的事情莫過于調試,程序往往會以你意想不到的流程來運行,你期待執行的語句其實根本沒有執行,這時候,就需要調試了。
雖然用IDE調試起來比較方便,但是最后你會發現,logging才是終極武器。
### 參考源碼
[do_assert.py](https://github.com/michaelliao/learn-python3/blob/master/samples/debug/do_assert.py)
[do_logging.py](https://github.com/michaelliao/learn-python3/blob/master/samples/debug/do_logging.py)
[do_pdb.py](https://github.com/michaelliao/learn-python3/blob/master/samples/debug/do_pdb.py)
- 關于
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用slots
- 使用@property
- 多重繼承
- 定制類
- 使用枚舉類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- StringIO和BytesIO
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模塊
- PIL
- virtualenv
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 異步IO
- 協程
- asyncio
- aiohttp
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫Web App骨架
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 編寫配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- FAQ
- 期末總結