Python Debug(調(diào)試)的終極指南
即使您編寫(xiě)了清晰可讀的代碼,即使您是非常有經(jīng)驗(yàn)的開(kāi)發(fā)人員,奇怪的bug也不可避免地會(huì)出現(xiàn),您將需要以某種方式調(diào)試它們。很多人使用一堆print語(yǔ)句來(lái)查看代碼中發(fā)生了什么。這種方法遠(yuǎn)不是理想的,有更好的方法可以找出代碼的錯(cuò)誤所在,本文將探討其中一些問(wèn)題和應(yīng)對(duì)方法。
日志是必須的
如果在編寫(xiě)應(yīng)用程序時(shí)沒(méi)有設(shè)置日志記錄,那么您最終會(huì)后悔的。應(yīng)用程序中沒(méi)有任何日志會(huì)使故障排除變得非常困難。幸運(yùn)的是,在Python中,建立基本的日志程序非常簡(jiǎn)單:
- import logging
- logging.basicConfig(
- filename='application.log',
- level=logging.WARNING,
- format= '[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',
- datefmt='%H:%M:%S'
- )
- logging.error("Some serious error occurred.")
- logging.warning('Function you are using is deprecated.')
這就是所有你需要開(kāi)始寫(xiě)日志的文件,它看起來(lái)像這樣,你可以找到文件的路徑使用logger . getloggerclass ().root.handlers[0].baseFilename):
- [12:52:35] {<stdin>:1} ERROR - Some serious error occurred.
- [12:52:35] {<stdin>:1} WARNING - Function you are using is deprecated.
這種設(shè)置看起來(lái)似乎已經(jīng)足夠好了(通常情況下也是如此),但是擁有配置良好、格式化、可讀的日志可以使您的工作變得更加容易。改進(jìn)和擴(kuò)展配置的一種方法是使用被logger讀取的.ini或.yaml文件。舉個(gè)例子,你可以在配置中做什么:
- version: 1
- disable_existing_loggers: true
- formatters:
- standard:
- format: "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"
- datefmt: '%H:%M:%S'
- handlers:
- console: # handler which will log into stdout
- class: logging.StreamHandler
- level: DEBUG
- formatter: standard # Use formatter defined above
- stream: ext://sys.stdout
- file: # handler which will log into file
- class: logging.handlers.RotatingFileHandler
- level: WARNING
- formatter: standard # Use formatter defined above
- filename: /tmp/warnings.log
- maxBytes: 10485760 # 10MB
- backupCount: 10
- encoding: utf8
- root: # Loggers are organized in hierarchy - this is the root logger config
- level: ERROR
- handlers: [console, file] # Attaches both handler defined above
- loggers: # Defines descendants of root logger
- mymodule: # Logger for "mymodule"
- level: INFO
- handlers: [file] # Will only use "file" handler defined above
- propagate: no # Will not propagate logs to "root" logger
在python代碼中使用這種擴(kuò)展的配置將很難導(dǎo)航、編輯和維護(hù)。將內(nèi)容保存在YAML文件中,可以通過(guò)非常特定的設(shè)置(如上面的設(shè)置)更容易地設(shè)置和調(diào)整多個(gè)日志記錄器。
在文件中有了配置,意味著我們需要加載。最簡(jiǎn)單的方法做與YAML文件:
- import yaml
- from logging import config
- with open("config.yaml", 'rt') as f:
- config_data = yaml.safe_load(f.read())
- config.dictConfig(config_data)
Python logger實(shí)際上并不直接支持YAML文件,但是它支持字典配置,可以使用YAML .safe_load輕松地從YAML創(chuàng)建字典配置。如果您更傾向于使用舊的.ini文件,那么我只想指出,對(duì)于新應(yīng)用程序,根據(jù)文檔,推薦使用字典configs。
__repr__ 可讀的日志
對(duì)代碼進(jìn)行簡(jiǎn)單的改進(jìn),使其更具可調(diào)試性,可以在類中添加__repr__方法。如果你不熟悉這個(gè)方法-它所做的只是返回一個(gè)類實(shí)例的字符串表示。使用__repr__方法的最佳實(shí)踐是輸出可用于重新創(chuàng)建實(shí)例的文本。例如:
- class Circle:
- def __init__(self, x, y, radius):
- self.x = x
- self.y = y
- self.radius = radius
- def __repr__(self):
- return f"Rectangle({self.x}, {self.y}, {self.radius})"
- ...
- c = Circle(100, 80, 30)
- repr(c)
- # Circle(100, 80, 30)
除了__repr__,在調(diào)用print(實(shí)例)時(shí),執(zhí)行__str__方法也是一個(gè)好主意。有了這兩種方法,你可以通過(guò)打印你的變量得到很多信息。
針對(duì)字典的__missing__方法
如果出于某種原因需要實(shí)現(xiàn)自定義dictionary類,那么在嘗試訪問(wèn)一些實(shí)際上不存在的密鑰時(shí),您可能會(huì)遇到一些由keyerror引起的錯(cuò)誤。為了避免在代碼中到處查看丟失了哪個(gè)鍵(key),你可以實(shí)現(xiàn)特殊的__miss__方法,每次KeyError被提出時(shí)調(diào)用。
- class MyDict(dict):
- def __missing__(self, key):
- message = f'{key} not present in the dictionary!'
- logging.warning(message)
- return message # Or raise some error instead
上面的實(shí)現(xiàn)非常簡(jiǎn)單,只返回和記錄丟失鍵的消息,但是您還可以記錄其他有價(jià)值的信息,以便了解代碼中出現(xiàn)了什么問(wèn)題。
調(diào)試崩潰的應(yīng)用程序
如果您的應(yīng)用程序在您有機(jī)會(huì)了解其中發(fā)生了什么之前就崩潰了,那么您可能會(huì)發(fā)現(xiàn)這個(gè)技巧非常有用。
使用-i參數(shù)運(yùn)行應(yīng)用程序(python3 -i app.py)會(huì)導(dǎo)致程序一退出就啟動(dòng)交互式shell。此時(shí),您可以檢查變量和函數(shù)。
如果這還不夠好,您可以帶一個(gè)更強(qiáng)大的工具 - pdb - Python調(diào)試器。pdb有很多特性,可以單獨(dú)寫(xiě)一篇文章來(lái)說(shuō)明。但這里有一個(gè)例子和最重要的部分的綱要。讓我們先看看崩潰腳本:
- # crashing_app.py
- SOME_VAR = 42
- class SomeError(Exception):
- pass
- def func():
- raise SomeError("Something went wrong...")
- func()
現(xiàn)在,如果我們用-i參數(shù)運(yùn)行它,我們就有機(jī)會(huì)調(diào)試它:
- # Run crashing application
- ~ $ python3 -i crashing_app.py
- Traceback (most recent call last):
- File "crashing_app.py", line 9, in <module>
- func()
- File "crashing_app.py", line 7, in func
- raise SomeError("Something went wrong...")
- __main__.SomeError: Something went wrong...
- >>> # We are interactive shell
- >>> import pdb
- >>> pdb.pm() # start Post-Mortem debugger
- > .../crashing_app.py(7)func()
- -> raise SomeError("Something went wrong...")
- (Pdb) # Now we are in debugger and can poke around and run some commands:
- (Pdb) p SOME_VAR # Print value of variable
- 42
- (Pdb) l # List surrounding code we are working with
- 2
- 3 class SomeError(Exception):
- 4 pass
- 5
- 6 def func():
- 7 -> raise SomeError("Something went wrong...")
- 8
- 9 func()
- [EOF]
- (Pdb) # Continue debugging... set breakpoints, step through the code, etc.
上面的調(diào)試會(huì)話非常簡(jiǎn)單地展示了使用pdb可以做什么。程序結(jié)束后,我們進(jìn)入交互式調(diào)試會(huì)話。首先,導(dǎo)入pdb并啟動(dòng)調(diào)試器。此時(shí),我們可以使用所有pdb命令。作為上面的示例,我們使用p命令打印變量,使用l命令列出代碼。大部分時(shí)間你可能會(huì)想要設(shè)置斷點(diǎn),可以與b LINE_NO和運(yùn)行程序,直到斷點(diǎn)(c),然后繼續(xù)與年代,逐頁(yè)瀏覽功能的選擇可能與w。
堆棧跟蹤
假設(shè)您的代碼是運(yùn)行在遠(yuǎn)程服務(wù)器上的Flask或Django應(yīng)用程序,在那里您無(wú)法獲得交互式調(diào)試會(huì)話。在這種情況下,你可以使用traceback和sys包來(lái)了解你的代碼中失敗的地方:
- import traceback
- import sys
- def func():
- try:
- raise SomeError("Something went wrong...")
- except:
- traceback.print_exc(file=sys.stderr)
在運(yùn)行時(shí),上面的代碼將打印引發(fā)的最后一個(gè)異常。除了打印異常,您還可以使用traceback包來(lái)打印stacktrace (traceback. print_stack())或提取原始堆棧幀,格式化它并進(jìn)一步檢查它(traceback. format_list(traceback.extract_stack()))。
在調(diào)試期間重新加載模塊
有時(shí),您可能在交互式shell中調(diào)試或試驗(yàn)?zāi)承┖瘮?shù),并經(jīng)常對(duì)其進(jìn)行更改。為了使運(yùn)行/測(cè)試和修改的循環(huán)更容易,您可以運(yùn)行importlib.reload(模塊),以避免在每次更改后重新啟動(dòng)交互會(huì)話:
- >>> import func from module
- >>> func()
- "This is result..."
- # Make some changes to "func"
- >>> func()
- "This is result..." # Outdated result
- >>> from importlib import reload; reload(module) # Reload "module" after changes made to "func"
- >>> func()
- "New result..."
這個(gè)技巧更多的是關(guān)于效率而不是調(diào)試。能夠跳過(guò)一些不必要的步驟,使您的工作流程更快、更高效總是很好的。一般來(lái)說(shuō),不時(shí)地重新加載模塊是一個(gè)好主意,因?yàn)樗梢詭椭苊庹{(diào)試已經(jīng)被修改了很多次的代碼。