- 機械学習の入門データ・セットとしてよく使われているIris Data Setを例に、集計するプログラムを書いてみよう。その際に起きがちなトラブルをデバッグしてみよう。
- なお、本来ならば「要因を発見し、コード修正する」まで行うのがデバッグ作業であるが、ここでは「バグ要因を発見する」ところまでを行う。
- <目次>
-
今回の理想的な流れ
- データセットを用意。
- 1行ずつ読み込み、各行の1つ目の数字のみを抽出する。 *コード例1はここまでを書いたもの。
- 抽出した文字列をfloat型に変換する。
- 抽出した値の平均を求める。
-
データセットを用意(iris.data)
curl -O https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data
-
PyCharmで新規プロジェクトを作成。プロジェクト名の例「exercise_debug」
-
作成したプロジェクト内のフォルダに、iris.dataを移動orコピー。
- プロジェクトの保存場所はPyCharmの左上で確認できる。
-
コード例1: 1行ずつ読み込み、各行の1つ目の数字のみを抽出する。
- 下記コードをコピーし、適当なスクリプト名を付けてプロジェクトに保存。
- re.compile()は、パターンマッチングを高速に実行するための関数。高速にするだけなので、使わなくても良い。
- readlinesを使わずにfor文を回しているが、ファイルサイズが大きい場合にはこちらの書き方の方がベター。readlinesでは、ファイル内のすべての行を一度に読み込み保持するため、メモリを逼迫することがある。
# コード例1: test1.py
import re
filname = 'iris.data'
pattern = r'(^[0-9]*\.[0-9]*),.*'
prog = re.compile(pattern)
data = []
with open(filname, 'r') as fh:
for line in fh:
line = line.rstrip()
result = prog.match(line)
value = result.group(1)
data.append(value)
print(data)- test1.pyを実行すると、以下のように
AttributeErrorとなるはず。
Traceback (most recent call last):
File "/Users/tnal/PycharmProjects/sample/test1.py", line 12, in <module>
value = result.group(1)
AttributeError: 'NoneType' object has no attribute 'group'
- 概要
- エラーが出た箇所に関するドキュメントを参照したり、エラーが出た変数の型を確認することでバグ要因を特定してみる。
- 本来はエラー文を解釈してから探し始めるのが良いが、ここではまだ使い慣れていないライブラリの利用を含んだ例になっているため、ドキュメント参照から始めている。
- step 1: ドキュメントを参照してみる。
- Python標準ライブラリの検索窓から、検索キーを入力して探してみよう。今回は「group」や「re」が該当。
- キーワード候補としては、まずエラー文に出てくる「group」が考えられる。また、groupは正規表現(reライブラリ)にも関連していることが分かっている。このことを踏まえて検索結果を眺めていくと、 re.Match.group が今回の対象になっていそうだと推測できる。
- re.Match.groupのページには、その使い方は説明されているが、NoneType に関する説明は出てこない。
- Python標準ライブラリの検索窓から、検索キーを入力して探してみよう。今回は「group」や「re」が該当。
- step 2: エラー文を解釈してみる。
AttributeError: 'NoneType' object has no attribute 'group'の主語は「NoneType object」である。何か良くわからないが、NoneType型のオブジェクトがあり、そのオブジェクトが group を持っていないというエラーになっている。- エラーが出た行から考えると、group関数を実行使用している変数は result である。このresultには、直前の行でマッチした結果(マッチオブジェクト)が保存されているはずだが、そこがおかしいのだろうか?
- この推測を確認するため、prog.match() についてドキュメントで確認してみよう。
- step 2: ドキュメントを参照してみる。その2。
- Python標準ライブラリの検索窓から、検索キーを入力して探してみよう。今回は「match」や「re」が該当。検索結果一覧から、関連していそうなページであるre.Pattern.matchを参照してみよう。
- ここの解説によると 正規表現とマッチするなら、対応する マッチオブジェクト を返します。文字列がパターンにマッチしなければ None を返します。 とある。NoneTypeとはこの後者に該当しそうだ。
- 実際に None を入力された場合にどうなるか、次のコードで確認してみよう。
- Python標準ライブラリの検索窓から、検索キーを入力して探してみよう。今回は「match」や「re」が該当。検索結果一覧から、関連していそうなページであるre.Pattern.matchを参照してみよう。
# コード例1: test1.py
result = None
print(type(result))
result.group(1)- これで同じエラーが出てくるはずだ。つまり、re.match()によるマッチング結果が該当しない場合に None が返ってくるにも関わらず、そのことを気にせずに「マッチするだろ?1番目にマッチした箇所教えてくれ」というコードを書いてしまっていたために、エラーが起きていた。
- 本来ならこの要因を解消するコードに書き直すところまでがデバッグ作業だが、ここでは要因分析までに留めることにする。(各自で考えてみよう)
- 概要
- 単純にデバッグ実行してみて、そこから得られる情報を元に要因分析してみよう。
- step 1: エラーが出た行をコメントアウトして、実行してみよう。
- こうやる意図:該当行以外が、想定通りに動いているかを確認してみたい。
- step 2: 実際に該当行だけをコメントアウトすると、その次の行で使っている変数 value が NameError となるはず。そこもコメントアウトしてみよう。
- 現状確認
- AttributeErrorが出た行と、その行で初期値を設定していた変数のある行をコメントアウトすると、エラーが無くなり、最後まで動作するようになる。
- ただし、data.append(value)を実行していないため、最後のprint(data)では空のリストが出力されている。これではやりたいことが実現できていない。
- step 3: デバッガを起動してみよう。
- 単にデバッグ実行するだけでも得られる情報が多い。
Consoleタブには、通常実行時の結果に加え、どのようにスタックフレームを辿ってたのか(=Traceback)も記載されている。 *今回はこれらの情報からは要因分析困難だけれども。Debuggerタブには、エラーで停止する直前の状態が保持されている。ここでスタックフレームやその中身(変数の中身)等を確認することが可能。- 同様に、コード上にも変数の中身が出力されている。
- コードやDebuggerタブを眺めて、気になる所はないだろうか? あるなら、そこを更に深掘りする。ないなら、別のアプローチを検討する。
- 単にデバッグ実行するだけでも得られる情報が多い。
- step 4: コードから分かる情報を元に、要因分析。
result に NoneTypeが保存されている。- これは、re.match() によるマッチ結果がある場合には「マッチオブジェクト」が返ってくるが、マッチ結果がない無い(指定したパターンに該当しなかった)場合には「
None」が返ってくるという仕様に基づいた結果。 Noneは type で型確認すると NoneType 型であり、マッチオブジェクトではないため、result.group() を実行しようとしても「result(NoneType)はgroupなんて関数しらないよ」というエラーを出力している。- このエラーが
AttributeError: 'NoneType' object has no attribute 'group'。
- このエラーが
- これは、re.match() によるマッチ結果がある場合には「マッチオブジェクト」が返ってくるが、マッチ結果がない無い(指定したパターンに該当しなかった)場合には「
- resultにNoneTypeに保存されている理由は、その直前の数行を眺めると分かる。ここでは、lineの中身が空だから。
- lineの中身が空(何もない行)というのが何故出てきたのかは、データファイルを眺めてみると分かる。ここでは、ファイルの最後に「空の行」が入っているため。
- データ処理(ファイル処理)をしていると、このような「想定外の箇所」というのは頻繁に出てくる。エラー遭遇時に、問題を切り分けて要因分析できるようになろう。
- 本来ならこの要因を解消するコードに書き直すところまでがデバッグ作業だが、ここでは要因分析までに留めることにする。(各自で考えてみよう)
-
概要
- デバッグは「トラブルが起きた時にその要因を分析し、取り除く行為」のことであり、デバッガ利用にも様々な手段がある。print実行しまくるのも一つの手。
- ここでは別アプローチとして、「処理対象を小さく」してみよう。小さくすることで動作確認しやすくなる(観察しやすくなる)ため、要因分析しやすくなる。
-
step 1: 小さな処理対象を用意する。
- 今回の処理対象は iris.data というテキストファイルである。全ての行にデータが記述されていると想定しているため、試しに「3〜5行程度」だけをコピーして作成した小さなファイルを用意してみよう。ここではファイル名を iris2.data とする。
-
step 2: 用意したファイルに対して実行してみる。
- 小さなファイルでは問題なく処理できるはず。このことから、「小さなファイルには含まれていない、何か別の要因が元々のファイルには含まれている」ことが分かる。
- このことを踏まえ、元のファイルを処理対象に戻し、デバッガを利用してみよう。
-
step 3: breakpointを設定し、デバッグ実行してみる。
- エラーが出た12行目にbreakpointを設定し、デバッグ実行してみよう。
step overを何度か繰り返し、ループ処理を複数回眺めてみると、特に問題なく処理できているようにみえる。run to cursorを実行すると、今回のようなループ処理の場合には「カーソル指定した場所に達するまでまとめて実行」してくれるようになる。これにより、ループ処理1回目〜5回目などをクリックするだけで観察しやすくなる。- が、今回は数十回眺めても問題にはたどり着けない。このような場合には、「breakpointに条件をつける」と楽になる。
-
step 4: conditional breakpointを利用する。
- ここでやりたいことは for文のループ回数を指定して、breakpointを設定したい 。
- ループ回数を数えるために、for文の前に
count = 0を追加しよう。 - ループ処理の最後尾である、data.append()の後で
count += 1を追加しよう。これでループ回数を数えることができるようになった。
- ループ回数を数えるために、for文の前に
- 条件づけしたいbreakpointを、
ctrl+clik(コントロールキー押しながらクリックする。- condition の欄に、
条件を書く。- 例えば
count == 50と書くと、ループを50回繰り返した時点で一時停止してくれる。 - 50回目でもまだ問題ないなら100回、150回、、、と適当な回数を指定して試してみよう。エラーが出たなら、具体的に何回目でそのエラーが出てくるのかを試行錯誤して見つけよう。
- 例えば100回目で普通に実行でき、200回目でエラーが出るなら、その間である150回目はどうなるだろうか? そうやって範囲を狭めていこう。
- 例えば
- condition の欄に、
- ここでやりたいことは for文のループ回数を指定して、breakpointを設定したい 。
-
本来ならこの要因を解消するコードに書き直すところまでがデバッグ作業だが、ここでは要因分析までに留めることにする。(各自で考えてみよう)
- 概要
- 更に別のアプローチとして、「コードの気になる箇所(確認したい箇所)を抜き出し、動作確認する」方法を試してみよう。
- step 1: 「確認したい箇所」を設定する。
- 今回はforブロックの一部でエラーが出た。forブロック全体を確認対象として設定しても良いし、必要最小限に選択しても構わない。今回はそもそも全ての処理が関連しているため削ることが難しいが、エラーが出た行の次(data.append(value))は除外して観察してみよう。
- step 2: 実行するために必要な初期値を設定する。
- いくつかの変数はforブロック内ではなく、他の部分で設定された変数を利用している。例えば、、、
lineはループ処理開始前に設定される。毎回1行分の文字列がstr型で保存されている。(ことを期待している)progは4行目で設定されている、パターン設定用の変数。prog.match() や result.group()は、それらが「reライブラリ」を利用したオブジェクトであることを前提としている。(事前に re ライブラリを読み込んでおく必要がある)
- 上記を踏まえ、まずは以下のコードを実行してみよう。
- 想定通りに動いているだろうか? 動くならば、ここで指定した範囲に関しては動作することを確認できたことになる。
- いくつかの変数はforブロック内ではなく、他の部分で設定された変数を利用している。例えば、、、
# コード例2: test2.py
import re
pattern = r'(^[0-9]*\.[0-9]*),.*'
prog = re.compile(pattern)
line = '5.1,3.5,1.4,0.2,Iris-setosa'
line = line.rstrip()
result = prog.match(line)
value = result.group(1)
print(value)- step 3: 初期値を変更して動作確認してみる。
- 元々のプログラムでは、単にデバッグ実行すると line の中身が '' となっている状態でエラーが起きていた。このことを踏まえ、lineの初期値を '' に変更し、動作確認してみよう。
- この結果、同じエラー
AttributeError: 'NoneType' object has no attribute 'group'が出てくるはずだ。これで エラー状況を再現するために、関連している場所を少し狭めた ことになる。 - 今回の例でいうと、with構文やopen, for文自体には恐らく問題がない(想定通りに動作している)、ということが分かった。このようにコードを分解して動作確認することで、正しい部分を除外し、不確かな部分を抽出していくことでバグの要因となっている箇所を同定する のが、3つ目の「部分的に実行してみる」やり方である。
- この結果、同じエラー
- 元々のプログラムでは、単にデバッグ実行すると line の中身が '' となっている状態でエラーが起きていた。このことを踏まえ、lineの初期値を '' に変更し、動作確認してみよう。
- テキストファイルの最後に改行がない。
- 一部の値が欠落している。
- 文字コードの指定ミス。