一覧PG-18-1 · デバッグと小さなテスト

デバッガによる不具合の調査

単価120×個数3と、単価200×個数2の合計は760になるはずですが、試料は325を返します。Pythonのpdbで先頭行に停止した状態から始め、条件付きブレークポイント、ステップ実行、監視式、呼出し履歴で原因を特定します。

説明

合計が期待した値にならないときは、入力、関数へ渡した値、戻り値、累積値のどこで差が生じるかを確認します。デバッガは実行を途中で止め、変数や呼出し元を調べるための道具です。

単価120×個数3と、単価200×個数2の合計は760になるはずですが、試料は325を返します。Pythonのpdbで先頭行に停止した状態から始め、条件付きブレークポイント、ステップ実行、監視式、呼出し履歴で原因を特定します。

1停止中のコードを確認

現在はcart.pyの先頭行を実行する直前です。行番号付きで全体を表示します。

list 1, 121〜12行目を表示

line_totalが一行の金額、totalが合計を計算します。7行目でline_totalの戻り値をamountへ加えます。->は次に実行する位置です。

2条件付きで停止する場所を指定

7行目に到達し、quantityが3のときだけ停止するブレークポイントを置きます。

break 77行目に停止点, quantity == 3個数3のときだけ停止

Breakpoint 1が設定されます。条件は7行目に到達した時点の変数で評価されます。まだこの行の加算は実行しません。

3指定した行まで実行

continueで実行を再開します。

continue条件に一致する停止点まで進む

totalの7行目で止まります。この表示はamountへ加えた後ではなく、これからline_totalを呼ぶ位置です。

4代入前の値と監視式

引数と累積値を表示し、このフレームのamountを監視します。;;はpdbの複数コマンドの区切りです。

p (price, quantity, amount)現在の局所変数を表示 ;; display amountこのフレームの値を監視

単価120、個数3、累積値0です。displayは停止時に式を再評価し、値が変わった場合に表示します。観察で状態を変えないよう、ここでは単純な変数参照だけを使います。

5関数の中へ入る

stepで呼出し先のline_totalへ進みます。

step呼出し先へ入る

--Call--が表示され、line_totalへ入りました。呼出し前後だけを見たい場合のnextと異なり、stepでは呼出し先の内部を調べられます。

6計算行へ進む

nextで同じフレーム内の次の行へ進みます。

next同じ関数の次の行へ進む

2行目のreturn price + quantityを実行する直前です。ここで値を確認すれば、計算前の引数と式を対応させられます。

7引数と仕様からの期待値

関数の引数を表示し、仕様どおり単価と個数を掛けた値も評価します。

args現在の関数の引数 ;; p price * quantity仕様に基づく計算

引数は120と3で、掛け算なら360です。ソースの+は単価と個数を足しており、仕様の掛け算と一致しません。期待値を誤った関数の再呼出しで作らず、仕様の計算と照合しています。

8呼出し元までの履歴

whereで呼出し履歴を表示します。

where呼出し履歴を表示

cart.pyの11行目からtotalの7行目を経由し、line_totalの2行目へ来ています。先頭のbdb.runはデバッガの実行部分です。>が現在選択中のフレームで、各フレームは別の局所変数を持ちます。

9呼出し元の変数を確認

upでtotalのフレームを選び、amountを確認してからdownでline_totalへ戻ります。

up呼出し元のフレームを選ぶ ;; p amount呼出し元の局所変数 ;; down呼出し先へ選択を戻す

呼出し元のamountはまだ0です。フレームの選択は観察する局所変数を切り替える操作であり、処理を実行して過去へ戻す操作ではありません。

10戻り値が決まるまで実行

returnで現在の関数が戻るところまで進めます。

return関数の戻りまで実行

--Return--と123が表示されます。120+3の値であり、期待した360ではありません。ここで入力の取り違えではなく、関数内の式に原因があると確認できます。

11戻り値を累積へ反映

特殊な__return__で戻り値を確認し、nextで呼出し元の続きを進めます。

p __return__停止中の戻り値 ;; next呼出し元で加算した後へ進む

123が表示され、その後amountの監視値も0から123へ変わります。関数の戻り値が呼出し元の加算へ使われたことが分かります。

12残りの処理を実行

continueで残りを実行します。

continue残りを実行して出力を確認

最終出力は325です。二行目も200+2=202なので、123+202になっています。修正対象はline_totalの+で、仕様どおり*へ変更し、入力120・3なら360、合計なら760になることを再確認する必要があります。

まとめ

停止位置は通常、その行を実行する前です。変数の値を代入前後で混同しないよう確認します。

stepは呼出し先へ入り、nextは同じフレームで進みます。フレーム選択と実行の進行は別の操作です。

入力・期待値・戻り値・累積値を照合すると、どの式で仕様と異なる値になったかを特定できます。

操作履歴と解説を振り返れます。

Python 3.15 / pdb

1
list 1, 12
  1  ->	def line_total(price, quantity):
  2  	    return price + quantity
  3  	
  4  	def total(rows):
  5  	    amount = 0
  6  	    for price, quantity in rows:
  7  	        amount += line_total(price, quantity)
  8  	    return amount
  9  	
 10  	rows = [(120, 3), (200, 2)]
 11  	result = total(rows)
 12  	print(result)
2
break 7, quantity == 3
Breakpoint 1 at /lab/cart.py:7
3
continue
> /lab/cart.py(7)total()
-> amount += line_total(price, quantity)
4
p (price, quantity, amount) ;; display amount
(120, 3, 0)
display amount: 0
5
step
--Call--
> /lab/cart.py(1)line_total()
-> def line_total(price, quantity):
6
next
> /lab/cart.py(2)line_total()
-> return price + quantity
7
args ;; p price * quantity
price = 120
quantity = 3
360
8
where
  /opt/homebrew/Cellar/python@3.15/3.15.0_1/Frameworks/Python.framework/Versions/3.15/lib/python3.15/bdb.py(904)run()
-> exec(cmd, globals, locals)
  /lab/cart.py(11)<module>()
-> result = total(rows)
  /lab/cart.py(7)total()
-> amount += line_total(price, quantity)
> /lab/cart.py(2)line_total()
-> return price + quantity
9
up ;; p amount ;; down
> /lab/cart.py(7)total()
-> amount += line_total(price, quantity)
0
> /lab/cart.py(2)line_total()
-> return price + quantity
10
return
--Return--
> /lab/cart.py(2)line_total()->123
-> return price + quantity
11
p __return__ ;; next
123
> /lab/cart.py(6)total()
-> for price, quantity in rows:
display amount: 123  [old: 0]
12
continue
325

演習完了