.. title:: Pythonで心理実験 - 例題1-3 例題1-3:実験っぽくする ========================== **A:** さて、今回は前回の例題に少し手を加えてより実験っぽくするぞ。これが今回の例題のサンプルプログラムだ。 **B:** 少し手を加えてって、100行超えるじゃないですか。 **A:** 実験ぽくするために課題文を56行も入れたからね。実質的には数行しか増えていない。 + 行番号なしのソースファイルをダウンロード→ `01-3.py `_ .. literalinclude:: source/01-3.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** 前回予告した事を覚えているか。今回は関数とクラスの解説をする。前回説明を飛ばした80行目に出てきているtextobj = Text(... と91行目のtextobj.parameters.text=...を理解するのが目標だ。 **B:** なんかまた話が長~くなりそうで嫌だなあってことしか覚えていません。 **A:** そうか、たっぷり話てやるから覚悟しろ。まず、関数って中学だか高校だかの数学で習った事を覚えているか? **B:** 中学だか高校だかっていいかげんな。ええと、y=3x+2とかそういう奴ですよね。グラフとか描かされたりして。 **A:** そう。xに1という数字を入れるとyに5という数字が、-2という数字を入れると-4という数字がyから出てくる。こういう具合に、何かを入れると **入れた物に対応した何かが戻ってくる** ものを関数というんだよ。 pythonの関数も基本的には同じで、何かを入れると対応した何かが戻ってくるブラックボックスを関数と言うんだ。 **B:** ブラックボックス? 何をやっているかわからなくて怪しそうな名前…。 **A:** いや、むし中で何をやっているかは隠れてる方がいいんだよ。たとえばxの正弦の値を戻すy=sin(x)という関数があるとする。 コンピュータはsinのマクローリン展開かなにかを使って計算してるんだろうが、そんな中でやっている計算をいちいち意識しないと使えないようでは困る。「xの正弦を求めたければsin(x)と書く」。 それだけ知っていれば使える方が望ましいんだよ。 **B:** ま、まく…? **A:** マクローリン展開。まあそれは今回のテーマとは関係ないから細かい事はどうでもいい。ここでちょっと専門用語を説明しておくと、関数に入れるものを引数、戻ってくるものを戻り値、あるいは返り値という。 y=sin(x)の場合、xが引数でyが戻り値だな。 **B:** ふんふん。 **A:** さて、この用語をさっそく使って、pythonの関数と数学の関数の違いを説明するぞ。pythonの関数は #. 引数や戻り値は数値以外のものをとる場合がある。 #. 引数が同じでも状況に応じて戻り値が異なる場合がある。 #. 関数が戻り値を返す以外の機能(副作用)を持つ場合がある。 #. 戻り値が存在しない場合がある。 **B:** 「場合がある」って、なんかずいぶんいい加減ですね。 **A:** 柔軟と言ってほしいな。数値以外を引数にし得るってのはリスト変数とか文字列とかを放り込めるって事だね。 次の戻り値が異なる場合があるってのは、数学ではy = 3x + 2wで何故かxにしか値を入れられないようなものを考えればいいかな。その時のwの値が何であるかによって同じxを放り込んでもyに戻ってくる値が変わるでしょ。 **B:** うーん。よく分かりませんが…。wって何ですか? **A:** 具体的な例を出した方がいいかな。例題1-1から時間を求めるのに使っている関数、VisionEgg.time_func()。これは引数がない関数の例にもなっているね。 これ、戻り値が毎回違うってのはわかるよね? **B:** 時刻を教えてくれる関数なんだから、毎回値が変わるのは当たり前だと思いますが。 **A:** そう、当たり前だ。コンピュータの中に時計機能が内蔵されているわけだが、この時計が先の例でいうところのwに当たる。VisionEgg.time_func()で時計が指している時刻を変える事は出来ない。 そして、時計が指している時刻によってVisionEgg.time_func()の戻り値は変化する。 **B:** わかったような、わからないような…? **A:** まあ、最初はあまり深く考える必要はないよ。VisionEgg.time_func()と唱えれば時刻が戻ってくる。使い方が分かっていればそれでいい。 **B:** じゃあなんでわざわざ説明するんですか! **A:** 引数や戻り値という用語はこれから何度も使うから、一応覚えてもらっておく必要がある。じゃあ引数ってなんだ、戻り値ってなんだって話になるとやはり一応は説明しておかないといけないからね。 **B:** むー。 **A:** まあまあ。先へ進むぞ。次は副作用の話。これは今までの解説せずに飛ばしてきたscreen.clear()なんかが該当するね。今回のサンプルプログラムでは97行目だね。 この関数を実行すると、VisionEggのウィンドウが灰色に塗りつぶされる。言いかえると、この関数はVisionEggのウィンドウを灰色に塗りつぶすという副作用を持つんだ。まあ、「副作用」というのはあくまでプログラム上で 値として変数で受け取る事が出来ないというだけで、この副作用こそがscreen.clear()のメインの目的だ。 **B:** むむむー。 **A:** 最後の戻り値が存在しない場合があるってのは、この副作用と関係がある。副作用がメインの目的で、特に戻り値を返す必要がない関数では戻り値がない場合がる。まあもっと上達していすれ自分で関数を 作るようになれば、この辺りの知識が必要になってくるが、今は「ふーん」程度に思っておいてくれればいい。 **B:** そんなんでいいんですか。 **A:** いいよ。いきなり理解出来なくてもいいから、少しずつ専門用語に慣れていけばいいのさ。関数を自分で作る時に初めて専門用語を学ぶよりもちょっとはマシなんじゃないかと思う。 **B:** まあそういう事にしておきます。 **A:** さて、これでようやく80行目の説明を半分くらいすることが出来る。 .. literalinclude:: source/01-3.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 80-82 **A:** 画面に文字列を表示するためのText()という関数を呼び出して、変数textobjに戻り値を格納している。()の中に書かれているのが引数で、左から順番にtextという引数の値が""、anchorという引数の値が'center'、…となっているわけだ。 **B:** 81行目と82行目は80行目の続きなんですか? **A:** ああ、これは説明していなかったね。pythonでは、カッコが閉じていない場合、ひとつの文を複数行にわたって書く事が出来る。80行目のText(text...の左カッコが閉じてないだろう? 82行目最後の右カッコが対応するカッコで、ここまでが関数Text()の呼び出しだね。 **B:** font_nameとかfont_sizeってのは文字のフォントや大きさを指定してるんですかね。 **A:** その通り。positionとanchorは表示する位置の指定だね。positionは画面の左下を(0,0)として、右方向、上方向に何ピクセルかで指定する。スクリーンの解像度を1024×768に設定してる時はこんな感じだね。 .. figure:: img/01-3-01.png **A:** anchorはこんな感じでpositionで指定した位置に文字列のどの部分が一致するかを指定する。 .. figure:: img/01-3-02.png **B:** なるほどなるほど。ところでこれ、textobjには戻り値が格納されるって事でしたけど、何が戻ってくるんですか? **A:** ふむ。それを説明するためには **クラス** を理解してもらう必要がある。VisionEgg.Text.Textというクラスの **インスタンス** が格納されるんだ。 **B:** インスタント? **A:** インスタンス、だよ。どう説明するのがわかりやすいかなぁ。例えばさ、唐突だけど、たい焼き屋さんでこしあんとかよもぎあんとか、いろんなたい焼きを店頭に並べて売ってるとする。 **B:** は? たい焼きですか? **A:** そう。焼き上がりは中身の餡の区別がつかないから、メモ用紙に焼きあがった順番に餡が何だったか書いていって、売れるたびにメモから消していくとする。 **B:** …? **A:** ちょっとミスしてしまったら、メモと売り物の対応がムチャクチャになっちゃうよね。 **B:** そりゃそうですね。でも今の話と何の関係があるのかわかんないんですが… **A:** もっと確実に管理するにはどうしたらいいか? たい焼きひとつひとつに餡に対応した印をつければいい。しっぽに切れ込みがなければこしあん、切れ込みひとつならよもぎあん。 **B:** …。 **A:** クラスの概念をきちんと教えている人からは怒られそうだけど、雑な話をすれば、これと似たような発想をもっと洗練させたのがクラスなんだよ。 例えばプログラムを書いていて、todayという変数に30という数字が入っているとしよう。うかつにもこの数字が何を意味しているの忘れてしまった。もしかして今日が何日かが格納されているんだったら、この数字は マイナスとか32以上の数字になっちゃいけない。けれども今日の収支が格納されているんだったら、マイナスにも1万にもなるだろう。だから、30という値と一緒にこの数字が何を意味してるのかという印や、 この数字が適切かどうかチェックする機能もセットにしてtodayという変数に格納出来たら便利だろう? **B:** なんか話が急展開しすぎてよくわかりません… **A:** まぁまずひと通り言わせてくれ。こういう具合にデータの集合とその扱い方をひとまとめにしたものを **オブジェクト** と言い、オブジェクトの定義を **クラス** という。 そしてクラスを使って実際に作成されたオブジェクトをそのクラスの **インスタンス** と言う。 もっとも、文献によってはインスタンスの事をオブジェクトと呼んでいる場合もあるので注意が必要だ。たとえば「変数xには○○クラスのオブジェクトが格納されている」てな具合だな。 この講座では出来るだけ「インスタンス」という表現を使っていこうと思うが、時々「オブジェクト」と言っちゃうかも知れない。まあこのあたりは文脈で判断してほしい。 **B:** 文脈も何も、さーっぱりわかりません。 **A:** さっきのたい焼きの例で言うなら、たい焼きのクラスはレシピや調理器具の一式だ。そして焼きあがったたい焼きのひとつひとつがインスタンスだ。 **B:** ??? わかるような、わからないような…? **A:** まあ、さっきの引数や戻り値と一緒で、今はきちんとした意味がわからなくても使い方を覚えてもらえばいい。 多分使っているうちに慣れてくるよ。さて、80行目の説明に戻ろう。 .. literalinclude:: source/01-3.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 80-82 **A:** さっきText()関数の戻り値はVisionEgg.Text.Textクラスのインスタンスだと言ったよね。このオブジェクトには、どのような文字列を表示するのか、どの位置に表示するのか、何色で表示するのかといった情報が セットになって格納されている。オブジェクトに含まれているデータや機能を **メンバ** と言う。メンバにはデータ属性やメソッドなどの種類があって、実は今まで関数って言ってきたものの中には厳密に言うとメソッドにあたるものもあるんだけど、 その辺はいずれクラスについてもう少し踏み込んだ解説をしよう(注: :doc:`例題5-3 <05-3>` 、 :doc:`5-4 <05-4>` )。 メンバにアクセスするには演算子 . (ピリオド)を使う。VisionEgg.Text.Textクラスのオブジェクトはparametersというメンバを持っていて、このparametersにさらにさまざまなデータがまとめて格納されている。具体的には、80行目のText()関数で作成したtextobjの場合、 .. csv-table:: :delim: $ textobj.parameters.anchor$'center'という文字列が格納されている。 textobj.parameters.font_size$40という値が格納されている。 **A:** という具合になる。さて、ここまでくれば、やっっっと前回の例題の最後にB君が質問してくれた点に答えることが出来る。 **B:** …ぼく、何を聞いたんでしたっけ。 **A:** ずいぶん話があちこち行ったから忘れてるのも無理はないか。今回の例題のサンプルプログラムで言えば90行目だよ。 .. literalinclude:: source/01-3.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 90-101 **B:** ええと、textobjのparameters.textってメンバに何か代入してるんですよね。= の右はなんでしたっけ? **A:** お、わかってるじゃないか。= の右辺のsentenceListってのは19行目から定義されているリストで、画面に表示したい文が格納されている。 currentSentenceってのは、次にリストの何番目の文を表示するかを格納している変数だ。 **B:** ふむふむ。 **A:** この91行目は90行目から始まるwhileのループの中にあって、何度も繰り返される。そして101行目に注目してほしいのだが、同じwhileループの中でcurrentSenteceに1を加算している。 だから、whileのループが一周するたびにcurrentSenteceが1ずつ増加していって、その度にtextobj.parameters.textに新しい文が代入されるんだよ。 **B:** 101行目の currentSentence += 1 って1増加するって意味なんですか? **A:** ああ、説明してなかったか。v += 1ってのはv = v + 1の省略形だ。変数名がvとかみたいに短ければいいんだけど、 currentSentence = currentSentence + 1とかみたいに長くなると冗長だからね。同じようにv = v - wはv -= wとかいう具合に書ける。 **B:** なるほど、こうやって自動的に画面に表示する文を入れ替えていくんですね。 **A:** その通り。で、92行目から99行目は前回の例題でやった「画面表示しながらスペースキーが押されるのを待つ」というプログラムそのままだよね。 まとめると、91行目で次に表示する文を設定して、92行目から99行目でその文を表示しながらスペースキーが押されるのを待つ。100行目から101行目は次の文の表示に向けた準備だ。 **B:** あー、やっと何となくわかった気がします。 **A:** そうか、そりゃよかった。じゃあ少し休憩して残りの説明をするか。 **A:** さて、じゃあ気分を入れ替えて。プログラムの残りの部分、103行目から110行目は前回の例題と全く一緒だね。 90行目から110行目でスペースキーが押されたら次々と課題文を表示し、制限時間内にターゲット語を再生するというリーディングスパンテストの試行一回分になっている。 あとはこれを必要な回数だけ繰り返せばよいわけだ。前回、同じ事を繰り返すにはどうしたらいいって言ったか覚えてる? **B:** へっ? forかwhileのループを作って中に放り込む、でしたっけ。 **A:** 正解。サンプルプログラムでは88行目にforを置いて、110行目までをごっそりその中に入れている。このプログラムの残りのポイントはこのforだな。 for x in L はどういう動作をする制御構文だった? **B:** えーと、えーと、リスト変数Lに格納された値を順番にxに入れて繰り返す。 **A:** そうそう。88行目の場合、for maxSentence in setSizes:だから、リスト変数setSizesの値をmaxSentenceに次々と代入しながら 処理を繰り返す。じゃあsetSizesってのは何が入ってるかというと、それは76行目だな。[2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5]。 **B:** もしかして、リーディングスパンテストの課題文の数ですか? **A:** 正解。いいかげん作者も会話を考えるのが面倒臭くなってきてるな? **B:** ん? 何か言いましたか? **A:** いや、こっちの話。まず88行目でmaxSentenceに何文続けて提示するかが代入される。そして89行目でtrialという変数に次に表示する文が何文目であるかを格納する。もちろん最初は1文目なので1を代入する。 **B:** 何文目かを格納する変数なんだったらtrialって名前はおかしいんじゃないですか? **A:** うるさい。要はプログラムを書く人がわかれば何でもいいんだよ。英語をベースにした変数名を使っている中でいきなりローマ字を使ったりする人とかいるけど、 本人がそれでわかりやすいのなら、それでいいのさ。 **B:** はあ、そんなもんですか。 **A:** そんなもんさ。で、90行目から文を表示するためのループに入るんだけど、一回繰り返すたびにtrialを1増加させている。100行目だね。 で、90行目のwhileの繰り返し条件がtrial <= maxSentenceだから、maxSentenceが2ならtrialが2、maxSentenceが3ならtrialが3以下の間繰り返される。要するにそれぞれ2文、3文表示し終わるまで繰り返すわけだ。 **B:** うーん、なるほど。ところでtrialとcurrentSenteceってどちらも何文目かを表しているように思うんですけど、なんで別々の変数を用意してるんですかね? **A:** このあたりはちょっとトリッキーだからな。よく見てほしいんだが、trialは88行目のforループが繰り返される度に1に戻る。currentSentenceは88行目より前の86行目で0にセットされた後、ずっと増え続ける。 trialは個々の試行において何文目を表示しようとしているのかを示していて、currentSentenceは19行目からずらーっと定義されている課題文のうち何文目を表示するかを示しているんだ。表にしてみるとわかりやすいかな? .. csv-table:: :delim: $ forの繰り返し回数$maxSentenceの値$whileの繰り返し回数$trialの値$currentSentenceの値 1$2$1$1$0 2$2$1 2$2$1$1$2 2$2$3 3$2$1$1$4 2$2$5 4$2$1$1$6 2$2$7 5$3$1$1$8 2$2$9 3$3$10 6$3$1$1$11 2$2$12 3$3$13 7$3$1$1$14 2$2$15 3$3$16 8$3$1$1$17 2$2$18 3$3$19 9$4$1$1$20 2$2$21 3$3$22 4$4$23 以下省略 **B:** なるほど、こうすると具体的にイメージ出来ますね。 **A:** プログラムを書いたり読んだりしながらこういった変化をイメージ出来るかどうかが上達のカギかも知れないね。これで今回のサンプルプログラムも解説終了かな? **B:** じゃあさっそく動かしてみましょう。 .. figure:: img/01-3-03.png **B:** おおお、まともな文が表示されるだけですごくそれっぽい! あとターゲット語の下線が引けたら完成じゃないですか? **A:** まあ下線を引くってのは一苦労するところなんだが。それはさておき、これを実験として他人にやってもらうためにはもうちょっと気配りが欲しいところだな。 次回は今まで飛ばしてきたVisionEggの初期化の説明を兼ねつつ、気配りの話をするか。 **B:** Aさんから気配りなんて言葉を聞くとは驚きです。似合わね~ **A:** むっ。この企画でもかなりあちこちで気配りしてるんだぞ。とにかく今回はここまで。