例題5-2:繰り返し使う処理をまとめる(2)

A: よっし、じゃあ改めてキー入力待ち関数を作っていくぞ。例題5-1ではここまで作成したんだった(サンプル3)。 例題5-2ではこれに引数や戻り値を付けて機能を拡張していこう。

サンプル3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python
# -*- coding:shift_jis -*-

from pygame import *
from pygame.locals import *

def waitkey():
    waitingKeyPress = True
    while waitingKeyPress: #スペースキーが押されるのを待つ
        for e in event.get():
            if e.type == KEYDOWN and e.key == K_SPACE:
                waitingKeyPress = False

init()
display.set_mode((256,256))
waitkey()

B: 引数や戻り値をつけると機能UPするんですか?

A: いや、もちろん何でもつけりゃいいってわけじゃないが。でも、今のwaitkey()関数って不便だと思わないか?

B: へ? 何がですか?

A: 例題5-1の最初でB君が書こうとしていたプログラムではXかCのキーが押されるまで待つ、ってのがあったじゃんか。 今のwaitkey()関数だとスペースキーしか待てないから、このままだとXとCを待つ関数waitXCkey()か何かを作らないといけない。

B: はあ。作ればいいんじゃないでしょうか?

A: そんなもんいちいち作ってたら、waitなんちゃら()関数を山ほど作んないといけないだろ。そうしたら再利用しようとした時に不便じゃん。

B: ならいっそすべての実験をスペースキー押しにすれば…

A: まあそれもひとつの手だが、必ずそうできるとは限らないからな。被験者に2択で反応させたい場合とかどうする?

B: うっ。

A: まあとにかく、waitkey()関数で何のキーが押されるのを待つか指定するようにしてみよう。改造の方針は以下の通り。

  • 引数で待つキーを指定する。キーは複数個指定できるようにする。
  • 押されたキーが戻り値として得られる。

B: 複数個指定するって、10個でも20個でもいけるんですか?

A: 指定したければ、な。そんなに指定したら被験者はどのキーを押すのか覚えられないと思うが。 さて、待つべきキーがkeylistという名前のリストとして与えられるとしよう。前回話したように、関数の定義は def 関数名( 引数 ): と書くのだから、waitkey()関数の定義に以下の太字のように書き足す。

サンプル4(その1)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env python
# -*- coding:shift_jis -*-

from pygame import *
from pygame.locals import *

def waitkey(keylist):
    waitingKeyPress = True
    while waitingKeyPress: #スペースキーが押されるのを待つ
        for e in event.get():
            if e.type == KEYDOWN and e.key == K_SPACE:
                waitingKeyPress = False

B: ふむふむ。

A: 次はwhileループを終了する条件だが、それはどこで定義されている?

B: えっと、9行目でwaitingKeyPressがFalseなら終了します。

A: そんなこと分かっとる。waitingKeyPressがFalseになる条件は?

B: 11行目のif文ですかね。

A: そうそう。eventのタイプがKEYDOWNで、キーがK_SPACEなら終了するわけだが、 ここを「keylistで指定されたキーが押されたら終了する」という条件に書き換えればいい。

B: ええっと。keylistの中には何が入ってるんですかね?

A: 何を入れたらいいと思う?

B: えっ? 何をって言われても… K_SPACEとかが入っていればいいんですかね。

A: そう、その通り。Xキーを待つのならK_x、スペースキーを待つのならK_SPACE、といった値が並んだリストにするのが良いね。 そういうリストがkeylistに格納されているとして、このリスト内のキーが押されたかどうかを確認するにはどうすればいい? スペースキーを確認する場合が11行目だから、これを基本として考えればいい。

B: Xキーを待つならe.type == KEYDOWN and e.key == K_x、Cキーを待つならe.type == KEYDOWN and e.key == K_c、…かな。

A: そうだな。keylistの要素それぞれについてそのチェックを行えばいい。

B: リストのそれぞれについてですか、じゃあforを使えばいいですかね。ちょっと手ごわそうだぞ。

A: forでも間違いじゃないけど、もっと便利な方法があるだろ。

B:

A: 例題3-3を復習したまえ。

B: 例題3-3は…っと。あぁ、in演算子ですね! e.type == KEYDOWN and e.key in keylist !

A: 正解。11行目のifの条件式を置きかえれば目的達成だ。サンプルとしてスペースキー、Xキー、Cキーを待つようにしてみたぞ。

サンプル4(その2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python
# -*- coding:shift_jis -*-

from pygame import *
from pygame.locals import *

def waitkey(keylist):
    waitingKeyPress = True
    while waitingKeyPress: # keylistに含まれるキーが押されるのを待つ
        for e in event.get():
            if e.type == KEYDOWN and e.key in keylist:
                waitingKeyPress = False

init()
display.set_mode((256,256))
waitkey([K_SPACE, K_x, K_c])

B: えいっ!実行! …おお、確かにスペースでもXでもCでも終了できます。

A: これで入力待ちするキーの複数指定はあっさりクリアだな。後はどのキーが押されたのかを戻り値で得る方法だが、これもreturnを使えばすぐに出来る。 11行目のifに引っかかった時点で、e.keyには押されたキーが入っているわけだから、前回出てきたreturnを使ってe.keyの値を返せばいい。

サンプル4(その3)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python
# -*- coding:shift_jis -*-

from pygame import *
from pygame.locals import *
import time

def waitkey(keylist):
    while True: # keylistに含まれるキーが押されるのを待つ
        for e in event.get():
            if e.type == KEYDOWN and e.key in keylist:
                return e.key

init()
display.set_mode((256,256))
keyname = key.name(waitkey([K_SPACE, K_x, K_c]))
quit()
print keyname
time.sleep(5)

B: あれ、ずいぶんいろいろ増えましたね。

A: 16行目以降にpygameウィンドウを閉じて押されたキーを表示する処理を付けたからね。16行目のkey.name()というのは 正式にはpygame.key.name()という関数で、キーの番号を人間に分かりやすい文字列に変換してくれるありがたい関数だ。あと17行目のquit()は正式にはpygame.quit()で、14行目のpygame.init()で開いたウィンドウを閉じる関数。 from pygame import *とせずにimport pygameとして全部正式名称で書いた方が分かりやすかったかもな。行き当たりばったりで書いてるとこうなるんだよ。まったく。

B: 自分で自分に突っ込まなくても。

A: あと、11行目のifの条件式がTrueになればreturnでこの関数を抜けてしまうので、変数waitingKeyPressは不要になった。だからwaitingKeyPressを削除して9行目のwhileの条件式をTrueにしている。 こうすると、12行目のreturnで関数を抜けるまでずっとwhileループが繰り返される。

B: なるほど。

A: さて、説明はこのくらいにしておいて実行してみてくれ。keylistに含まれたキーを押した後、pygameのウィンドウが閉じてコマンドシェルの画面にこんなふうに押したキーの名前が表示されるはずだ。

../_images/05-2-01.png

B: おお、確かに。

A: これで目標達成!と言いたいところだが、最後にもうひとつ機能を追加しておこう。心理実験では、2秒以内に反応がなければ「反応なし」とするといった感じで反応のタイムアウトを設ける場合がある。この機能を追加してみよう。

B: そんなことまでできるんですか。

A: おう、簡単にできるぞ。先にプログラムを見てもらおうか。

サンプル4(完成)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python
# -*- coding:shift_jis -*-

from pygame import *
from pygame.locals import *
import time

def waitkey(keylist,timeout):
    startTime = VisionEgg.time_func()
    while VisionEgg.time_func()-startTime < timeout:
        for e in event.get():
            if e.type == KEYDOWN and e.key in keylist:
                return e.key
    return

init()
display.set_mode((256,256))
key = waitkey([K_SPACE, K_x, K_c], 2.0)
if key == None:
   keyname = 'no response'
else:
    keyname = key.name(key)
quit()
print keyname
time.sleep(5)

B: むー、さらに複雑になりましたね。

A: ポイントは9行目と10行目。関数に入ってすぐに変数startTimeにVisionEgg.time_func()でキー入力待ちの開始時刻を格納しておく。 そして10行目でstartTime()からの経過時刻がtimeoutより短い間はwhileループを回してキー入力を待つ。このあたりは例題1-1でやった「5秒待つ」の応用だね。 このtimeoutも引数として関数に与えてやらないといけないわけだが、複数の引数を与える場合は8行目のように , で区切って並べるとよい。呼び出すときも18行目のように引数を , で区切る。

B: ふんふん。

A: あともうひとつ説明が必要なのが14行目。timeoutで指定した時間以内にkeylistで指定したキーが押されなかった場合、10行目のwhileループが終了する。 そうするとwhileと同じ字下げになっている14行目に処理が飛ぶ。もしtimeoutした場合に特別な処理が必要なら、その処理をここに書けば良い。この例では単にreturnしている。 pythonは察しがいいのでこのreturnは書かなくても自動的に関数を終了してくれるが、書き方の例として入れてみた。

B: returnって一つの関数に複数書けるんですね。

A: 書ける。

B: returnの後ろに戻り値を書かなければどうなるんですか?

A: それは19行目から22行目を見てもらえばわかる。return文なしに関数が終了したり、returnの後に戻り値を書かずにreturnした場合はNoneという値が戻り値になる。要するに「無い」という値があるわけだな。 だから、18行目のwaitkey()の結果がタイムアウトだった場合、変数keyにはNoneが入っている。そこで19行目でkeyの値がNoneだったらキーが押されなかったという意味を込めて’no response’という文字列をkeynameに代入して、 None以外だった場合はキー番号が入っているはずなのでkey.name()でキー名に変換している。

B: なるほど。ところでこれ、VisionEgg.time_func()-startTimeの値はキーを押すまでの反応時間なんですよね? そうしたらこの値をreturnするようにすれば反応時間も測れるんでしょうか?

A: むむっ。なかなか鋭いな。反応時間のことは例題を改めて詳しく説明したかったのだが。まあ、B君の質問のとおり、この関数を少し拡張すれば反応時間を測る事が出来る。 複数の値をreturnする例としてちょうどいいので簡単に説明しておこうか。returnの時にVisionEgg.time_func()をもう一度呼び出すのが気持ち悪くなければ、13行目と14行目を以下のように書きかえればよい。

13
14
                return (e.key, VisionEgg.time_func()-startTime)
    return (None, VisionEgg.time_func()-startTime)

B: VisionEgg.time_func()を呼ぶのが気持ち悪い? なぜですか?

A: 10行目でVisionEgg.time_func()を呼んでるのにもう一度呼ぶのは無駄だろ。 それに10行目でtimeoutを超えてから14行目でもう一度VisionEgg.time_func()するということは、1ミリ秒以下だろうけど誤差が生じる。

B: 1ミリ秒以下って、セコすぎです!

A: まぁそうなんだけどさ。性能が低い昔のPCで苦労した年寄りの悪い癖なのかねぇ。 とにかく、pythonではタプルを関数の戻り値に出来るので、戻したい値が複数ある時はタプルで戻してやればいい。 当然受け取る側もタプルを受け取る事を前提に書きなおさないといけないが、これは練習問題ね。

B: はーい。

A: さて、と…。かなり夜も更けてきたな。関数の書き方はとりあえずこれで一区切りだが、 引数についていくつか言い逃した事を補足しておくか。まず、pythonの関数の引数はすべて 参照渡し という方法で関数に渡される。 C/C++言語などを学んだことがある人はこれだけでピンと来るだろうが、初めてプログラムを学ぶ人にはちょっと難しいかもしれないな。

B: 参照渡し? なんですかそれ?

A: そうだなあ、例え話で言うと、研究室のPCに入っているExcelのデータを誰かに処理してもらいたいとする。 この時、研究室のPCからデータをコピーして渡す方法と、データを保存しているPCを教えてデータを処理しておくよう頼む方法が考えられる。 前者の方法を値渡し、後者の方法を参照渡しと言う。

B: はあ。

A: 値渡しだと、処理が終わったデータを返してもらわないと処理後のデータが手に入らない。 その代り、頼まれた人がコピーして渡したデータをぐちゃぐちゃに書き換えてしまっても研究室のPCのデータは無事だ。

B: ふむふむ。

A: 参照渡しだと、処理が終わったらPCに処理済みのデータが入っているのでいちいち返してもらう必要はないが、 その代りデータをぐちゃぐちゃに書き換えられてしまったらデータはパアだ。

../_images/05-2-02.png

B: むむ。バイト先の後輩D君に頼む時には値渡しにしなくちゃ。

A: 誰だよD君って。まあ値渡しも参照渡しもそれぞれ一長一短なわけだが、pythonではすべて引数は参照渡しで関数に渡される。 従って、関数の中で変に引数を操作してしまうと、引数のデータが書き変わってしまう。例えば次のようなプログラムを実行すると、foo()の中でリストxの値が書き換えられて しまうので、最初のprint xでは[1,2,3]と表示されるが、2回目のprint xでは[1,2,0]と表示される。

def foo(L):
    L[2] = 0 # 引数Lに変更を加えている

x = [1,2,3]
print x  # [1,2,3]と表示される
foo(x)
print x  # [1,2,0]と表示される

B: えーと、これは何かまずいんですか?

A: 関数のおいしい点は、一度関数としてまとめてしまえば数ヵ月後に関数の中身をきちんと覚えていなくても コピー&ペーストすれば使える事だ。もともと今回関数を解説し始めた最初の目的もそれだったわけだ。 それなのに、こういう風に値が書き変わってしまう関数を作ってしまうと、関数を実行する事でどの引数がどのように書き変わってしまうのか覚えておかないといけないだろ。

B: あ、なるほど。

A: まあ書き変わってしまうことを理解した上で書いてるんならそれで良いんだけど、この事を理解せずにプログラムを 書いていると思わぬミスを犯してしまう恐れがあるわけだ。個人的な意見だが、 参照渡しの長所と短所がよくわからない間は、関数内で引数を書き替えないことを強くお勧めする 。 C言語では参照渡しを使って引数の値を書き替える事は基本的なテクニックだが、これはC言語がpythonのようにタプルを使って複数の値を戻り値として返す事が出来ない という点と深く関係している。pythonはそんな事をしなくても複数の戻り値を返せるのだから、わざわざ危険を冒すことはない。

B: うーん。わかったような、わからないような、よくわかりませんけどとにかく関数内で引数を書き替えないって覚えておきます。

A: よろしい。もうひとつ押さえておきたいのが デフォルト値付き引数 だ。 通常、関数は1番目の引数はキーのリスト、2番目の引数はタイムアウト…という具合に、何番目が何の値だったかを全部覚えておいて、全部書かないといけない。 しかし、pythonでは引数に名前を付けておくことで、引数の名前で値を渡す事が出来る。また、値を渡さなかった時に自動的に補う値(デフォルト値)を指定する事が出来る。 例えば今書いたwaitkey()関数で、8行目を以下のように変更する。

def waitkey(keylist=[K_SPACE,K_ESCAPE],timeout=2.0):

A: このようにしておくと、以下のように引数の名前を指定して値を渡す事が出来る。pythonは引数の名前を使って 正しく関数に値を渡す事が出来るので、順番は自由に変更できる。

k = waitkey(timeout = 5.0, keylist = [K_x, K_c, K_SPACE])

A: また、以下のように引数を省略する事が出来る。

k = waitkey(timeout = 5.0) # keylistとして[K_SPACE,K_ESCAPE]が補われる
k = waitkey(keylist = [K_x, K_c]) # timeoutとして2.0が補われる
k = waitkey() # keylistとして[K_SPACE,K_ESCAPE]が、timeoutとして2.0が補われる

B: あー、これ、VisionEggのViewport()関数とかTarget2D()関数とかで見た書き方ですね。なるほど、そういう意味だったのか。

A: ひとつ注意しないといけない事がある。 ひとつの関数でデフォルト値付き引数とデフォルト値がない普通の引数を両方使いたい場合は、 デフォルト値がない普通の引数を先に書かねばならない 。 つまり、次のような関数はエラーになる。

def foo(x = 100, y, z):

B: う、これはやってしまいそう。

A: まあ、心理実験のプログラムを書くにあたって、わざわざ普通の引数とデフォルト値付き引数を両方つかわないといけない関数を描く必然性はないと思うよ。 デフォルト値付き引数を使いたければ、全部の引数をデフォルト値付き引数にしときゃいいのさ。デフォルト値付き変数の話はこのくらいにしておいて、他に 任意引数 というものもある。

B: 任意? 次々と新しい言葉が出てきますね。

A: ここから先は初級レベルを超える話なので、とりあえず簡単な実験プログラムが書ければいい人は読み飛ばして良い 。 任意引数とは、あってもなくても良い引数だ。もちろんあってもなくてもどうでもいいという意味ではなく、 必要に応じて値を渡したり渡さなかったりする事が出来る引数だ。具体的には、以下のように普通の引数の最後に*を付けた引数をつける。 このように書いておくと、普通の引数より多い引数が与えられた場合、過剰な引数はリストとして*がついた変数に渡される。pythonインタプリタで関数を直接定義して実行してみよう。 ここは初級以上のレベルを想定しているのでインタプリタの操作の説明はパスね。

>>> def foo(x, y, *z):
...     print x, y
...     for v in z:
...         print v
...
>>> foo(1,2,3,4,5)
1 2
3
4
5

B: はあ…?

A: キーワード引数は、デフォルト値付き引数のようなものを任意引数のように与えられるものだ。 キーワード引数は以下のように*を2個つけた引数として指定する。

>>> def foo(x, y, **z):
...     print x, y
...     for key, value in z.items():
...         print key, value
...
>>> foo(4,3,a=2,b=1,c=0)
4 3
a 2
b 1
c 0

B: あのー。さっぱりわかりません。

A: 心理実験のプログラムを書く程度ならこんな複雑な引数を持つ関数はまず必要ない。と、思う。 ただ、次回あたりそろそろクラスの説明に入ろうと思ってるんだが、そこでこのキーワード引数が出てくるのさ。まあ、キーワード変数が何たるか わからなくても何も困らないと思うけど。正直なところ、私もpythonを勉強し始めた頃はこの**がついた変数が何なのかさっぱりわからなかったんだよな。 わからないけれども、とりあえず動くからいいかってな感じでプログラムを書いてた。そんなもんだ。

B: うーん。心理実験のプログラムってレベル低いんですか?

A: レベル低いというか…。何と言うか、プログラムの目的に応じて求められるテクニックが違うというのが 正しいんじゃないかな。心理実験には心理実験の難しさがある。だからこそこうやって心理実験のプログラムに特化した解説の存在意義があるってもんさ。

B: あ、なんかきれいにまとめようとしてますね?

A: そうだな。もうすぐ日付も変わろうかという時間だし、今回の話はそろそろ締めくくろうか。 関数は非常に重要なので、しっかり復習して、自分でいろいろな関数を書こうとしてみてほしい。そうすることで理解が深まっていくと思う。 じゃあ、私はそろそろ帰るよ。エアコンを切るの忘れないでね。んじゃ、おやすみー。

B: おやすみなさーい