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

テーマ.6-4 ボタン操作でプログラミングできる車型ロボットの制作

ボタン操作で車型ロボットの動きをプログラミングしてみよう

このレッスンで学ぶこと

このレッスンでは、特殊なメソッドのオーバーライドクラスメソッドとインスタンスメソッドのちがいについて学習します。レッスンの後半では、これまでの学習のまとめとして、ボタン操作で動きをプログラミングできる車型ロボットを製作します。

新しいPython文法の学習

ここでは、簡単なサンプルプログラムを通して、特殊なメソッドのオーバーライドと、クラスメソッドとインスタンスメソッドのちがいについて学習しましょう。

2. 1 特殊なメソッドのオーバーライド

Pythonでは、「+」や「-」などの演算子に対する処理や、str()len()のような標準関数に対する処理を定義した特殊なメソッド名が用意されています。次の表はその一覧です。

【 比較に関する特殊メソッド名の例 】
演算子特殊メソッド名
== (等しい)__eq__(self, other)
!= (異なる)__ne__(self, other))
<__lt__(self, other)
>__gt__(self, other)
<=__le__(self, other)
>=__ge__(self, other)

参考までに 対応表

記号メソッド英語表記
==eqequal
!=nenot equal
<=leless than or equal to
<ltless than
>=gegreater than or equal to
>gtgreater than
【 計算に関する特殊メソッド名の例 】
演算子特殊メソッド名
+__add__(self, other)
-__sub__(self, other)
*__mul__(self, other)
//__floordiv__(self, other)   ※小数点以下切り捨てで整数値で返す
/__truediv__(self, other)
%__mod__(self, other)
**__pow__(self, other)   ※累乗
【 標準関数に関する特殊メソッド名の例 】
関数名特殊メソッド名
str()__str__(self)
len()__len__(self)

このように、コンストラクタの「__init__()メソッド」と同じように、特殊なメソッドは名前の前後に「_(アンダースコア)」が2つ付きます。

実は、これまでの学習の中でもこの特殊メソッドのオーバーライドを利用して、使い勝手を良くしているクラスを利用していました。それが、StuduinoBitImageクラスです。

StuduinoBitImageクラスでは「+演算子(__add__()メソッド)」をオーバーライドして、2つのイメージを合成した新たなイメージを返すようにしています。

例えば次のコードでは、最終的に欲しいイメージを色ごとに分けてインスタンスとして作成し、+演算子でそれらのイメージを合成した新たなインスタンスを得ています。

【 __add__()メソッドのオーバーライドを利用した演算処理の例 】
img_yellow = StuduinoBitImage("10000:01000:00000:01010:00000", color=(31, 31, 0))
img_brown = StuduinoBitImage("10000:01000:00000:01010:00000", color=(4, 1, 1))
img_giraffe = img_yellow + img_brown

では実際に、特殊なメソッドをオーバーライドする練習をしてみましょう。

■ 特殊なメソッドのオーバーライドの練習

例として、あるアンケートを学校で実施し、クラスごとに集計した結果をまとめる場面を考えてみましょう。このアンケートでは「A」「B」「C」の3つの回答があり、それぞれに何人が投票したのかを調査しています。「class 1」と「class 2」は次のようになり、2つを合計した回答数を計算してみました。

同じ作業をすべての学年のすべてのクラスで行うのはとても大変です。そこで、これらの結果をPythonの辞書(dict)にまとめて「+演算子」で足し合わせることを思い付きました。

しかし、辞書では+演算子に対応するメソッドがなく、思いついたことは実行できません。そこで、辞書をプロパティとしてもつ独自のクラスDataを作成し、+演算子に対応する__add__()メソッドをオーバーライドして、この計算を行えるようにします。

まずは、Dataクラスを宣言し、コンストラクタ(__init__()メソッド)で辞書を引数として受け取り、プロパティとして格納する処理を書きましょう。

dictは、Pythonの予約語であるため_dictとしています。

次に、__add__()メソッドをオーバーライドします。ここでは引数として、同じDataクラスのインスタンスを受け取ります。そして、順番に辞書のキーを取り出し、2つの辞書の値を足し合わせた結果を新たに作成した辞書newに追加します。そしてこの辞書newを戻すことで、考えた計算が行えるようになります。

追加【5行目~9行目】

実際に、2つのクラスのデータを用意して、+演算子で足し合わせた結果を表示してみましょう。

【 サンプルコード 2-1-1 】
追加【12行目~15行目】
(実行結果)
{'A': 22, 'C': 30, 'B': 20}

このように、特殊な処理に対応するメソッドをオーバーライドすることで、既存のクラスを損なうことなく、補足したい機能を追加したクラスを作成することもできます。

2. 2 インスタンスメンバ変数とクラスメンバ変数

次に、クラスやオブジェクトに関する2種類の変数(プロパティ)として、「インスタンスメンバ変数」と「クラスメンバ変数」のちがいについて見ていきましょう。

まず、クラスメンバ変数はクラス自身が所有する変数で、インスタンスメンバ変数は各インスタンスごとに所有する変数です。例として次のコードを見てみましょう。

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

このクラスでは、voiceがクラスメンバ変数として、nameageがインスタンスメンバ変数として定義されています。つまり、ざっくり言うとクラスの直下で定義されたものがクラスメンバ変数となり、メソッド内で定義されたものがインスタンスメンバ変数となります。

クラスメンバ変数はクラスオブジェクトから直接呼び出すことができます。一方で、インスタンスメンバ変数はクラスオブジェクトからは呼び出すことはできません。インスタンスメンバ変数は、インスタンスのオブジェクトからのみ呼び出すことができます。

【 クラスメンバ変数の呼び出し 】
【 インスタンスメンバ変数の呼び出し 】

クラスメンバ変数はクラスオブジェクトと同じものがインスタンスオブジェクトにも紐づけられています。そのため、以下のコードのようにクラスメンバ変数を別の値に書き換えると、インスタンスから呼び出した変数voiceもその値に変わっています。

【 サンプルコード 2-2-2 】
追加【9行目~12行目】
(実行結果)

ただし、インスタンスから変更を行った場合は、クラスメンバ変数への参照がはずれて、新しい値が格納されるため、他のインスタンスが影響されることはありません。

【 サンプルコード 2-2-3 】
追加・変更【9行目~13行目】
(実行結果)

2. 3 インスタンスメソッドとクラスメソッド

変数と同様にメソッドについても「インスタンスメソッド」と「クラスメソッド」のちがいがあります。クラスメソッドとして定義する場合は、直前に@classmethodデコレータを記述します。以下のコードでは、bark_cl()メソッドをクラスメソッドとして定義しています。

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

インスタンスメソッドをクラスオブジェクトから実行する場合は、引数selfへの受け渡しを省略できませんが、クラスメソッドは、これを省略することができます。

引数selfが指定されていないため、エラーが発生。
引数selfにクラスオブジェクト自身を渡すと実行できる。
クラスメソッドの場合は引数selfへの受け渡しが省略できる。

このようなちがいがあるのは、@classmethodデコレータが付いた場合、メソッドの実行時に自動的にクラス自身が引数として渡されるようになるためです。また、インスタンスからインスタンスメソッドが呼び出された場合も、自動的にインスタンス自身が引数として渡されるようになっています。

車型ロボットの組み立て

このレッスンでは、レッスン22で組み立てた車型ロボットをそのまま使います。分解している場合は、以下の組立説明書を確認して、再度組み立てを行ってください。

レッスン22で製作した車型ロボットの組立説明書

ボタンでプログラミングできる車型ロボットの制作

ここではボタンを使って、前進/後進/右回転/左回転の4つの動作を登録して実行できる車型ロボットのプログラムを作成します。

【 製作するロボットの動作 】

このプログラムは大きく分けると、「メイン」「動作の登録」「動作の実行」の3つの処理で構成されています。

【 プログラムの構成 】

この中で最も複雑な処理が「動作の登録」です。動作を登録するときは、Aボタンを押して動きを選択し、Bボタンで決定します。このとき、どの動作が選択されているのかが分かるように、動きに対応した矢印のイメージをLEDディスプレイに表示します。

【 選択された動きに対応して表示する矢印のイメージ 】

Bボタンで決定した動作はリストに記録していきます。play()関数では、そのリストに貯められた情報を順番に取り出し、対応する動作を行うVehicleRobotクラスのメソッド(move_forward()rotate_right()など)を実行します。

4. 1 動作に関する情報を管理する`Direction`クラスの定義

ここではまず、動作に関する情報をまとめて管理するDirectionクラスを用意します。このDirectionクラスには、次のプロパティとメソッドを持たせます。

※ 「direction」は「方向」の意味を表す英単語です。
【 Directionクラスのプロパティ 】
  • 動作を表す定数
動作名前
前進FORWARD10
右回転RIGHT20
後進BACKWARD30
左回転LEFT40
  • directions:動作を表す定数をまとめたタプル
directions = (FORWARD, RIGHT, BACKWARD, LEFT)
  • images:動作に対応する矢印のイメージをまとめた辞書
キー(プロパティ)
FORWARDStuduinoBitImage.ARROW_N(上向きの矢印)
RIGHTStuduinoBitImage.ARROW_E(右向きの矢印)
BACKWARDStuduinoBitImage.ARROW_S(下向きの矢印)
LEFTStuduinoBitImage.ARROW_W(左向きの矢印)
【 Directionクラスのメソッド 】
  • get_direction()メソッド
    引数として、プロパティdirectionsに指定するインデックスを受け取り、そのデータを返します。
  • get_image()メソッド
    引数として、プロパティFORWARDRIGHTBACKWARDLEFTのいずれかを受け取り、プロパティimagesのキーとして指定し、その値(イメージ)を返します。
  • get_len()メソッド
    プロパティdirectionsの長さを返します。

いきなり色々な情報が出てきたので頭の中を整理するのが大変ですが、ここからは実際にコードを書きながら、それぞれのプロパティやメソッドをなぜ用意しているのかを説明していきます。

■ 動作を表す定数の定義

動作をリストに登録するときに、「"move_forward"」や「"rotate_right"」のように動作を表す文字列を記録することもできますが、ここではあえて文字列ではなく、代わりに数値を使用します。これには次のような理由があります。

Studuino:bitのようなマイコンは、PCと比べてとても容量が小さいメモリ(データを一時的に記憶するための部品)でプログラムを処理しています。基本的に数値と文字列では記録に必要なデータ容量にちがいがあり、文字列は数値とくらべてより多くの容量を必要とします。そのため、マイコンのプログラムにおいては、代替できる場合も文字列ではなく数値を使用する慣習があります。Pythonでは、C言語のように古くから使われているプログラム言語ほどの差はありませんが、それでも文字列は数値の2倍程度かそれ以上の容量を必要とします。塵も積もれば山となるということわざにもあるように、多くのデータを記録していく場合は、その差は馬鹿にできません。

そこで、今回は練習も兼ねて数値を使用してみたいと思います。ただし、数値を扱う場合は、文字列と違ってコードを読んでもすぐにそれが何の代わりに使われた数値なのか判断することができません。そこで、定数として名前を付けて定義することで、読んで理解できるようにします。

では、前置きが少し長くなりましたが、Directionクラスを定義して、次のようにコードを書きましょう。

4つの定数は、このクラスのクラスメンバ変数として扱います。これによって、クラスから直接アクセスして値を取り出すことができます。

print(Direction.FORWARD)

また、7行目ではdirectionsプロパティとして、これら4つの定数をタプルにまとめておきましょう。

■ 動作に対応する矢印のイメージをまとめた辞書の定義

次に、それぞれの動作に対応するイメージをまとめた辞書を用意します。矢印のイメージはStuduinoBitImageクラスにあらかじめ用意されていますので、インポートして使用しましょう。

追加【1行目、11行目~16行目】

■ 3つのメソッドの定義

最後に3つのメソッドを定義します。

  • get_direction()メソッド
  • get_image()メソッド
  • get_len()メソッド

これらは、後のregist()関数でボタンを押して動作の選択を切り替える処理を書くときに使います。今はまだこれらをメソッドとして用意した理由がピンと来ないかもしれませんが、後々その便利さが分かってきます。また、これら3つのメソッドはクラスから直接呼び出せるように、クラスメソッドとして定義しましょう。

まずは1つめのget_direction()メソッドから書いていきます。このメソッドは、引数としてdirectionsプロパティに指定するインデックス(index)を受け取ります。そして、このインデックスのデータを返します。

追加【16行目~18行目】

2つめとして、指定された動作に対応する矢印のイメ―ジを返すget_image()メソッドを定義します。このメソッドは引数として、このクラスがプロパティとしてもつ4つの定数「FORWARD/RIGHT/BACKWARD/LEFT」を受け取り、辞書imagesのキーとして指定し、その値を返します。

追加【20行目~22行目】

最後の3つめに、directionsプロパティの長さを返すget_len()メソッドを定義します。

追加【24行目~26行目】

4. 2 動作を登録する`regist()`関数の定義

regist()関数では、ボタンを押して動作の選択と登録を行い、リストに追加していきます。この関数の処理の流れは次のようになります。

【 regist()関数の処理の流れ 】

regist()関数ではボタンAとB、それからLEDディスプレイを使用するため、これらを追加でインポートしましょう。また、空のリストcommandsもグローバル変数として用意します。

追加・変更【1行目、3行目】

■ リストを空にする

では、regist()関数を定義していきます。この中で、さきほど用意したグローバル変数のcommandsを使いますので、最初にglobal宣言をします。また、この登録処理を呼び出すたびに、前に記憶していた情報は削除します。そのため、リストのclear()メソッドで空にしておきましょう。

追加・変更【32行目~34行目】

■ 初期選択の動作を決めディスプレイに表示する

次に、最初に選択する動作を決めて、LEDディスプレイに表示します。ここでは、4つある動作の中で、インデックスが0のものを最初は選ぶようにしておきましょう。

変数indexを用意し、その値を0にします。これをDirectionクラスのget_direction()メソッドの引数に指定して、動作を表す値を取得し、その値を変数selected_directionに格納します。そして、Directionクラスのget_image()メソッドに変数selected_directionを指定してイメージを取得し、displayオブジェクトのshow()メソッドで表示しましょう。

追加・変更【36行目~38行目】

■ ボタンAを押すたびに次の動作を選択してディスプレイに表示する

ボタンBが長押しされるまでは、ボタンが押されたことを調べる処理を繰り返し行いますので、ここでは、変数loop_flagを用意して、この値がTrueからFalseに変更されるまで繰り返すというコードを書きます。

追加【40行目、41行目】

ボタンAが押されことを判定して、押された場合は変数indexを1ずつ変えます。また、ボタンを押し続けることで連続して処理されるのを防ぐためにボタンが離されるまで待ってからindexを変更します。そして、indexの値がDirectionクラスのdirectionプロパティの最大のインデックス番号(長さ - 1)を超えるとエラーが発生するため、条件式でその場合は0に戻すようにします。それから、変更後のインデックスで動作の値と対応する矢印のイメージを取得して、LEDディスプレイに表示します。

追加【42行目~49行目】

■ ボタンBを押して選択中の動作を登録する/ボタンBの長押しでループを抜ける

ボタンBは短く押された場合と、長押しされた場合で行う処理が変わります。まずは、押された時間の長さを計測するためにtimeモジュールを利用するため、プログラムの先頭でインポートしましょう。

追加【2行目】

まずは、ボタンBが押された時間の長さを計測し、2秒以上押し続けられたらloop_flagFalseに変更して、ループを抜ける処理を書きます。現在のStuduino:bit内の時間はtimeモジュールのticks_ms()関数で取得できます。ボタンBが押され、時間の計測を開始する前に一度この関数を実行し、その時間を変数startに格納します。

追加【51行目、52行目】

ボタンBが押されている間は時間の計測を繰り返します。この繰り返しの中で、timeモジュールのticks_diff()関数で、取得した最新の時間(ticks_ms())と計測開始時の時間(start)の差を求めます。この差が2秒(2000ミリ秒)を超えている場合は、変数loop_flagFalseに変更し、LEDディスプレイの表示を消します。そして、ボタンが離されるまで待ち、break文でこの階層のwhile文を抜けます。

追加【54行目~59行目】

break文でwhile文を抜けるのは理由があります。2秒が経過する前に、ボタンBが離されたとき、つまり正常にwhile文を抜けたときだけ、else文を使用して現在選択している動作をリストに追加するためです。このとき、リストに動作を追加したことが使用者に分かるように、短く(200ミリ秒ほど)LEDディスプレイを点滅させるようにしましょう。

追加【60行目~66行目】

少し長くなりましたが、これでregist()関数が定義できました。

4. 3 登録した動作を実行するplay関数の定義

play()関数では、リストcommandsに登録されている動作を表す値を順番に取り出して、その値によって、VehicleRobotクラスのメソッドを実行して車型ロボットを動かします。この関数の処理の流れは次のようになります。

この関数では、車型ロボットを制御するため、VehicleRobotクラスのインスタンスを用意しておきましょう。

追加【3行目、6行目】

では、play()関数を定義していきます。まずは、リストから情報(動作を表す値)を取り出して、LEDディスプレイに表示するコードを書きましょう。

追加【70行目~74行目】

次に、この値(direction)によって、ロボットの動作を制御します。次のようにコードを書きましょう。Directionクラスのプロパティを使用して値を比較することで、コードが読みやすくなっていることがわかります。

追加【75行目~83行目】

4. 4 メインの処理を行うmain関数の定義

最後に、ボタンAとBのどちらが押されたかを見て、regist()関数とplay()関数を実行するメインの処理を行うmain()関数を定義します。この関数の処理の流れは次のようになります。

この処理は、コードとして次のように書くことができます。

追加【86行目~95行目】

また、待機中はそのことが分かるように、下の図のような笑った顔のイメージを表示するようにしておきましょう。

このイメージは、StuduinoBitImageクラスにHAPPYというプロパティ名であらじめ用意されています。また、点灯させるときの色の定義もあらかじめStuduinoBitImageクラスに用意されており、黄色の場合はYELLOWというプロパティがあります。そのため、以下のようにコードを書くだけで上の図のイメージをLEDディスプレイに表示することができます。

display.show(Image.HAPPY, color=Image.YELLOW)

このコードを以下の3か所に追加しましょう。

追加【87行目、93行目、98行目】
def main():
display.show(Image.HAPPY, color=Image.YELLOW) # 追加
while True:
if button_a.is_pressed():
while button_a.is_pressed():
pass
play()
display.show(Image.HAPPY, color=Image.YELLOW) # 追加
if button_b.is_pressed():
while button_b.is_pressed():
pass
regist()
display.show(Image.HAPPY, color=Image.YELLOW) # 追加

4. 5 動作を確認する

これでプログラムが完成しました。プログラムの末尾に、main()関数を実行するコードを追加して、プログラムを実行しましょう。

プログラムがうまく動作しない場合は、以下のサンプルコードと見比べて誤りがないかをチェックしてください。

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

課題:クラスではなくモジュールとして`Direction`を定義する

【 サンプルコード 4-5-1 】では、動作に関連する情報をDirectionクラスにまとめて定義していました。しかし、クラスとしてではなく、モジュールとして定義しても同じように使用することができます。この課題では実際に、新しくファイルを作成してコードを移し、Direction(.py)という名前でStuduino:bit内に保存することで、モジュールとして利用できるようにしてみましょう。

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

まずは新しくファイルを作成し、Direction(.py)という名前を付けてPCに保存しましょう。

次に、元のファイルから以下のコードをコピーして、新しいファイルに貼り付けます。貼り付け終わったら元のファイルからこのコードを削除するか、もしくはコメント化しましょう。

新しいファイルの方で、上のコードを次のように変更します。ファイルが分かれるため、StuduinoBitImageクラス(省略形のImage)を先頭でインポートするようにし、また@classmethodの修飾と引数selfを削除して、インデントを調整しましょう。

【 Direction.py 】

このファイルをStuduino:bit内に保存します。メニューのファイルをクリックして、ウィンドウを開き、PCからStuduino:bitへ写しましょう。

できたら、元のファイルへ戻り、このDirection(.py)をモジュールとしてインポートしましょう。

追加【4行目】

これで変更完了です。プログラムを実行して、【 サンプルコード 4-5-1 】と同じように動作することを確認しましょう。

5. 2 クラスとモジュールの使い分けについて

クラスとモジュールには似ている部分があるため、今回は上で確かめたように、モジュールとして定義し直しても、元のコードをほとんど変えずにそのまま実行することができました。

では、クラスとモジュールはどのように使い分ければ良いでしょうか。これについて、簡単なガイドラインを以下にまとめていますので、参考にしてください。

【 クラスとモジュールを使い分けるときのガイドライン 】
  • メソッドは同じでも、プロパティの値がちがう複数のインスタンスが必要な場合は、クラスを使用する方が良い。
  • 反対に、インスタンスを1つだけしか作成しないようなときは、モジュールの方が適当なケースもある。(例えば、Pythonの場合、モジュールはプログラムの中で何度インポート参照されても1つのコピーしかできないため、メモリの消費が抑えられるというメリットがある。)
  • 継承する可能性があるものは、クラスを使用する方が良い。(モジュールは継承ができないため。)
  • 複数の値をもつ変数があり、これらを複数の関数に引数として渡せるときは、変数をプロパティとして、関数をメソッドとしてもつクラスを定義した方がコードがすっきりとする。

おわりに

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

このレッスンでは、演算子や標準関数に対応する特殊なメソッドのオーバーライドと、クラスやインスタンスに所属する変数とメソッドのちがいについて紹介しました。このレッスンでクラスについての基礎学習は終わりですが、これから先のレッスンではクラスを多用してコードを作成していきますので、分からなくなったときはこのテーマ.6を振り返るようにしてください。

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

テーマ.7では、様々なゲームを製作していきます。これまでよりもさらに複雑なプログラムに取り組んでいきますが、頑張って学習していきましょう。

TOP