整理 | Rachel責編 | Jane出品 | Python大本營(id:pythonnews)
【導語】程序員每日都在和 debug 相伴。新手程序員需要學習的 debug 手段複雜多樣,設置斷點、查看變量值……一些網站還專門針對debug撰寫了新手教程。老司機們在大型的項目中要 debug 的問題不一樣,模塊衆多、代碼超長,面對大型項目的debug之路道阻且長。針對新手和老手程序員會遇到的不同debug問題,本文推薦了兩個GitHub上的開源debug工具:PySnooper 和 Behold,幫助大家更加優雅、簡潔地 debug 代碼。
在之前的推薦中,營長爲大家介紹過一些有趣的實用工具,包括自動化UI測試工具、代碼修復神器、幫小白快速修復error、pdf翻譯工具、變量命名神器等等。今天,營長要爲大家推薦兩個基於 Python 的 debug 工具:PySnooper 和 Behold,幫助大家對不同規模的項目,有針對性的優雅 debug。
查看變量值,是 debug 過程中常要做的一件事。Python 開發者們除了使用 print 對變量逐個輸出以外,是否還有其他方法可用呢?其實,使用 Print 語句查看變量有時候也很繁瑣:首先需要找到變量所在的代碼行,然後註釋掉部分代碼,再加一行輸出命令;之後再根據原步驟進行復原。這波操作在代碼量較大時就要耗費大量精力了,並且如果忘記復原,或者在復原代碼時出現手誤,甚至可能在 debug 過程中再新加 Bug,着實不值得!
此外,在一些大型項目上,我們有時只需要對項目的部分模塊或代碼行進行調試,但 Python 項目調試的時候需要人工對代碼進行劃分,以滿足調試需求,這就使 debug 變得更困難。
爲了讓大家更專注寫代碼、debug 更輕鬆,營長特別選取了兩個 Github 的 debug 神器:PySnooper 和 Behold,分別推薦給新手和大型代碼項目的老司機。
接下來,先簡單介紹並比較兩個工具的特性,之後再具體講解使用步驟、功能,如果想查看工具源代碼和文檔,可以到文末查看,別忘了給營長點」在看「!
對象不同,簡潔相同
兩個項目有何異同?兩個作者對項目的描述就能輕鬆發現兩者的不同:PySnooper——a poor man's debugger」,針對新手程序員;Behold——爲大型Python項目專門搭建的 debug 工具。
兩個工具都不約而同地把「簡便易用」作爲了首要目標。PySnooper 和 Behold 都是一行代碼搞定:」pip install「。使用上,兩者對查看變量做了針對性地改進,都支持使用一行命令輸出多個變量,不同於以往使用 print 語句的方式。
比較而言,PySnooper 更適用於調試單個函數,對函數變量的更改過程、指向操作所在代碼行上更突出,可以對變量值及值發生改變時所對應的代碼行進行輸出,並將輸出存儲爲文件。而 Behold 更加註重對代碼的整體調試,以及 debug 時對變量的篩選,例如支持對全局變量和局部變量的區分等。
具體而言,PySnooper 的特性包括:
Behold 的特性包括:
PySnooper: 新手程序員救星
1.安裝:使用pip
pip install pysnooper
2.設置需要調試的函數:使用@pysnooper.snoop()
import pysnooper @pysnooper.snoop() def number_to_bits(number): if number: bits = [] while number: number, remainder = divmod(number, 2) bits.insert(0, remainder) return bits else: return [0] number_to_bits(6)
輸出如下:
Starting var:.. number = 6 21:14:32.099769 call 3 @pysnooper.snoop() 21:14:32.099769 line 5 if number: 21:14:32.099769 line 6 bits = [] New var:....... bits = [] 21:14:32.099769 line 7 while number: 21:14:32.099769 line 8 number, remainder = divmod(number, 2) New var:....... remainder = 0 Modified var:.. number = 3 21:14:32.099769 line 9 bits.insert(0, remainder) Modified var:.. bits = [0] 21:14:32.099769 line 7 while number: 21:14:32.099769 line 8 number, remainder = divmod(number, 2) Modified var:.. number = 1 Modified var:.. remainder = 1 21:14:32.099769 line 9 bits.insert(0, remainder) Modified var:.. bits = [1, 0] 21:14:32.099769 line 7 while number: 21:14:32.099769 line 8 number, remainder = divmod(number, 2) Modified var:.. number = 0 21:14:32.099769 line 9 bits.insert(0, remainder) Modified var:.. bits = [1, 1, 0] 21:14:32.099769 line 7 while number: 21:14:32.099769 line 10 return bits 21:14:32.099769 return 10 return bits
3.將上述記錄輸出爲文件,並保存在文件夾:文件命名爲file.log,保存在「/my/log/」文件夾:
@pysnooper.snoop('/my/log/file.log')
4.查看一個或多個非局部變量的值:查看foo.bar, self.whatever變量的改變過程,這兩個變量不在number_to_bits函數中
@pysnooper.snoop(variables=('foo.bar', 'self.whatever'))
5.輸出調試函數所引用的函數的變量更改記錄:
@pysnooper.snoop(depth=2)
6.在緩存中輸出記錄,提高運行速度:
@pysnooper.snoop(prefix='ZZZ ')
1.安裝:使用pip
pip install behold
2.簡單輸出一個或多個變量的改變過程:
from behold import Behold letters = ['a', 'b', 'c', 'd', 'A', 'B', 'C', 'D'] for index, letter in enumerate(letters): # 輸出效果等價於如下代碼 # print('index: {}, letter: {}'.format(index, letter)) Behold().show('index', 'letter')
3.依據變量的值對輸出進行條件篩選:
from behold import Behold letters = ['a', 'b', 'c', 'd', 'A', 'B', 'C', 'D'] for index, letter in enumerate(letters): # 輸出效果等價於如下代碼 # if letter.upper() == letter and index % 2 == 0: # print('index: {}'.format(index)) Behold().when(letter.upper() == letter and index % 2 == 0).show('index')
4.對變量的輸出值給予自定義標籤,提高輸出結果的區分度:這裏依據變量的值分別打「even_uppercase」和「odd_losercase」標籤,附在變量之後
from behold import Behold letters = ['a', 'b', 'c', 'd', 'A', 'B', 'C', 'D'] for index, letter in enumerate(letters): # 輸出效果等價於如下代碼 # if letter.upper() == letter and index % 2 == 0: # print('index: {}, letter:, {}, even_uppercase'.format(index, letter)) # if letter.upper() != letter and index % 2 != 0: # print('index: {}, letter: {} odd_lowercase'.format(index, letter)) Behold(tag='even_uppercase').when(letter.upper() == letter and index % 2 == 0).show('index', 'letter') Behold(tag='odd_lowercase').when(letter.lower() == letter and index % 2 != 0).show('index', 'letter')
5.依據調試變量所在函數的所屬模塊篩選是否輸出變量值:
首先使用behold對函數設定調試規則:
from behold import Behold # 這是一個在代碼庫中常用的自定義函數 def my_function(): x = 'hello' # 這是函數本身的邏輯 # 在「testing」環境時輸出x的值 Behold().when_context(what='testing').show('x') # 僅在「debug」環境時對函數進行調試輸出 if Behold().when_context(what='debugging').is_true(): import pdb; pdb.set_trace()
在另一個代碼模塊中對設定調試規則的函數進行調試:
from behold import in_context # 設置context爲「testing」 @in_context(what='testing') def test_x(): my_function() test_x() # 將輸出'x: hello' # 使用環境管理器設置環境爲「debugging」以進行調試 with in_context(what='debugging'): my_function() # 轉至pdb調試工具
6.輸出對象的部分或全部屬性:使用「with_args」指定調試對象的部分屬性,使用「no_args」輸出調試對象的全部屬性
from behold import Behold, Item item = Item(a=1, b=2, c=3) #輸出對象的部分屬性 Behold(tag='with_args').show(item, 'a', 'b') #輸出對象的全部屬性 Behold(tag='no_args').show(item)
7.依據全局變量和局部變量對輸出進行篩選:
from __future__ import print_function from behold import Behold, Item # 定義全局變量 g = 'global_content' # 定義一個函數,設定局部變量 def example_func(): employee = Item(name='Toby') boss = Item(employee=employee, name='Michael') print('# Can't see global variable') Behold().show('boss', 'employee', 'g') print(' # I can see the the boss's name, but not employee name') Behold('no_employee_name').show(boss) print(' # Here is how to show global variables') Behold().show(global_g=g, boss=boss) # 可以對變量的輸出順序進行調整 print(' # You can force variable ordering by supplying string arguments') Behold().show('global_g', 'boss', global_g=g, boss=boss) print(' # And a similar strategy for nested attributes') Behold().show(employee_name=boss.employee.name) example_func()
8.將輸出存儲爲Pandas.Dataframe格式的數據:需要對變量值的標籤進行定義,標籤將存儲爲變量的鍵值
from __future__ import print_function from pprint import pprint from behold import Behold, in_context, get_stash, clear_stash def my_function(): out = [] for nn in range(5): x, y, z = nn, 2 * nn, 3 * nn out.append((x, y, z)) # 對變量值的標籤進行定義 # 盡在測試x的環境下存儲y和z的值 Behold(tag='test_x').when_context(what='test_x').stash('y', 'z') # 僅在測試y的環境下存儲x和z的值 Behold(tag='test_y').when_context(what='test_y').stash('x', 'z') # 僅在測試z的環境下存儲x和y的值 Behold(tag='test_z').when_context(what='test_z').stash('x', 'y') return out @in_context(what='test_x') def test_x(): assert(sum([t[0] for t in my_function()]) == 10) @in_context(what='test_y') def test_y(): assert(sum([t[1] for t in my_function()]) == 20) @in_context(what='test_z') def test_z(): assert(sum([t[2] for t in my_function()]) == 30) test_x() test_y() test_z() print(' # contents of test_x stash. Notice only y and z as expected') pprint(get_stash('test_x')) print(' # contents of test_y stash. Notice only x and z as expected') pprint(get_stash('test_y')) print(' # contents of test_z stash. Notice only x and y as expected') print(get_stash('test_z'))
也可以對存儲的結果進行清除。
clear_stash()
當該命令的參數爲空時,默認清除所有調試數據的緩存。如果想要指定清除某個或某些參數的調試緩存數據,則需在參數中進行指定。
9.在輸出時使用自定義字典對變量輸出的值進行重新定義:
下例中對變量的值進行了自定義。假設自定義字典中的鍵值爲數據庫索引,下例展示了將該索引轉變爲自定義標籤的方法。
from __future__ import print_function from behold import Behold, Item # 定義Behold的子類以支持自定義的屬性提取 class CustomBehold(Behold): @classmethod def load_state(cls): cls.name_lookup = { 1: 'John', 2: 'Paul', 3: 'George', 4: 'Ringo' } def extract(self, item, name): # 如果沒有加載lookup state,則先進行加載 if not hasattr(self.__class__, 'name_lookup'): self.__class__.load_state() # 抽取變量的值 val = getattr(item, name) # 如果變量是一個Item類變量,則進行值轉換 if isinstance(item, Item) and name == 'name': return self.__class__.name_lookup.get(val, None) # 否則使用Behold默認的轉換函數 else: return super(CustomBehold, self).extract(item, name) # 定義一組Item變量用於測試 items = [Item(name=nn) for nn in range(1, 5)] print(' # Show items using standard Behold class') for item in items: Behold().show(item) print(' # Show items using CustomBehold class with specialized extractor') for item in items: CustomBehold().show(item, 'name', 'instrument')