2.41. デバッグ¶
カメラ上で失敗するスクリプトのほとんどは、次の3つのいずれかの形で失敗します。例外を送出するか、誤った値を生成するか、ハングするかです。それぞれに対して異なるツールのセットがあります。
2.41.1. トレースバックを読む¶
スクリプトが例外を送出し、それを処理するものがない場合、REPLまたはIDEは トレースバック を表示します。これは、最も外側のスクリプトから例外を送出した行まで連なる呼び出しチェーンの記録です。
トレースバックは下から上へ読みます。
一番下の行は例外クラスとそのメッセージを示します(
ValueError: invalid literal for int()...)。その上にある各
File "...", line N, in <name>ブロックは フレーム です。上に行くほど1つ深い呼び出しを表します。最上部のフレームはスクリプトが開始した場所であり、最下部のフレームはエラーが発生した場所です。
まず一番下を読んで 何が おかしかったのかを把握し、次に上へとたどってスクリプトが どのように そこに至ったのかを確認します。行番号はスクリプト内の正確なソース位置を指し示します。
2.41.2. printによるデバッグ¶
スクリプトが何をしているのかを突き止める最も手っ取り早い方法は、疑わしい値を出力することです。3つの組み込み関数がprintをより役立つものにします。
repr()-- 値の開発者向け文字列を返します。print(repr(value))は"5"と5を、あるいはNoneと"None"を区別できますが、これは単純なprint()ではできません。type()-- 値のクラスを返します。print(type(value))は、「int であるはずの」変数が実はひそかに文字列になっていないかを調べる方法です。len()-- シーケンスやコレクションの長さです。バグの意外なほど多くが、off-by-one(1個ずれ)やサイズの不一致の問題です。
print("got:", repr(value), "type:", type(value), "len:", len(value))
気になる各分岐の中に print を入れてみましょう。if の両方の枝、各 except ブロック、一度も実行されていないと疑っているループの本体などです。コストは1行の出力にすぎず、その価値は、自分が実行されている と思っている コードパスが、実際に実行されているものかどうかを突き止められる点にあります。
2.41.3. オブジェクトを調べる¶
「このものに対して何ができるのか」に答える2つの組み込み関数があります。
dir()-- オブジェクト上に定義されているすべての名前のリストを返します。メソッド、属性、ダンダー、その他すべてです。help()-- 関数、メソッド、クラスのドキュメント文字列(CPythonではシグネチャも)を表示します。
両者を組み合わせて使います。dir で名前を見つけ、help でそれが何をするのかを調べます。
2.41.3.1. dirで名前を見つける¶
>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delitem__',
'__eq__', '__ge__', ..., 'append', 'clear', 'copy',
'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort']
リストの最初のまとまりは、あらゆるオブジェクトから継承されるダンダーメソッドです。注目すべき名前は通常その後にあります。dir はあらゆるものに対して機能します。クラス、インスタンス、モジュール、組み込み型などです。
>>> import json
>>> dir(json)
['__name__', 'dump', 'dumps', 'load', 'loads']
この2つ目の形式は、REPLを離れることなく、モジュールが実際に公開しているトップレベルの名前を調べる方法です。
2.41.3.2. helpで調べる¶
dir で候補が浮かび上がったら、help がそれを説明します。
>>> help(str.split)
split(sep=None, maxsplit=-1)
Return a list of the words in the string, ...
MicroPythonでは help はCPythonよりも簡素です。シグネチャだけのこともあれば、1行のドキュメント文字列だけのこともあり、組み込みのC関数では何も出ないこともあります。それでも、IDEのツールチップがすぐ近くにないときには手早い手助けになります。
2.41.4. 何かがハングしたとき¶
戻ってこないスクリプトは、例外を送出するものよりも診断が難しくなります。よくある原因は次のとおりです。
条件が決して false にならない
whileループ。各反復でループ変数を出力してみましょう。値が変化していなければ、ループ本体にバグがあります。決して来ない入力を待つブロッキング呼び出し。空のキューからの読み取りや、終わりのないsleepなどです。呼び出しの前後を print で挟んで、スクリプトがどの行で止まっているのかを確認します。
無限再帰。最終的に発生したときのトレースバック(
RecursionErrorを伴う)は、たいていその箇所をぴたりと指し示します。
ハングしたスクリプトに対して最も効果的な復旧手段は、IDEの stop ボタンです。これはUSB経由でスクリプトに KeyboardInterrupt を送ります。割り込みは現在実行中の行でのトレースバックとして現れ、それはしばしば戻ってこない当の行そのものです。
注釈
あらゆる診断でもハングが解消しない場合、つまりスクリプトが正しく見え、割り込みのトレースバックが自分のスクリプトではなく組み込みやファームウェアのコードを指している場合、あるいは同じコードが以前のファームウェアビルドでは動作していた場合、その原因はスクリプトのバグではなくファームウェアのバグかもしれません。スクリプトをハングを再現する最小限のものまで削減し、OpenMV フォーラム に報告を投稿してください。ファームウェアのバージョン、実行したボード、削減後のスクリプトを含めてください。
2.41.5. 出荷前に診断を取り除く¶
開発中の戦略的な print は非常に有用ですが、本番スクリプトに残された100個もの print 呼び出しは出力を散らかし、本来の処理が使えるはずのヒープを消費します。バグが修正できたら、print を取り除く(あるいはオフに切り替えられるデバッグフラグの後ろに隠す)ようにしましょう。
コードパスに長期的に残しておくべき診断には、print() から logging モジュールへ切り替えます。これは各メッセージに レベル (debug、info、warning、error)を付与し、単一の設定で本番環境では目立たないものを黙らせることができます。
import logging
log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")
ロガーのレベルを logging.WARNING に設定すると、info と debug の呼び出しは実質的にコストがゼロになり(メッセージ文字列が構築されることはありません)、行をコメントアウトする必要もありません。これにより logging は 恒久的な 診断に適したツールとなります。一方、生の print は 使い捨ての 診断には十分です。