Pythonロボティクスコース レッスン 21

テーマ.5-4 「ジャンケンゲームの制作」

エラーを安全に処理する方法を学ぼう

このレッスンで学ぶこと

このレッスンでは、プログラムの実行途中でターミナルから入力を受け付けることができる「input()関数」の使い方と、エラーが発生した場合にプログラムを中断することなく正常に処理を終えるための「例外処理」を学習します。

レッスンの後半では、学習したことを応用して「ジャンケンゲーム」を制作します。ターミナルにジャンケンで出す手を入力して、コンピュータと対戦します。

新しいPython文法の学習

始めに、今回のレッスンで初めて扱うPythonの文法について学習しましょう。

2. 1 プログラムの途中で入力を受け付ける方法

例えばクイズゲームのプログラムでは、問題を出題した後に、プレイヤーからの回答を受け付け、正解かどうかを判定する処理が行われています。このように、プログラムの実行途中で、使用者から何らかの入力を受け付けたいときに使えるシンプルな命令として、Pythonでは input() 関数が用意されています。

■ input()関数の使い方

プログラムの実行途中でinput()関数が呼び出されると、ターミナル上でユーザーからの入力を受け付けます。input()関数の呼び出し後は、入力待ちの状態になり、続きの処理が一時停止します。

また、input() 関数はターミナルで入力された結果を文字列として返します。さらに、input() 関数は入力を受け付けるときにターミナルに表示する文字列を引数として指定することもできます。

では、次の「ユーザーが月の数字を入力すると、その月を表す英単語を表示する」プログラムを通して、input()関数の使い方を見ていきましょう。

始めに、月の数字をキーに、その英単語を要素としてもつ辞書型のデータを定義します。プログラムでは、このデータを使ってユーザーが入力した月の英単語を検索します。

次に、ユーザーからの入力を受け付けて、英単語を表示する処理を関数にまとめます。ユーザーから入力された文字列(月を表す数字)を変数に格納し、それを辞書内で検索して、print()関数で表示します。

【 サンプルコード 2-1-1 】
追加【16行目~19行目】

このプログラムを実行して、ターミナルからget_manth_name()関数を呼び出してみましょう。

(実行例)

このように、input()関数の引数に指定した文字列が表示されます。この文字列の続きに、キーボードから文字を入力できるようになっていますので、1~12の中から適当な数字を入力して、Enterキーを押しましょう。

(入力例)

定義した辞書dict_month_nameで検索された英単語が表示されました。ここまでがinput()関数の基本的な使い方になります。ただし、Studuino:bitでは、input()関数を実行するときに1つだけ注意しなければいけないことがあります。

■ Studuino:bitでinput()関数を実行するときの注意点

【 サンプルコード 2-1-1 】を次のように書き換えてプログラムを実行すると、入力を受け付けることができません。

追加【21行目】
(実行結果)

このプログラムでは、input()関数の実行後に入力待ちの状態にならず、そのまま続きの処理が行われたため、変数month_numに文字列が格納されず、あとで紹介するKeyErrorという「辞書型のデータに指定したキーが存在しない」エラーが発生しています。

このため、Studuino:bitでinput()関数を実行するときは、必ずターミナルから処理を呼び出す必要があります。

2. 2 エラーを安全に処理する方法

インターネットが普及してから、多くの人がWebサイトを利用して、特定のサービスへの会員登録を行ったり、チケットや物品の購入を行ったりするようになりました。こういった手続きを行うときは、Webサイトの申込フォームに、名前や住所・電話番号・メールアドレスなど必要な情報を入力して送信する作業を行います。

このとき、もしユーザーが入力した情報に誤りがあるとどうなるでしょうか?例えば、入力された電話番号やメールアドレスに誤りがあると、サービスの提供者から大切なお知らせをユーザーに届けることができなくなってしまいます。

同じように、Pythonでinput()関数を使用して、ユーザーから入力を受け付けるときも、入力された文字列の内容に誤りがあることで、続きの処理で問題が起きてしまい、プログラムの実行が中断してしまうことが考えられます。

実際に、【 サンプルコード 2-1-1 】でも、1~12以外の数字を入力するとエラーが発生し、プログラムが中断してしまいます。

>>> get_month_name()
Please input month number >>> 13
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 36, in get_month_name
KeyError: 13

そのため、Pythonではエラーが発生した場合に、それを例外として扱うことで安全に処理する方法が用意されています。

■ 例外を扱うtryexcept

上の実行結果を見ると、最後にKeyError: 13というメッセージが表示されています。これは、「13という文字列(数字)が、辞書型のデータ(dict_month_name)にキーとして存在していない」ということを示しています。このKeyErrorのことを 例外 と呼び、次に紹介するtryexcept文で発生した例外を捕捉することで、プログラムを中断せずに、適切に処理することができます。

tryexcept文は、「例外が発生する可能性がある処理を行う」try文のブロックと、「例外を捕捉したときにのみ特定の処理を行う」except文のブロックに分かれます。

try:
    .
    .    # 例外が発生する可能性がある処理
    .

except 捕捉する例外の種類:
    .
    .    # 例外を捕捉したときに実行する処理
    .

使い方を確認するための例として、【 サンプルコード 2-1-1 】にtryexcept文を追加し、1~12以外の文字列を入力したときに発生するKeyErrorを補足すると、誤った入力があったことを知らせ、再度入力を促すようにプログラムを改善してみましょう。

まず、【 サンプルコード 2-1-1 】の中で、例外が発生する可能性がある場所を考えます。

【 サンプルコード 2-1-1 】

このプログラムでは、18行目の「入力された文字列(月の数字)を辞書dict_month_nameで検索する」コードで例外KeyErrorが発生する可能性があります。

そこで、このコードをtry文のブロックとして囲み、except文のブロックで例外KeyErrorを捕捉します。また、例外を捕捉したときは、入力された月の数字が無効(invalid)であることをprint()関数で表示してユーザーに知らせます。

追加・変更【16行目~23行目】

そして、例外を捕捉したときはもう一度ユーザーに入力を促し、正常に入力が行われたときは、月の英語名をprint()関数で表示します。

tryexcept文には、他に「else」と「finally」が用意されていて、else文のブロックでは「例外が捕捉されなかった場合のみ行う処理」を、finally文のブロックでは「例外が捕捉されたときも捕捉されていないときもどちらの場合でも最終的に行う処理」をそれぞれ書くことができます。

try:
    .
    .    # 例外が発生する可能性がある処理
    .

except 捕捉する例外の種類:
    .
    .    # 例外を捕捉したときに実行する処理
    .
    
else:
    .
    .    # 例外を捕捉しなかった場合のみ実行する処理
    .

finally:
    .
    .    # どちらの場合でも最終的に実行する処理
    .

ここでは、else文を新たに追加します。また、「ユーザーにもう一度入力を促す」処理は、get_month_name()関数の内部で自身の関数を呼び出すことで簡単に行えます。

追加・変更【23行目~25行目】

このように自分自身を内部で呼び出す構造をもつ関数を「再帰関数」といい、プログラミング手法のひとつとして、複雑な計算問題を扱う場合によく使われています。では、改善したプログラムを実行して、動作を確認してみましょう。

(実行結果)

今度は誤った数字を入力しても、プログラムが途中で中断されることなく、正常に処理が続けられるようになりました。

■ MicroPythonで用意されている例外の種類

Pythonでは、KeyErrorの他にも様々なエラーごとに個別に対応できるように、いくつかの「例外クラス」が用意されています。Studuino:bitに搭載されているMicroPythonでは、その中の一部の例外クラスが利用できるようになっています。

下の表はその例外クラスの一覧です。今はまだ内容が理解できないものも多いと思いますが、レッスンを進めていき、より多くの知識を身に着けていく中で少しずつ使い方が理解できるようになります。

【 MicroPythonで利用できる例外クラス 】
エラー(例外)名内容
Exceptionすべての例外の基底となるクラス
AssertionErrorプログラムのテスト環境で用いられるassert文で特定の条件を満たした場合に発生する例外
ImportErrorfrom~import文でモジュール定義を見つけられなかった場合や、指定した名前空間が存在しなかった場合に発生する例外
KeyboardInterruptコマンドの実行中に割込み文字をタイプすると発生する例外
KeyError辞書に指定したキーが存在しないときに発生
AttributeErrorインスタンスから呼び出した属性やメソッドが存在しない場合に発生する例外
RuntimeErrorプログラムの実行時に発生する例外
SyntaxErrorプログラム上の構文に誤りがあるときに発生
TypeError関数などで適切でない型のオブジェクトが利用された場合に発生
ZeroDivisionError0による除算が行われた場合に発生する例外
IndexErrorリスト等のシーケンスの添え字が範囲外の場合に発生
MemoryErrorプログラムに使用できるメモリーが不足した場合に発生
NameError定義されていない変数名を利用した場合に発生
NotImplementedErrorある処理が未実装である場合に発生させる例外
OSErrorコマンドの実行前にOSレベルのエラーが発生した場合の例外
StopIterationジェネレータ―オブジェクトで返せる値がなくなると発生する例外
SystemExitPython を終了する際に発生させる例外
ValueError誤った値(期待する範囲外の数字)が引数として渡された場合に発生する例外

エラーが起きると、MicroPythonからメッセージが返されます。今までも作成したプログラムを実行して、エラーが返ってくることが何度もあったのではないでしょうか。ここでは、上の表の中からいくつかの例外クラスについて、サンプルコードと合わせて詳しく説明していきますので、これから先にこれらの例外が発生したときは、メッセージを理解して自分で原因を考えてみましょう。

【 SyntaxError 】

SyntaxErrorは、構文に間違いがあるときに発生する例外です。次のコードでは、3行目でelse文を誤ってelesと書いたために、この例外が起きています。

※ 「syntax」は英語で「構文」の意味があります。
【 ImportError 】

ImportError は、fromimport文で指定したモジュール名やクラス名、オブジェクト名などに間違いがあるときに発生する例外です。次のコードでは、1行目でdisplayオブジェクトが定義されているboardモジュールの名前を誤ってbaordと書いたために、この例外が起きています。

【 NameError 】

NameError は、使用した変数名や関数名に間違いがあるときに発生する例外です。次のコードでは、1行目で定義した変数fooprint()関数に渡すときに、誤って hoo と書いたために、この例外が起きています。

【 IndexError 】

IndexError は、文字列やタプル、リストなどのシーケンスなデータで指定した要素番号(インデックス)が存在していない場合に発生する例外です。次のコードでは、1行目で定義したリストが0番目~2番目までの要素しか持たないにも関わらず、3行目で3番目の要素を取得しようとしているために、この例外が起きています。

【 KeyError 】

KeyError は、辞書の要素をキー名で検索したときに、そのキーが存在しなかった場合に発生する例外です。次のコードでは、1行目で定義した辞書に、3行目で指定した'strawberry'というキーが存在しないために、この例外が起きています。

Traceback (most recent call last):
  File "&ltstdin&gt", line 6, in &ltmodule&gt
KeyError: strawberry
【 AttributeError 】

AttributeError は、呼び出したオブジェクトのメソッド名やプロパティ名に間違いがあるときに発生する例外です。次のコードでは、3行目のdisplayオブジェクトのscroll()メソッドの呼び出しで、誤ってscrolと書いたために、この例外が起きています。

Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
AttributeError: 'StuduinoBitDisplay' object has no attribute 'scrol'

ジャンケンゲームの作成

ここからは、学習したinput()関数と例外処理を応用して、コンピュータと対戦する「ジャンケンゲーム」を作成します。

【 制作例の動作動画 】
【 ジャンケンゲームのルール 】

言わずと知れたジャンケンゲームですが、今一度そのルールを確認しておきましょう。

  • プレイヤーは「グー(rock)」「チョキ(scissors)」「パー(paper)」の3種類のパターンから1つを選択して、その形を手でつくり、相手と同時に前に出します。
  • 自分と相手の出した手の形の組み合わせで、勝ち負けが決まります。相手よりも強い形を出すと勝利となり、同じ形を出すと引き分けとなります。引き分けの場合はもう一度勝負を行います。
  • それぞれの手の形の強弱の関係は次のように決めれています。「グー」は「チョキ」に勝ち、「チョキ」は「パー」に勝ち、「パー」は「グー」に勝ちます。

これから、このジャンケンゲームを人間対コンピュータで対戦できるプログラムを作成していきます。

3. 1 ゲームのプログラム処理の流れ

このゲームには、以下の機能が必要になります。

【 ゲームに必要な機能 】
  1. コンピュータが出す手をランダムに選び、LEDディスプレイに表示する機能
  2. プレイヤーから出す手の入力を受け付けて、その入力値に誤りがないかを確認する機能
  3. プレイヤーの入力した手をLEDディスプレイに表示する機能
  4. コンピュータとプレイヤーの手から勝ち負けを判定して、その結果をLEDディスプレイに表示する機能

これらの機能を組み合わせて、以下の流れでプログラムを処理していきます。

では、それぞれの機能を順番に作成していきましょう。

3. 2 コンピュータが出す手をランダムに選び、LEDディスプレイに表示する機能の作成

次の手順で、コンピュータがランダムに出す手を選び、それをLEDディスプレイに表示するところまでのコードを書いていきましょう。

■ 手の形に対応するLEDの点灯パターンを用意する

はじめに、「グー(rock)」「チョキ(scissors)」「パー(paper)」の手の形に対応したLEDの点灯パターンのイメージを用意します。

Imageクラスをインポートして、それぞれのイメージをもつインスタンスを作成しましょう。また、作成したこれらのイメージは辞書patternsにまとめます。

■ ランダムに出す手を選んで表示する

ここからの処理は、関数start_game()にまとめます。まずは、レッスン17(テーマ.4-4)で学習したrandomモジュールをインポートして、そのchoice()メソッドで辞書pattarnsのキーの一覧からランダムにコンピュータが出す手を選択します。

辞書のキーの一覧はkeys()メソッドで取得できますが、これは「イテレータ」と呼ばれるデータとなっており、choice()メソッドの引数として渡すためには、これをリストまたはタプルに変換する必要があります。そこで、以下のように関数tuple()を使い、タプルに変換してからchoice()メソッドに渡します。

これを踏まえて、以下のようにコードを追加しましょう。

追加【2行目、10・11行目】

続けて、LEDディスプレイに選ばれた手の形を表示します。まず、displayオブジェクトをインポートします。次に、ランダムに選ばれた手が格納されている変数comを辞書patternsのキーに指定して検索し、対応するイメージを取得して変数pattern_comに格納します。そして、その変数をdisplayオブジェクトのshow()メソッドの引数として渡しましょう。また、表示された手の形を一定時間プレイヤーが確認できるように、引数にdelay=2000clear=Trueを指定して、2秒ほど表示が続くようにしておきましょう。

追加【1行目、12行目、13行目】

このプログラムを実行して、ターミナルで関数start_game()を呼び出してみましょう。何度か呼び出して、そのたびにランダムに選ばれた手の形がLEDディスプレイに表示されることを確認してください。

3. 3 プレイヤーから出す手の入力を受け付けて、その入力値に誤りがないかを確認する機能/プレイヤーの手をLEDディスプレイに表示する機能の作成

次に、プレイヤーから出す手の入力を受け付け、その入力値に誤りがないかどうかを例外処理で確認をする機能を作成します。

まずは、関数input()でプレイヤーからの入力を受け付け、入力された文字列を変数playerに格納します。そして同じく、辞書patternsのキーに指定して検索し、対応するイメージを取得して変数pattern_playerに格納します。

追加【14行目、15行目】

このとき、変数playerに格納された文字列が辞書patternsのキーとして存在していれば問題ありませんが、そうではない場合は例外KeyErrorが発生します。そこで、tryexcept文を使い、この例外を捕捉します。もし例外が捕捉された場合は、入力値が無効であることと、rockscissorspaperのいずれかを選んで入力することを伝えます。

追加・変更【15行目~19行目】

また、例外が捕捉されたときは、もう一度入力を促し、捕捉されなかった場合のみ、LEDディスプレイにプレイヤーの手を表示します。前のチャプターで学習したことを踏まえて、else文と自分自身の関数を呼び出すコードを追加しましょう。

追加・変更【20行目~23行目】

3. 4 コンピュータとプレイヤーの手から勝ち負けを判定して、その結果をLEDディスプレイに表示する機能の作成

最後に、コンピュータとプレイヤーの勝ち負けを判定して、LEDディスプレイに結果を表示する機能を作成します。ここでは、変数comと変数playerにそれぞれが出した手の情報が入っているため、これらを比較することで勝敗の判定します。

勝敗を判定する分岐処理の組み方はいくつか考えられますが、まずは引き分けかどうかを先に判定します。引き分けかどうかは、単純に上の2つの変数のデータ(文字列)が同じであるかどうかを比較するだけで簡単に判別できます。引き分けの場合は、勝敗の結果を記録しておく変数result'Draw'を入れましょう。

追加【25行目、26行目】

続けて、引き分けではなかった場合に勝敗を判定するコードを書いていきます。この場合は、コンピュータの手の形とプレイヤーが出した手の形の組み合わせで勝敗が分かれるため、複数の分岐処理が必要になります。

まずは、コンピュータの手の形によって、ifelifelse文で処理を分けます。その中でさらに、プレイヤーの手の形で処理を分けるところは、レッスン14(テーマ.4-1)で学習した「条件式」を利用して短く記述することができます。以下のようにコードを追加しましょう。

追加【27行目~33行目】

最後に、判定の結果をLEDディスプレイにスクロール表示します。もし、結果が引き分けだった場合は、もう一度勝負をやり直すために、自分自身の関数start_game()を最後に呼び出します。

追加【35行目~37行目】

これでプログラムの完成です。このプログラムの全体像は次のようになっています。最後にもう一度誤りがないか確認して、プログラムを実行しましょう。

【 サンプルコード 3-4-1 】

課題:ジャンケンゲームのあとにさらに続けて行うゲームの追加

この課題では、ジャンケンゲームを行ったあとに、さらに続けて以下のゲームを行って勝敗を決めるように【 サンプルコード 3-4-1 】を改造します。

【続けて行うゲームのルール】
  • ジャンケンの勝敗によって、プレイヤーとコンピュータは攻撃側(勝者)と防御側(敗者)に分かれます。
  • プレイヤーは攻撃側となった場合、一定時間内にボタンAを押して攻撃を行います。反対に防御側になった場合、一定時間内にボタンBを押して防御を行います。
  • プレイヤーが攻撃に成功した場合、勝利となります。反対に攻撃に失敗した場合、コンピュータが防御に成功したものとして再びジャンケンゲームを行います。
  • プレイヤーが防御に成功した場合、再びジャンケンゲームを行います。反対に防御に失敗した場合、プレイヤーの負けとなります。

このプログラムの処理は以下のような流れになります。

勝敗の判定結果は、イメージの表示と文字列のスクロール表示('Victory''Defeat''Protected')で伝えます。表示するイメージは、それぞれStuduinoBitImage(Image)クラスにあらかじめ用意されているものを利用します。

この課題は少し難しいので、見通しが立たない場合は、以下のプログラムの作成例を見ながらコードを書いていきましょう。

4. 1 プログラムの作成例

ゲームの勝敗の決め方が変わるので、ジャンケンゲームの結果を表示する35行目のdisplay.scroll(result, delay=50)を削除します。

削除【35行目】

ジャンケンで引き分けでなかった場合は、続けてボタンAとボタンBが押されたかとうかを調べます。ボタンが押し続られていなくても正しく判定できるように、ここではStuduinoBitButtonクラスのwas_pressed()メソッドを使います。このメソッドの戻り値は後で何度も使用するため、変数was_pressed_a、変数was_pressed_bにそれぞれ格納します。

変更【1行目】
追加【38行目、39行目】

was_pressed()メソッドは以前に一度でも押されているとTrueを返すため、ゲームと関係の無い所でボタンが押された場合もTrueを返してしまいます。これを防ぐために、LEDディスプレイにコンピュータの手の形を表示する直前でwas_pressed()メソッドを1度実行して、状態をリセットするようにしておきましょう。

追加【23行目、24行目】

これで判定の準備が整いました。下の表を参考にして、分岐処理でゲームの勝敗を判定し、変数judgementに格納します。

このとき、ボタンAとボタンBの両方を押して強制的に成功させるという不正を防ぐために、片方のボタンだけが押されたきのみ成功と判定されるように条件を作りましょう。

追加【43行目~52行目】

最後に、判定結果によってそれぞれ以下のイメージをLEDディスプレイに表示し、その後で変数judgementに格納した判定結果を流しましょう。

ただし、判定結果が'Protected'だった場合は、もう一度ジャンケンゲームからやり直すため、関数start_game()を呼び出すようにしておきましょう。

追加【54行目~63行目】

これでプログラムの完成です。完成したプログラムの全体像は以下になります。実行してゲームで遊んでみましょう。

【 サンプルコード 4-1-1 】

おわりに

5. 1 このレッスンのまとめ

このレッスンでは新たに次のことを学習しました。

  • プログラムの実行途中でユーザーからの入力を受け付けるinput()関数の使い方
  • プログラムの実行中にエラー(例外)が発生した場合でも、それを捕捉して、中断することなく正常に処理を続けることができるtryexceptelsefinally文の使い方。
  • Python(MicroPython)で用意されている例外クラスの種類とそれぞれの意味。

どんなプログラムにもエラーはつきものです。エラーの発生を防ぐことはもちろんですが、それと同じくらい発生したときの対処をあらかじめ組み込んでおくことも重要です。このレッスンで学んだ例外処理をすぐに実践で応用するのは難しいかもしれませんが、ある程度プログラムを作ることに慣れてきたら、できるだけ取り入れられないか考えるようにしてください。

5. 2 次のレッスンについて

次のレッスンからは、車型ロボットの制作を通して、まだ紹介できていないオブジェクト指向なプログラム言語がもつ特徴について詳しく学習していきます。

TOP