プログラミングでぶちあたる壁
プログラミングをしていると必ず、『プログラムが動かない』、『動かない原因がわからなく先に進めない』という壁にぶちあたり、なかなか先に進めない状態となります。
小生も20年以上もの長年、プログラミングに携わり、このようなケースはゴマンと経験して来ており、分からなくて脳汁を大量に流し、イライラして、放り出し、放心状態になるなど、とにかく苦労しました。
しかし、その苦労の末、エラーや、エラーとはならない問題が3種類しかなく、解決に導くには5種類程度の方法で進めれば良いことに気づきました。
本記事では、どのようにして問題箇所を特定するかを説明します。
問題の発生パターンと解決までの道筋
問題が発生するパターンには大きく以下の3つのパターンがあります。
それぞれについて、解決への道筋がありますので、リンクをクリックしてお進みください。
・ プログラムがエラーを出している場合
”エラーメッセージの意味を良く理解する“以降に進みます。
・ プログラムがエラーを出さずに突然終了している場合
”問題の発生元を探る”以降に進みます。
・ プログラムが終了せずに動き続けている場合
プログラムを終了させた後に、”問題の発生元を探る”以降に進みます。
しかし、同じ問題が再現しない可能性があるなど、
どうしてもプログラムを終了させたくない理由がある場合は、
”プロセスにアタッチする“に進みます。
エラーメッセージの意味を良く理解する
エラーメッセージを『出たー!』と言って流してしまうのではなく、テキストエディターなどに貼り付けて、メッセージの意味を良く理解してましょう。
英語で出力されることが多々ありますので、ブラウザの英語翻訳で翻訳しましょう。
また、今後同じ問題が遭遇した場合に備えて、テキストエディターの内容はどこかに保存しておくことを心がけましょう。
例1
>>> pritn(“Hello”)
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
NameError: name ‘pritn’ is not defined
“NameError: name ‘pritn’ is not defined”
→ ”名前エラー:名前 ‘pritn’は定義されていません”
ということから、pritnがprintの綴り間違いであることがわかります。
例2
>>> filename = “tmp.txt”
>>> fd = open(filename,”r”)
Traceback (most recent call last):
File “”, line 1, in
FileNotFoundError: [Errno 2] No such file or directory: ‘tmp.txt’
“FileNotFoundError: [Errno 2] No such file or directory: ‘tmp.txt'”
→ ”ファイルが見つからないエラー:[Errno 2] そのようなファイルまたはディレクトリはありません:’tmp.txt'”
この例は、tmp.txtというファイル名が間違っているか、tmp.txtというファイルを作っていないこと(プログラムの問題ではない)が原因の可能性もあります。
Webで調べる
誰もが行うことですが、Webで調べます。
メッセージの意味を理解せずに、いきなりここに来る方が多いのですが、あまり推奨できません。
メッセージの意味が分かっていないと、以下の副作用があるからです。
・ Webで調べた解決策が必ずしも合っているとは限らない
・ 次に同じメッセージに遭遇しても忘れていることが多い
・ エラーメッセージに対する勘が養われない
前出の例2はWebで検索しなければ、なかなか原因がわかるものではありません。
ただし、一度、メッセージの意味を考えた後にWebで調べると、なぜ、そのようなメッセージを出したかが理解でき、次に備えられます。
例2
>>> a=[]
>>> a[1]=200
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
IndexError: list assignment index out of range
”IndexError: list assignment index out of range”
→ ”インデックスエラー:リスト割り当てインデックスが範囲外です”
インデックスとは索引のことで、a[1]でいうところの1です。
pythonのリスト(*1)はインデックスを付けてデータを管理しますが、数字が範囲外になっていると言っていますが、エラーの意味が分かりません。
そのためWeb検索した結果、a[1]にまだ値が入っていないため、入っていない値を参照しようとしてエラーになっているという説明がありました。
この例では、最初のa=[ ] でが、何も値は入れていません。
これは、a=[ ] で、空の定義をしているだけで、
他の言語でもよくある a[1] = 200 という記述で、データを入れられると勘違いしたためです。
そのため、Webサイトの説明そのままの対処では解決しません。
この例の場合は、リストにデータを追加するところで、append( )が必要でした。
>>> a=[]
>>> a.append(200)
>>> a.append(300)
>>> a
[200, 300]
何をして問題に至ったかを書き出してみる
問題が発生するまでの間に何をしていたかを時系列に書き出してみます。
すると、ヒントになるものが見えてくる可能性は高いのです。
プログラムを修正したのか、設定を変更したのかなど、できる限り書き出してみましょう。
プログラムを変更して問題を突き止める
ここまでのケースで解決できれば良いのですが、大半のケースでは、エラーの発生元がどこかが分からず、長い時間をかけて調査を行うことになります。
このような場合には以下のいずれかを試みます。
- 修正を戻して実行してみる
- print( )などの表示を埋め込んで実行してみる
- デバッガを使ってステップ実行してみる
1. 修正を戻して実行してみる
修正を繰り返してきたどこかの時点でエラーが発生するようになったはずです。
そのため、エラーが発生しなくなる時点まで、修正をさかのぼって実行してみることで、エラーの発生元がわかります。
また、修正した内容が何十~何百行にもなる場合、
・ 修正量の半分の時点まで戻して実行してみる
・ それでも依然としてエラーが発生する場合は、さらに半分の時点まで戻す
という作業を行って、エラーの発生元を突き止めます。
2. print( )などの表示を埋め込んで実行してみる
昔ながらの古典的手法ですが、シンプルで今でも現役な手法です。
コマンドプロンプトやシェル上で実行するプログラムの場合や、コンソール画面に出力できる環境の場合は、エラーが発生したところまでの主要な箇所にprint( )などの出力関数を埋め込んで実行します。
中には、動かしっぱなしにする常駐プロセスのように、 print( )のような出力関数の出力結果がどこにも出力されないケースがあります。
その場合には、ファイルへの出力で代用します。
例3
下記のプログラムを実行すると、
#!/usr/bin/python
# -*- coding : cp932 -*-
import sys
import os
# "data.txt"ファイルをオープンする
fd = open("data.txt","r")
while True :
# 1行読み込む
line = fd.readline()
if line == "" :
# 空行(ファイルの終わり)が見つかったのでループを抜ける
break
# 行を","で分割する
linesplt = line.split(",")
# 行の先頭の数字を100倍して画面に出力する
print(str(int(linesplt[0]) * 100))
途中で下記のエラーとなりました。
user@computer$ ./tmp.py
100
200
Traceback (most recent call last): File “tmp.py”, line 21, in print(str(int(linesplt[0]) * 100)); ValueError: invalid literal for int() with base 10: ”
print( )を入れてみます。
#!/usr/bin/python
# -*- coding : cp932 -*-
import sys
import os
print("No.1")
# "data.txt"ファイルをオープンする
fd = open("data.txt","r")
while True :
print("No.2")
# 1行読み込む
line = fd.readline()
if line == "" :
# 空行(ファイルの終わり)が見つかったのでループを抜ける
break
print("No.3")
# 行を","で分割する
linesplt = line.split(",")
print("No.4")
# 行の先頭の数字を100倍して画面に出力する
print(str(int(linesplt[0]) * 100))
実行してみました。
user@computer$ python3 tmp.py
No.1
No.2
No.3
No.4
100
No.2
No.3
No.4
200
No.2
No.3
No.4
Traceback (most recent call last):
File “tmp.py”, line 26, in
print(str(int(linesplt[0]) * 100));
ValueError: invalid literal for int() with base 10: ”
3回めのNo.4が表示されています。
2回正しく動作しており、結果を表示するところでエラーになっているため、data.txt のデータが怪しそうです。
data.txt のデータも print( )で表示させると、原因が特定できます。
以下は、さらに改良を加えたものです。コメントをdbg( ) という関数にして、print( )をするようにしたものです。
is_debug = Trueで表示され、Falseで非表示になります。
#!/usr/bin/python
# -*- coding : cp932 -*-
import sys
import os
def dbg(str):
global is_debug
if is_debug == True :
print(str)
is_debug = True
dbg( '"data.txt"ファイルをオープンする' )
fd = open("data.txt","r")
while True :
dbg( "1行読み込む" )
line = fd.readline()
if line == "" :
dbg( "空行(ファイルの終わり)が見つかったのでループを抜ける" )
break
dbg( '行を","で分割する' )
linesplt = line.split(",")
dbg( "行の先頭の数字を100倍して画面に出力する" )
dbg( "linesplt[0] = '" + linesplt[0] + "'" )
print(str(int(linesplt[0]) * 100))
実行結果は以下のとおり、わかりやすくなります。
user@computer$ python3 tmp.py
“data.txt”ファイルをオープンする
1行読み込む
行を”,”で分割する
行の先頭の数字を100倍して画面に出力する
linesplt[0] = ‘1’
100
1行読み込む
行を”,”で分割する
行の先頭の数字を100倍して画面に出力する
linesplt[0] = ‘2’
200
1行読み込む
行を”,”で分割する
行の先頭の数字を100倍して画面に出力する
linesplt[0] = ”
Traceback (most recent call last):
File “tmp.py”, line 29, in
print(str(int(linesplt[0]) * 100));
ValueError: invalid literal for int() with base 10: ”
3. デバッガを使ってステップ実行してみる
修正を戻してみたり、print()などを埋め込む方法で試しても突き止められない場合や、これらの修正を行うと問題が発生しなくなってしまって、前にも後ろにも進めなくなることがあります。
そのような場合に、この方法で問題箇所を特定します。
プログラミング言語用のデバッガが必ずあるはずなので、それを利用して行単位で実行するステップ実行を行ってデバッグを行います。
変数の中身を表示したり、その場で変更できる場合もあるため、かなり便利です。
ただし、デバッガは問題箇所が絞れてきた時に使うほうが効率的です。
問題の発生箇所がある程度絞れていないと、狙ったプログラム行までひとっ飛びに進めて、そこからどこで問題が発生するかを突き止められないためです。
下は、Microsoft Visual Studio Codeでpythonコードをステップ実行しているところです。
PHP, Javascript, Ruby, Java, C, C++, C# もVisual Studio Codeでデバッグが可能です。
上図では、黄色箇所が次のステップ実行行になっており、この行での変数の値や、特に監視したい値や式(ウォッチ式)、必ず停止する行を示すブレークポイントが、左ペインに表示されています。
プロセスにアタッチする
プログラムが実行されると、”プロセス”が起動します。プロセスは、プログラムを実行するロボットと考えてください。
プログラムが終了しない場合は、止めてから調査しても良いのですが、まずは止めずにデバッガを使ってプロセス(≒ロボット)に接続(アタッチ)して調査を行います。
そのほうが、問題発生の現場をおさえられるため、結果的に効率が良くなります。
ただし、この方法は、たいていの場合、プログラム側や、言語の実行基盤(pythonの場合python言語を実行してくれているプロセス)に対してアタッチを受け付けるための準備が必要となります。
そのため、その準備や準備内容を調べたりする時間がかかって、逆に効率が悪くなることがありますので、前もって準備を行っておくようにしましょう。
アタッチができたら、”デバッガを使ってステップ実行してみる“にあるように、捕捉した地点からステップ実行を行ったりして問題箇所を見つけます。
誰かにきく
どんな状況で、どこまで調査したのかなどをまとめて、聞ける形にして、誰かにきくことにしましょう。
以下のリンクできく内容やどこにきくかをまとめましたので、参照してください。
https://pasopet.com/try_to_inquire_here
まとめ
以上のように問題の原因には大きく3種類あります。
- プログラムがエラーを出している場合
- プログラムがエラーを出さずに突然終了している場合
- プログラムが終了せずに動き続けている場合
そして、解決するまでの方法には5つあります。
- エラーメッセージの意味を良く理解する
- Webで調べる
- 何をして問題に至ったかを書き出してみる
- プログラムを変更して問題を突き止める
- 誰かにきく
原因調査の方向性を決めたら、ひたすら問題箇所を特定する作業を繰り返すことになります。
プログラミングはあまり勢いに乗って進めてしまうと、かえって問題が発生したときに調査に何十倍もの時間がかかってしまい、最悪の場合は、続けていくモチベーションも下げてしまいます。
そうならないためにも、普段から自分が修正したところがどこだったかを分かるように、例えばgit(*2)などで修正の度にコミットをして、いつでも前版との差が分かったり戻せるようにします。
そして、少ない変更とちょっと実行してテストするといった、一歩一歩着実な進め方をすることが重要となります。
こういったことができてくると、プログラミングは楽しい作業となります。
新しい問題にぶちあたってもスムーズに解決して先に進むことができます。
そんな、プログラミングライフをエンジョイしていきましょう!
ここまでで問題が解決しない場合、小生のホームページからメールでご連絡いただきますと、ご相談に乗ります。お気軽にご連絡ください。
コメント