Python程序員Debug利器,和Print說再見

Python程序員Debug利器,和Print說再見 | 技術頭條

 

整理 | 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 與 Behold 對比:

對象不同,簡潔相同

  • 使用對象不同

兩個項目有何異同?兩個作者對項目的描述就能輕鬆發現兩者的不同:PySnooper——a poor man's debugger」,針對新手程序員;Behold——爲大型Python項目專門搭建的 debug 工具。

  • 安裝與使用

兩個工具都不約而同地把「簡便易用」作爲了首要目標。PySnooper 和 Behold 都是一行代碼搞定:」pip install「。使用上,兩者對查看變量做了針對性地改進,都支持使用一行命令輸出多個變量,不同於以往使用 print 語句的方式。

  • 特性

比較而言,PySnooper 更適用於調試單個函數,對函數變量的更改過程、指向操作所在代碼行上更突出,可以對變量值及值發生改變時所對應的代碼行進行輸出,並將輸出存儲爲文件。而 Behold 更加註重對代碼的整體調試,以及 debug 時對變量的篩選,例如支持對全局變量和局部變量的區分等。

具體而言,PySnooper 的特性包括:

  • 輸出關於某個函數中變量更改的詳細過程記錄,包括變量的值、使變量更改的相關代碼行、更改時間
  • 將上述記錄輸出爲一個.log文件
  • 查一個或多個非局部變量的值
  • 輸出調試函數所引用的函數的變量更改記錄
  • 在緩存中輸出記錄,提高運行速度

Behold 的特性包括:

  • 簡單輸出一個或多個變量的改變過程
  • 依據變量的值對輸出進行條件篩選
  • 對變量的輸出值給予自定義標籤,提高輸出結果的區分度
  • 依據調試變量所在函數的所屬模塊篩選是否輸出變量值
  • 輸出對象的部分或全部屬性
  • 依據全局變量和局部變量對輸出進行篩選
  • 將輸出存儲爲Pandas.Dataframe格式的數據
  • 在輸出時使用自定義字典對變量輸出的值進行重新定義

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 ')

Beholder: 針對大型Python項目的調製工具

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')