Pythonロボティクスコース レッスン 19 *
テーマ.5-2 「テルミン風電子楽器の制作」
引数の数が固定されていない関数について学ぼう
チャプター1
このレッスンで学ぶこと
このレッスンでは、引数の数が固定されていない(任意な数の引数をもつ)関数を定義する方法と、for文やwhile文で特別な処理を行うために使われる「continue文」や「break文」、「else文」ついて学習します。
レッスンの後半では、「光センサー」と「超音波距離センサー」の2つのセンサーの使用して、手を触れずに演奏できる「テルミン風電子楽器」を制作します。
チャプター2
新しいPython文法の学習
始めに、今回のレッスンで初めて扱うPythonの文法について学習しましょう。
2. 1 関数の引数を任意の長さ (可変長) にする方法
これまで作成してきたプログラムでは、関数が受け取る引数をあらかじめ決めておき、次のように定義してきました。
def function(a, b, c): . . .
しかし、受け取る引数を柔軟に変えられると便利な場合もあります。 print()
関数はまさにその例で、表示させたいデータを「,
(カンマ)」で区切り、いくつでも指定することができました。
print(1) # `1` とだけ表示される print(1, 2, 3) # `1`. `2`, `3` がそれぞれ表示される
ここでは、このように任意の長さで引数を受け取る方法を見ていきましょう。
■ 可変長な位置引数を受け取る方法
関数を定義するときに、引数名の前に「 *
(アスタリスク)」を付けると、任意の長さで「位置引数」を受け取ることができます。
def function(*args): . . .
※「args
」は英語で「引数」の意味がある「argument」の複数形「arguments」を省略した表記です。
このように定義した引数はタプルとして受け取った値を格納します。次のサンプルコードではこれを利用して、受け取った引数の数値を合計して、その結果を表示する関数を定義しています。
【 サンプルコード 2-1-1 】
def sum_nums(*nums):
print(nums) # `nums` 引数はタプルで全ての引数を受け取る
result = 0
for n in nums:
result += n
print(result)
sum_nums(1, 2, 3) # `6` と表示される
sum_nums(1, 2, 3, 4, 5) # `15` と表示される
■ 可変長なキーワード引数を受け取る方法
引数には、位置引数の他にも引数名を指定して渡す「キーワード引数」がありました。関数を定義するときに、引数名の前に「 *
」を2つ付けることで、任意の長さでキーワード引数を受け取ることができます。
def function(**kwargs): . . .
※「kwargs
」は、英語で「キーワード引数」を表す「keyword-arguments」を省略した表記です。
このように定義した引数は、辞書として受け取った値を格納します。次のサンプルコードでは、これを利用して、電子英単語帳のプログラムを作成しています。
【 サンプルコード 2-1-2 】
import time
from pystubit.board import display
def show_phrases(**phrases):
for phrase, japanese in phrases.items(): # `items()`メソッドでキーと値の両方を順番に取得します。
print(japanese) # 日本語を表示
time.sleep(1)
display.scroll(phrase) # LEDディスプレイに英単語を表示
time.sleep(1)
show_phrases(apple="りんご", banana="バナナ", pen="ペン") # 表示したい英単語をキーワード引数名,日本語を文字列で与える
引数は辞書として受け取るため、必ずしも指定した順序でデータが格納されるわけではありません。実際にこのプログラムを実行すると、「pen(ペン)⇒ banana(バナナ)⇒ apple(りんご)」の順に表示されます。
■ 可変長な引数を含む引数の指定順序
引数を指定するときは、必ず位置引数を先にして、その後ろにキーワード引数を指定する必要がありました。さらに可変長な引数を含む場合は、以下の順序で引数を指定する必要があります。もし、この順序に違反する関数を作成した場合、Pythonはエラーを出します。
def function(位置引数, 可変長な位置引数, キーワード引数, 可変長なキーワード引数): . . .
次のサンプルコードを実行して正常に結果が表示されることを確認し、その後で引数の順番を入れ替えて実行することエラーになることを確かめましょう。
【 サンプルコード 2-1-3 】
def sample(arg1, arg2, *args, kwarg1, kwarg2, **kwargs):
print(arg1, arg2) #`arg1`には「1」が、`arg2`には「2」が格納されます。
print(args) # `args`には`(3, 4)`のタプルが格納されます。
print(kwarg1, kwarg2) # `kwarg1`には "a" が、`kwargs2`には "b" が格納されます。
print(kwargs) # `kwargs`には`{"kwarg3":"c", "kwargs4":"d"}`の辞書が格納されます。
sample(1, 2, 3, 4, kwarg1="a", kwarg2="b", kwarg3="c", kwarg4="d")
(正常な実行結果)
1 2 (3, 4) a b {'kwarg4': 'd', 'kwarg3': 'c'}
(定義で引数の順序を入れ替えた場合の実行結果)
>OK Traceback (most recent call last): File "<stdin>", line 4, in sample SyntaxError: invalid syntax
2. 2 ループ処理内での continue文,break文,else文について
ループ処理を扱うwhile文やfor文と、これから紹介する「continue文」「break文」「else文」を組み合わせることで、高度なループ制御を行うことができます。
■ continue文の使い方
ループ処理内でcontinue文を実行すると、その回のループで残っているコードを飛ばして、次の回のループへすぐに移ることができます。
【 例:1~30の間で、奇数の場合のみ print()
関数で数値を表示する 】
for n in range(1, 31):
if n % 2 == 0:
continue # 偶数の場合は `continue文` ですぐに次のループに移る
print(n)
■ break文の使い方
ループ処理内でbreak文を実行すると、ループ処理そのものを途中で中断することができます。
【 例:ボタンAが押されるまでブザー音を鳴らし続ける 】
from pystubit.board import buzzer, button_a
buzzer.on('C4')
while True:
if button_a.is_pressed():
break # ボタンAが押された場合は `break文` でループ処理を中断する
buzzer.off()
■ else文の使い方
for文やwhile文の後ろにelse文を追加すると、break文などでループ処理が中断されずに終えた場合のみ、最後に実行する処理を書くことができます。continue文やbreak文に比べると使用する機会は多くはありませんが、なんらかの理由でループ処理の結果が上手くいかなかったことを知らせたいときなどに利用でき、見通しの良いプログラムを記述できます。
【 例:リスト内の数字を調べる 】
from pystubit.board import display
def findNumber(n, xs): # `n`には検索する数字を、`xs`には検索対象のリストを指定します。
for x in xs:
if x == n:
display.scroll('{} is found'.format(n))
break # リストの中に `n` が存在したら、処理を抜ける。
else:
# `break文` が実行されていないため、リストの中に `n` が存在しなかったことを知らせる。
display.scroll('{} is not found!!!'.format(n))
findNumber(3, [0, 1, 2, 4, 5])
チャプター3
テルミン風楽器の組み立て
テルミンとは、本体に手を触れることなく演奏できる特殊な楽器で、制作者である「レフ・テルミン」からその名前が付けられました。その独特な音色から、恐怖映画やSF映画の効果音にも使われてきました。
テルミンは2本のアンテナがあり、アンテナの周りにできる磁場を両手で制御することで、音を奏でます。右手側で音程を制御し、左手側では音量を制御できるようになっています。
ここでは、光センサーと超音波距離センサーを組み合わせて、テルミンのように、手を触れずに演奏ができる「テルミン風電子楽器」を制作します。まずは、組立説明書を開き、手順に沿って組み立てを行いましょう。
3. 1 組み立てに必要なパーツ
【 パーツ一覧 】
- Studuino:bit×1
- ロボット拡張ユニット×1
- 電池ボックス×1
- 超音波距離センサー×1
- センサー接続コード(3芯15cm)×1
- ブロック基本四角(赤)×4
- ブロック基本四角(黒)×2
- ブロック基本四角(グレー)×1
- ブロック基本四角(赤)×4
- ブロック三角(赤)×2
- ブロックハーフA(グレー)×1
- ブロックハーフB(赤)×1
- ブロックハーフC(白)×2
- ブロックハーフD(白)×1
- ステー×1
【 アーテックブロックの形状 】
3. 2 組立説明書
以下のリンク先から組立説明書を開いてください。
チャプター4
テルミン風電子楽器のプログラム作成
演奏するときは、右手を光センサーの前に、左手を超音波距離センサーの前にかざします。光センサーの値で音程(pitch)を決め、超音波距離センサーから一定の距離に手をかざすと、その音程でブザーを鳴らすことができます。
【 制作例の動作動画 】
この演奏方法を可能にする一連のプログラムの処理は、可変長なキーワード引数を取る関数にまとめます。引数に音程と光センサーの値の関係を指定することで、関数内部のコードを変えずに、鳴らせる音程の範囲を簡単に変更できるようにしてみましょう。
4. 1 プログラムの処理の流れ
テルミン風電子楽器のプログラムには次の機能が必要です。
【 テルミン風電子楽器に必要な機能 】
- 現在の光センサーの値から音程を選択してLEDディスプレイに表示する機能
- 現在の超音波距離センサーの値からブザーのON/OFFを制御する機能
これらの機能を組み合わせて、以下の流れでプログラムを処理していきます。また、全ての処理はplay_theremin
と名前を付けた1つの関数にまとめます。
では、それぞれの機能を順番に作成していきましょう。
4. 2 現在の光センサーの値から音程を選択してLEDディスプレイに表示する機能の作成
ここでは簡単に、’C4’、’D4’、’E4’の3種類の音を鳴らせる仕様でプログラムを考えてみます。まずは、それぞれの音と手をかざす位置の関係を決めましょう。
※ 下の例では、周りが明るくなるほど(光センサーから手の位置が遠いほど)音程が高くなるように設定しています。
■ 光センサーの値の範囲とそのときに鳴らす音の高さを決める
プログラムを作成する前に、それぞれの位置に手をかざしたときの光センサーの値を確認します。現在の光センサーの値を取得して、ターミナルに表示する下のプログラムを実行しましょう。
【 サンプルコード 4-2-1 】
from pystubit.board import lightsensor
import time
while True:
value = lightsensor.get_value()
print(value)
time.sleep_ms(500)
下の図に示しているように、音程が変わる境目となる位置で光センサーの値を調べます。
【 光センサーの値を調べるときの手の位置 】
調べた結果を元に、それぞれの音に対応する光センサーの値の範囲を次のように決めます。この決めた範囲はメモを取っておきましょう。
音程 | 光センサーの値の範囲 |
---|---|
C4 | 0 ≦ val < 上の図の①のときの値 |
D4 | 上の図の①のときの値 ≦ value < 上の図の②のときの値 |
E4 | 上の図の②のときの値 ≦ value < 上の図の③のときの値 |
また、これから示すプログラムでは、仮に次のように範囲を決めたものとして説明をします。
音程 | 光センサーの値の範囲 |
---|---|
C4 | 0 ≦ value < 1000 |
D4 | 1000 ≦ value < 2000 |
E4 | 2000 ≦ value < 3000 |
■ 音程と光センサーの値の関係を引数として受け取る関数の定義
プログラムを新しく作成します。まずは、可変長なキーワード引数として、上で決めた音程と光センサーの値の関係を受け取る関数play_theremin()
を定義します。
引数は次の書き方で、音程をキーとしてタプルで表した光センサーの値の範囲を要素とする辞書型のデータとして受け取ります。
play_theremin(C4=(0, 1000), D4=(1000, 2000), E4=(0, 3000))
また、このときの引数の数を可変長とするために、関数はplay_theremin(**kwargs)
と引数名の前に「*(アスタリスク)」を2つ付けましょう。
入力された引数を辞書型のデータとして正しく受け取れているかを確認するため、for~in文と、辞書型のitems()
メソッドを組み合わせて音程(変数p
)と光センサーの範囲(変数_range
)を順番に取り出してターミナルに表示してみましょう。
def play_theremin(**kwargs):
for p, _range in kwargs.items():
print('pitch:', p)
print('range', _range)
play_theremin(C4=(0, 1000), D4=(1000, 2000), E4=(2000, 3000))
※「_range
」のように名前の先頭に「_
(アンダースコア)」を付けているのは、組み込み関数の「range()
」と名前の衝突を防ぐためです。
このプログラムを実行すると次のような結果が得られます。引数は辞書型のデータとして受け取っているため、指定した順番と取り出す順番が一致しないことに注意してください。また、この入れ替わりによって、これから作成するプログラムで問題が起こることはありません。
(実行結果)
pitch: D4 range (1000, 2000) pitch: E4 range (2000, 3000) pitch: C4 range (0, 1000)
■ 光センサーの値から鳴らす音を判定してLEDディスプレイに表示する
それでは続けて、取得した光センサーの値から音程を選択してLEDディスプレイに表示するまでのコードを書いていきます。
まずは、先頭でlightsensor
オブジェクトとdisplay
オブジェクト、それからtime
モジュールをインポートしましょう。
追加【1・2行目】
from pystubit.board import lightsensor, display
import time
def play_theremin(**kwargs):
for p, _range in kwargs.items():
print('pitch:', p)
print('range', _range)
play_theremin(C4=(0, 500), D4=(500, 1200), E4=(1200, 3000))
次に、while文を利用した永久ループを用意し、その中で繰り返し光センサーの値を調べるようにします。そして、この調べた光センサーの値が、引数で受け取った光センサーの範囲(変数_range
)内にあるかを順番に判定します。もしその範囲内にあれば、そのときの音程(変数p
)を変数pitch
に格納します。
追加・変更【5行目〜9行目】
from pystubit.board import lightsensor, display
import time
def play_theremin(**kwargs):
while True:
value = lightsensor.get_value()
for p, _range in kwargs.items():
if value >= _range[0] and value < _range[1]:
pitch = p
play_theremin(C4=(0, 500), D4=(500, 1200), E4=(1200, 3000))
※変数_range
はタプルであるため、_range[0]
で範囲の下限値を、_range[1]
で範囲の上限値を取り出すことができます。
※また、値を表示する必要がなくなったため、関数print()
は削除しています。
もし、指定されたどの範囲にも光センサーの値がない場合、上のプログラムのままでは、変数pitch
が定義されず、後の処理でエラーが発生する原因となります。そこで、break文とelse文を使い、範囲内にあった場合はbreak文でループを強制的に終了し、見つからなかった場合のみ、else文で代わりに変数pitch
にNone
を格納します。
※Noneは値が存在しないことを表す特別なオブジェクトです。
追加【10行目〜12行目】
from pystubit.board import lightsensor, display
import time
def play_theremin(**kwargs):
while True:
value = lightsensor.get_value()
for p, _range in kwargs.items():
if value >= _range[0] and value < _range[1]:
pitch = p
break
else:
pitch = None
play_theremin(C4=(0, 500), D4=(500, 1200), E4=(1200, 3000))
続けて、変数pitch
に格納された音程を表す文字をLEDディスプレイに表示するコードを追加します。このとき、変数pitch
がNone
だった場合はエラーとなるため、処理を分けて、代わりにLEDディスプレイの表示を消します。
LEDディスプレイには同時に2文字以上を表示することができないため、音程を表す文字列'C4'
や'D4'
から音の種類を表す頭文字のアルファベットのみ取り出して表示します。文字列もリストやタプルと同様にシーケンスなデータであるため、pitch[0]
とすることで頭文字だけを取り出すことができます。
また、show()
メソッドでLEDディスプレイを点灯させるときに明るさを最大にしてしまうと、その光で光センサーの値が大きく変化してしまいます。そこで、引数にcolor=(5, 0, 0)
を指定して、暗めに点灯させるようにします。
【 サンプルコード 4-2-2 】
追加【14行目〜17行目】
from pystubit.board import lightsensor, display
import time
def play_theremin(**kwargs):
while True:
value = lightsensor.get_value()
for p, _range in kwargs.items():
if value >= _range[0] and value < _range[1]:
pitch = p
break
else:
pitch = None
if pitch: #「None」の場合はFalseとして扱われます。
display.show(pitch[0], delay=0, color=(5, 0, 0))
else:
display.clear()
play_theremin(C4=(0, 500), D4=(500, 1200), E4=(1200, 3000))
ここまでのプログラムを実行して動作を確認しましょう。
4. 3 現在の超音波距離センサーの値からブザーのON/OFFを制御する機能
超音波距離センサーで測定した距離が一定の値を下回ると、【 サンプルコード 4-2-2 】で決めた音程でブザーを鳴らすようにします。まずは、自分の演奏しやすい位置に左手をかざしたときの距離を調べましょう。調べた結果から、この位置よりも手を近くにかざすと音を鳴らすとコンピューターに判断させる「しきい値」を決めます。
■ 超音波距離センサーの値を調べる
【 サンプルコード 4-2-2 】とは別で新しいプログラムを作成します。画面上部の「新規」をクリックして作成される新しいタブ内に、以下の超音波距離センサーの値をターミナルに表示するプログラムを書きましょう。
【 サンプルコード 4-3-1 】
from pyatcrobo2 import UltrasonicSensor
us = UltrasonicSensor('P0')
while True:
val = us.get_distance()
print(val)
このプログラムを実行して調べた結果から、ブザーのONとOFFを切り替える境目となる距離(しきい値)を決めます。以降の説明では、しきい値を「8cm」として、プログラムを作成していきます。
■ 超音波距離センサーに手を近づけると選択した音程でブザーを鳴らす
まずは、ロボット拡張ユニットの「P0」に接続している超音波距離センサーを利用するための準備を行います。UltrasonicSensorクラスをインポートして、そのインスタンスを作成し、変数us
に格納しましょう。
続けて、超音波距離センサーの値をget_distance()
メソッドで取得します。音程が選択されていない場合は調べる必要がないため、変数pitch
に音程を表す文字列が格納さている場合のみ実行される位置にコードを書きます。また、超音波距離センサーは音の反射を利用しているため、発信した音波が戻ってくるまでに掛かる時間より短い間隔で連続して値を取得すると失敗してしまいます。そこで、time
モジュールのsleep_ms()
関数で次の取得まで20ミリ秒ほど時間を空けるようにします。
追加【2行目、5行目、20・21行目】
from pystubit.board import lightsensor, display
from pyatcrobo2.parts import UltrasonicSensor
import time
us = us = UltrasonicSensor('P0')
def play_theremin(**kwargs):
while True:
value = lightsensor.get_value()
for p, _range in kwargs.items():
if value >= _range[0] and value < _range[1]:
pitch = p
break
else:
pitch = None
if pitch:
display.show(pitch[0], delay=0, color=(5, 0, 0))
distance = us.get_distance()
time.sleep_ms(20)
else:
display.clear()
play_theremin(C4=(0, 500), D4=(500, 1200), E4=(1200, 3000))
取得した値を利用して、ブザーを制御します。buzzer
オブジェクトを新たにインポートして、取得した値が決めたしきい値より小さい場合(距離が短い場合)は、on
メソッドで変数pitch
の音を鳴らし、そうでない場合はoff()
メソッドで止めましょう。
また、超音波距離センサーの値がしきい値を超えていない場合でも、光センサーの値が範囲外になった場合は、音程が選択されていないので、こちらもoff()
メソッドを実行して、音を止めるようにしましょう。
【 サンプルコード 4-3-2 】
追加・変更【1行目、23行目~26行目、29行目】
from pystubit.board import buzzer, display, lightsensor
from pyatcrobo2.parts import UltrasonicSensor
import time
us = UltrasonicSensor('P0')
def play_theremin(**kwargs):
while True:
value = lightsensor.get_value()
for p, _range in kwargs.items():
if value > _range[0] and value <= _range[1]:
pitch = p
break
else:
pitch = None
if pitch:
display.show(pitch[0], delay=0, color=(5, 0, 0))
distance = us.get_distance()
time.sleep_ms(20)
if distance < 8:
buzzer.on(pitch)
else:
buzzer.off()
else:
display.clear()
buzzer.off()
play_theremin(C4=(0, 500), D4=(500, 1200), E4=(1200, 3000))
これでプログラムの完成です。プログラムを実行して、3つの音程とブザーのON/OFFを両手で制御してみましょう。また、動作が確認できたら、今度は関数play_theremin()
に渡す引数を変更して、より広い範囲で音程を制御できるようにプログラムを変更してみましょう。
チャプター5
課題:遠い位置に手をかざすと1オクターブ高い音程でブザーを鳴らす
ここでは課題として、さきほどよりも手を遠い位置にかざしたときに、引数で指定された音程より1オクターブ高い音程で音を鳴らせるようにプログラムを変更してみましょう。
<ヒント>
【 サンプルコード 4-3-2 】の23行目~26行目の分岐処理に新たな条件を追加します。
5. 1 プログラムの作成例
まずは、【 サンプルコード 4-3-1 】を使用して、新たに「1オクターブ高い音程で鳴る」区間のしきい値(上限値と下限値の両方)を決めます。
以下では仮に、「16cm以上24cm未満」とした場合の作り方を説明します。
次に、決めたしきい値を使い、以下のelif文で超音波距離センサーの値を利用した分岐をさらに1つ追加します。
elif distance >= 16 and distance < 24:
この分岐の中で、選択された音程より1オクターブ高い音程を表す文字列を作成します。音程は'C4'
のように、アルファベットと数字の組み合わせです。1オクターブ高い音程はこの数字の方を1大きくしたものになります。そこで、文字列がシーケンスなデータであることを利用して、pitch[1]
で数字の所を取り出し、関数int()
で数値に変換してから1を足します。この計算結果をさらに関数str()
で文字列に戻し、pitch[0]
と足すことで、新たに1オクターブ高い音程を表す文字列を取得できます。この文字列を変数new_pitch
に格納し、on()
メソッドで鳴らすようにしましょう。
elif distance >= 16 and distance < 24:
new_pitch = pitch[0] + str(int(pitch[1])+1)
buzzer.on(new_pitch)
これでプログラムの完成です。完成したプログラムの全体像は以下になります。
【 サンプルコード 5-1-1 】
追加【25行目~27行目】
from pystubit.board import buzzer, display, lightsensor
from pyatcrobo2.parts import UltrasonicSensor
import time
us = UltrasonicSensor('P0')
def play_theremin(**kwargs):
while True:
value = lightsensor.get_value()
for p, _range in kwargs.items():
if value > _range[0] and value <= _range[1]:
pitch = p
break
else:
pitch = None
if pitch:
display.show(pitch[0], delay=0, color=(5, 0, 0))
distance = us.get_distance()
time.sleep_ms(20)
if distance < 8:
buzzer.on(pitch)
elif distance >= 16 and distance < 24:
new_pitch = pitch[0] + str(int(pitch[1])+1)
buzzer.on(new_pitch)
else:
buzzer.off()
else:
display.clear()
buzzer.off()
play_theremin(C4=(0, 1000), D4=(1000, 2000), E4=(2000, 3000))
チャプター6
おわりに
6. 1 このレッスンのまとめ
このレッスンでは新たに次のことを学習しました。
- 任意な数の引数をもつ関数の定義方法
- 可変長な位置引数を受け取るときは「
*
(アスタリスク)」を1つ引数名の前に付けること。また、この方法で定義した引数には、データがタプルに格納されて渡されること。 - 可変長なキーワード引数を受け取るときは「
*
」を2つ引数名の前に付けること。また、この方法で定義した引数には、引数名をキーにデータを要素とする辞書に格納されて渡されること。 - for文やwhile文の中で「continue文」を使うと、残りの処理を飛ばして、次のループに移ることができる。
- for文やwhile文の中で「break文」を使うと、ループを途中で中断できる。
- for文やwhile文の後に「else文」を追加すると、break文で中断しなかった場合のみ実行する処理を記述できる。
どれも大事な文法ですので、しっかりと理解するように努めてください。
6. 2 次のレッスンについて
次のレッスンでは、ファイルからの入力処理(読み込み)と、ファイルへの出力処理(書き込み)について学習します。