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

テーマ.5-1 「対戦型ボタン連打ゲームの制作」

変数や関数でのデータの扱われ方を学ぼう

このレッスンで学ぶこと

このレッスンでは、変数に保存されたデータをコピーしたり、更新したりするときにデータの型によってコンピューター内部での扱われ方に違いがあることや、プログラムの中で変数や関数を定義したり、呼び出したりするときに気を付けたい「スコープ」という仕組みについて学習します。

レッスンの後半では、ボタン連打の速さを競う対戦型ゲームを制作します。

新しいPython文法の学習

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

2. 1 変数に保存したデータの扱われ方

Pythonには、数値や文字列、タプル、リストなど様々な型のデータがありました。これまでプログラムの中で変数に保存して扱うときは、これらのデータ型の違いをあまり意識することがありませんでしたが、実際には内部で「」と「参照」の2種類に分かれており、扱われ方に違いがあります。ここでは、この「値」と「参照」の間にどのような違いがあるのかを見ていきましょう。

■ 値として扱われるデータ

数値や文字列、ブール値(True, False)、タプルは「値」として扱われます。「値」は変数をコピーしたときに、同じ情報をもつ別のオブジェクトを作る性質があります。言葉の説明では少し分かりにくいので、次のサンプルコードを実行して詳しく見ていきましょう。

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

Studuino:bitを接続して、上のコードを実行すると、次のような結果になります。

(実行結果)

はじめの、1 1が3行目のprint文の表示で、その次の2 1が6行目のprint文の表示になっています。2行目で、変数bに変数aをコピーしているため、3行目のprint文の結果はどちらも同じ数値になっています。しかし実際には、この変数aと変数bは数値こそ同じですが、オブジェクトとしては別のものになっています。そのため、5行目で変数a1を足され、2になりますが、変数bにはこの計算は反映されず1のままになっています。

このように、「値」として扱われるデータはその変数がコピーされると、中身だけが複製されて、コピー先の変数には元の変数とは別のオブジェクトが保存されます。

同じく「値」として扱われる文字列でも似たような結果になります。

【 サンプルコード 2-1-2 】
(実行結果)

■ 参照として扱われるデータ

「値」は中の情報だけがコピーされ、別のオブジェクトが作られましたが、「参照」はこれとは反対に、コピー元とコピー先が同じオブジェクトを参照する性質があります。参照として扱われるのはリストや辞書型のデータです。こちらも、言葉の説明では少し分かりにくいので、次のサンプルコードを実行して詳しく見ていきましょう。

【 サンプルコード 2-1-3 】
(実行結果)

【 サンプルコード 2-1-1 】や【 サンプルコード 2-1-2 】とは違い、5行目で変数aに要素を追加すると、変数bにもそれが反映されています。これは、2行目のコードで変数aと変数bが同じリストのオブジェクトを参照するようになったためです。

ただし、「参照」として扱われるデータでも、必ずしも全ての処理で同じオブジェクトが参照されるわけではないということに注意してください。次にその具体例を紹介します。

■ 変数に保存されたデータの更新と再代入

変数に保存されたデータを変更するときに、「更新」と「再代入」の2つの方法があります。「更新」は元のオブジェクトを参照したまま、中身のデータだけを書き換えます。これに対して、「再代入」は元のオブジェクトの参照を外し、新たに作成されたオブジェクトを参照します。これも言葉では分かりにくいので、リストに要素を追加する2つの方法で「更新」と「再代入」の違いを見ていきましょう。

  • append()メソッドを使った要素の追加

こちらは、【 サンプルコード 2-1-3 】と同じです。append()メソッドを使用した場合はデータの「更新」になり、オブジェクトの参照が保持されます。そのため、変数aと変数bは全く同じ要素を持っています。

(実行結果)
  • +演算子を使った要素の追加

一方で+演算子を使用して、要素を追加したのが次のコードです。この場合、3行目のa + [3]の計算で、元のaの要素と[3]を要素に持つ新たなリストのオブジェクトが作成されて、その参照が変数aに再び代入されています。つまり、変数aには新たなリストが再代入されたということになります。

(実行結果)

この2つの違いを図にまとめると次のようになります。

このように、一見するとデータが更新されているように見える処理でも、新しく作成されたオブジェクトが再代入されている場合があります。特に複数の変数で同じオブジェクトを参照するようなプログラムを作成する場合は要注意です。

2. 2 スコープ (名前の有効範囲)

プログラムの中で同じ名前の変数や関数を複数作成してしまうと、意図していない結果になったり、実行途中でエラーが起きて、プログラムが止まってしまうことがあります。そのため、できるだけ名前の重複は避けなければいけません。

例えば、次のサンプルコードは、「2つの数値の足し算を行う関数」と、「2つの数値の掛け算を行う関数」を同じ名前calculateと定義しています。もし、「2つの数値の足し算を行う関数」を呼び出して「3+4」を計算するつもりで7行目のコードが書かれているとしたら、意図した結果になるでしょうか?

※ 「calculate」は、英語で「計算する」という意味があります。
【 サンプルコード 2-3-1 】

実際にこのコードを実行すると、「2つの数値の掛け算を行う関数」が呼び出されます。

(実行結果)

このような結果となるのは、後から定義した内容で先に定義した関数calculateが上書きされたからです。これは極端な例で、実際にこのようなプログラムを作成することはないと思いますが、大規模なプログラム開発をしていたり、他の人が作成したプログラムを一部利用して新たなプログラムを作成したり、複数人で共同してプログラムを開発したりすると、意図せず同じ名前を使用していたなんてことはあり得るでしょう。

そこで、Pythonも含めて一般的なプログラミング言語には、特定のルールのもとプログラム内で同じ名前の変数や関数の使用を許す仕組みがあります。具体的には、プログラムをいくつかの範囲に分け、その範囲から外れると同じ名前が使用できるというものです。この範囲のことを「スコープ」と言います。

■ スコープの基準

Pythonには2種類のスコープの基準があります。

  • グローバルスコープ:プログラム (1つのファイル) 内に必ず1つ作られる最も広いスコープ
  • 関数スコープ:関数の中だけで独立したスコープ

グローバルスコープと関数スコープには下の図のような入れ子の関係があります。また、関数の中で関数を定義すると、関数スコープの中にさらに別の関数スコープを作ることもできます。

では、それぞれのスコープについて、サンプルコードを通して詳しく見ていきましょう。

■ グローバルスコープ

最も外側で定義された変数や関数はグローバルスコープで管理されます。次のサンプルコードでは、x,y,zの3つの変数がグローバルスコープで定義されています。このようにグロ―バルスコープで定義された変数のことを「グローバル変数」といいます。

【 サンプルコード 2-3-2 】
(実行結果)

他のプログラミング言語の中には、「if文」や「for文」にも別のスコープが用意されるものもありますが、Pythonではこれらも同じスコープとして扱われます。同じスコープであれば、その中で定義された変数や関数を見ることができるので、print文で問題なく表示できています。

■ 関数スコープ

関数が定義されると、その内部はグローバルスコープとは別のスコープとして扱われます。次のサンプルコードでは、グローバルスコープで定義された変数xと、関数の内部(関数スコープ)で定義された変数xがそれぞれ別のものとして管理されています。

【 サンプルコード 2-3-3 】
(実行結果)

関数スコープで定義された変数は、「グローバル変数」に対して「ローカル変数」と呼ばれます。7行目のprint(x)では、グローバル変数x(0)を表示し、8行目のprint_x()では、ローカル変数のx(1)を表示しています。このように、スコープが異なるのであれば、同じ名前の変数を作成しても問題が起きません。

■ スコープの親子関係

グローバルスコープに関数を定義したり、関数内で別の関数を定義したりすると、スコープを積み重ねることができます。このような状態を「外側の関数は、内側の関数は」と見立てて、スコープに親子関係ができたと考えます。

Pythonでは、同じスコープ内に変数や関数の定義が無い場合、親のスコープにさかのぼりながら定義を探してくれます。

【 サンプルコード 2-3-4 】
(実行結果)

子のスコープから親のスコープの変数や関数をさかのぼって見ることはできますが、親のスコープから子のスコープは見ることはできません。そのため、次のコードを実行するとエラーになります。

【 サンプルコード 2-3-5 】
(実行結果)

関数scope_child()は、関数scope_parent()内のスコープで定義されているため、グローバルスコープからは見ることができず、それを呼び出そうとしたためエラーになっています。

■ スコープを超えた変数の変更

基本的に、子から親にさかのぼって変数を見ることはできますが、書き換えることはできません。ただし、グローバルスコープに存在する変数の場合は「global宣言」を行うことで、それ以外は「nonlocal宣言」を行うことで、書き換えが可能になります。

【 サンプルコード 2-3-6 】
(実行結果)

ボタン連打対戦ゲームの制作

ここでは、ボタンを使った対戦型ゲームを制作します。このゲームは2人のプレイヤーで遊びます。各プレイヤーはそれぞれボタンAとボタンBを連打し、先にボタンを30回押したプレイヤーの勝利です。

【 制作例の動作動画 】

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

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

【 ゲームに必要な機能 】
  1. ボタンAが連打された回数を記録する機能
  2. ボタンBが連打された回数も記録して勝者を判定する機能
  3. ゲーム開始の合図としてカウントダウンを行う機能
  4. ボタンAとボタンBを同時押しするとゲームを開始する機能

これらの機能を組み合わせて、以下の流れでプログラムを処理していきます。また、プログラムの見通しを良くするために、いくつかの機能は関数にまとめて実行できるようにします。

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

3. 2 ボタンAが連打された回数を記録する機能の作成

始めにボタンAが連打された回数を記録する機能を作成します。この機能を作成する前に、ボタンが押されたことを調べるためのメソッドについて振り返りをしておきましょう。

■ ボタンが連続して押されたことを判定するときに使うメソッド

ボタンが押されたかどうかは、StuduinoBitButtonクラスの以下の2つのメソッドで調べることができました。

  • is_pressed()メソッド
    ボタンが押されているときはTrueを、押されていないときはFalseを返します。
  • was_pressed()メソッド
    ボタンが過去に押されていた場合はTrueを、押されていなかった場合はFalseを返します。

2つのメソッドの違いを理解するには、「過去に」というキーワードがポイントになります。

is_pressed()メソッドはボタンを押している間は常にTrueを返します。一方で、was_pressed()メソッドは最初だけTrueを返し、それ以降はずっとFalseを返すようになっています。そして、ボタンを離してからもう一度押すと、再びwas_pressed()メソッドは1度だけTrueを返します。

実際に以下の「500ミリ秒おきに2つのメソッドでボタンの状態を調べる」プログラムを実行して、2つの違いを確認してみましょう。

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

プログラムの実行後、ボタンAを押し続けます。すると、is_pressed()メソッドはずっとTrueを返しますが、was_pressed()メソッドは一度だけTrueを返し、それ以降はFalseを返していることが確認できます。

(実行結果)

このゲームでは、ボタンを押し続けたときに複数回カウントされてはいけませんので、一度だけ数えられるwas_pressed()メソッドを使います。

■ ボタンAを連打した回数を記録する

では、【 サンプルコード 3-2-1 】を削除して、ボタンAが押された回数を数えるプログラムを作成していきます。

ボタンAを使うオブジェクトbutton_aをインポートし、また、ボタンAが押された回数を記録するための変数count_aを作成して始めの値として0を代入しましょう。

次に、while文を使った永久ループの中で、if文とwas_pressed()メソッドの組み合わせて、ボタンAが押されたかどうかを判定します。そして、ボタンAが押された場合は、変数count_aに1を足し、print()関数で表示します。

【 サンプルコード 3-2-2 】
追加【5行目〜8行目】

このプログラムを実行して、ボタンAを押すたびに回数が更新されていくことを確認してください。

3. 3 ボタンBが連打された回数を記録して勝者を判定する機能の作成

続けて、ボタンBが押された回数も調べ、先に30回を記録した方を勝者とする機能を作成します。まずは、【 サンプルコード 3-2-2 】にボタンBが押された回数を記録するためのコードを、ボタンAのときを参考にして追加しましょう。

変更・追加【1行目、4行目、9・10行目】
※ 【 サンプルコード 3-2-2 】からprint()関数は削除しています。

次に、勝者を判定する処理を追加します。まずは、記録されたボタンの回数によって、ループ処理を抜けられるようにします(以下の図に示した①の分岐処理)。そこで、6行目のwhile True:からwhile button_a < 30 and button_b < 30:に変更し、どちらも30回未満のときだけ、回数の記録を繰り返すようにします。これで、変数button_aと変数button_bのどちらかでも値が30になるとwhile文を抜けることができます。

そして、while文を抜けたあとに、変数count_aの値と変数count_bの値を比較し、勝者を変数winnerに記録します(以下の図に示した②の分岐処理)。ただし、引き分けの場合もあるため、その場合は代わりに'Draw'を記録します。

変更・追加【6行目、12行目〜17行目】

最後に、ゲームが終了したことをブザー音で知らせ、勝者(結果)をLEDディスプレイに表示します。

変更・追加【1行目、19・20行目】

ここまでのプログラムを関数start_game()としてまとめ、この関数を呼び出して実行するようにコードを変更しましょう。

【 サンプルコード 3-3-1 】
変更・追加【3~21行目、23行目】
※ 各行の先頭にインデントを追加して、関数start_game()内にコードをまとめましょう。

3. 4 ゲーム開始の合図としてカウントダウンを行う機能の作成

ゲーム開始の合図として、カウントダウンを行う機能を作成します。この機能は関数countdown()としてまとめていきます。

カウントダウンは次のように、ディスプレイへの数字の表示と、ブザー音を鳴らす処理の組み合わせになります。

関数countdown()を定義し、数字の3から0まで順番にLEDディスプレイに表示するコードを書いていきます。

まずは、range(3, 0, -1)で「3, 2, 1」と1ずつ減っていく数字の並びを作成します。それをfor文で繰り返し取り出してstr()関数で文字列に変換し、StuduinoBitDisplayクラスのshow()メソッドで表示させましょう。このとき、LEDディスプレイへの数字の表示とブザー音を鳴らす処理はほぼ同時に行う必要があるため、show()メソッドの引数delayには「0秒」を指定しておきます。また、数字の「0」は他の数字と表示時間が異なるため、for文の外に処理を書きましょう。

追加【4行目~12行目】
※13行目以降は表示を省略しています。

続けて、ブザー音を鳴らす処理を追加します。「3~1」まではC4の音を鳴らし、「0」は「G4」の音を鳴らすので、それぞれ以下の箇所にコードを書きます。

追加【7行目、10行目、13行目、16行目】
※16行目以降は表示を省略しています。

最後に、関数start_game()の前に、作成した関数countdown()を呼び出して実行するようにましょう。

【 サンプルコード 3-4-1 】
追加【38行目】

3. 5 ボタンAとボタンBを同時押しするとゲームを開始する機能の作成

関数countdown()と関数start_game()を、ボタンAとボタンBが同時押しされたときに呼び出すようにします。ここでは、StuduinoBitButtonクラスのis_pressed()メソッドを使います。

【 サンプルコード 3-5-1 】
追加・変更【38行目~41行目】

これで始めに提示した機能をすべて作成することができました。プログラムを実行して動作を確認してみましょう。こ

課題:3ゲームを先取したプレイヤーを勝者とするようにプログラムを改造する

【 サンプルコード 3-5-1 】では、1回のゲームごとに勝利者を決めていましたが、ここでは課題として、「3回先にゲームを取ったプレイヤーを勝利者とする」ようにプログラムを改造してください。

<ヒント>

グローバル変数として、各プレイヤーのゲームの取得数を数える変数(例えば、winning_awinning_bなど)を定義し、関数start_game()内で取得したゲーム数をカウントします。ただし、グローバル変数として用意した変数を関数内部で更新するため、あらかじめ「global宣言」を行うことに注意してください。

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

【 サンプルコード 3-5-1 】から以下のように変更を行います。

■ どちらかのプレイヤーが3ゲーム取得するまでゲーム繰り返す

ボタンAとボタンBの両方が押されたら、変数winning_aと変数winning_bを「0」にリセットします。ここで定義したこれら2つの変数はグローバル変数になります。そして、どちらもゲーム取得数が「3」未満の間は、while文内で関数countdown()と関数starg_game()を実行し、ゲームを繰り返します。また、下の例では、ゲーム間に1000ミリ秒だけ時間を空けるようにしています。

変更・追加【40行目~45行目】

■ 勝利プレイヤーを判定して表示する

ゲーム終了後、ゲーム取得数を比較して、勝者を判定します。そして、勝利したプレイヤーをLEDディスプレイにスクロール表示します。

追加【46行目~49行目】

■ ゲーム取得数をカウントする

関数start_game()の中で、どちらかのプレイヤーがゲームを取得したときに、その数をカウントする処理を追加します。ただし、そのために利用する変数winning_aと変数winning_bはグローバル変数であるため、関数の始めに「global宣言」を行います。これでプログラムの改造は完了です。

追加【19行目、31行目、36行目】

おわりに

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

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

  • 変数に保存されたデータの扱われ方には、「値」と「参照」の2種類があること。
  • 変数に保存されたデータを変更するときは、「更新」と「再代入」の2つの方法があること。
  • 変数や関数の名前の有効範囲を制限する「スコープ」という仕組みがあること。
  • スコープには、「グローバルスコープ」と「関数スコープ」があること。
  • スコープを超えて変数を扱い、データの変更を行うときは「global宣言」や「nonlocal宣言」を行うこと。

難しい所が多かったと思いますが、ここで学んだ概念をしっかりと理解することは、より規模の大きなプログラムを作成していく上で必ず助けになります。もし、このレッスンで疑問点が残っているのであれば、メンターに質問をして解決しておきましょう。

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

次のレッスンでは、数が固定されていない引数をもつ関数を定義する方法と、for文やwhile文で特別な処理を行うために使われるcontinue文やbreak文、else文ついて学習します。

TOP