.. title:: Pythonで心理実験 - 例題1-4 例題1-4:気配りをする ======================== **A:** さて、今回はこれまで説明を飛ばしてきた部分を少し補うぞ。それだけだと手抜きくさいのでちょっと気配りの話もしておく。 以下が今回のサンプルプログラムだ。 **B:** ずいぶん長いプログラムになってきましたねぇ。 **A:** でも89~97行目と122~130行目が加わったくらいだよ。しかも新しいテクニックは全く使っていない。 あと変数名を少し変更している。 **B:** なんで変えたんですか? **A:** ん? 何も考えずにサンプルを書いて、後で読み返したらこの変数名は良くないかなぁと思ったからな。 **B:** 行き当たりばったりに書いてるからそんな事になるんですよ。 **A:** ほっとけ。 + 行番号なしのソースファイルをダウンロード→ `01-4.py `_ .. literalinclude:: source/01-4.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** さて、1行目と2行目の説明から始めるかな。これらの行は#から始まっているからpythonのプログラムとして解釈されないのは最初に注意した通りだが、 でも実はプログラムの実行に大きな意味があるんだ。 **B:** プログラムじゃないのに大事ってよくわかりませんが。 **A:** まず1行目、これはUnix系のOSを使っている人で、pythonのスクリプトをコマンドシェルから直接実行する人にとっては大きな意味がある。 ここにpythonのインタプリタへのパスを書くんだ。Windowsで実行している限り、この1行目には大した意味はない。Windowsが拡張子.pyとpython.exeを関連付けてくれているからな。 **B:** じゃあ、書かなくても動くんですか? **A:** Windowsで使っている限りは、な。Linuxなどでは、コマンドシェルからxxx.pyという風にスクリプト名だけタイプして実行する時に1行目が重要な役割を果たすが、 Windowsでは拡張子.pyとpython.exeの関連付けがOSに登録されているので、1行目に関わらずpython.exeが起動する。 その辺を詳しく知りたかったらコマンドシェルについて自分で勉強しなさい。 **B:** えー。 **A:** えー、じゃない。ぱぱっと解説を済ませるつもりだったのに、もうこんなに行数を使ってしまった。続いて2行目。# -\*- coding:shift_jis -\*-という記述は、このファイルで使われている文字コードが指定されている。文字コードがわからないと、pythonのインタプリタは内部で例題1-2の最後の例のような文字化けを起こしてしまう。 Windowsで日本語を使っているなら、普通はmbcsかshift-jisというコードを指定しておけばいい。他にはiso-2022-jpとかeuc-jpとかutf-8があるな。これらのコードが標準となる 環境を使っている人なら、これらのコードが何であるかここで説明しなくてもわかるでしょ。だから説明はパス。 **B:** 前後についてる-\*-とかは削除しちゃいけないんですか? **A:** んー。厳密に言うと「1行目か2行目のコメントの内容がcoding[=:]\\s\\*([-\\w.]+)という正規表現にマッチすればどう書いても構わない」が、こう言われて何のことかさっぱりわからないのならきっちりこの通りに書いておけばいい。 **B:** ぶは、さっぱりわかりません。 **A:** 同じPCでプログラミングしているなら文字コードも毎回同じだろうから、コピペすればいいよ。文字コードを何も指定しない場合はASCIIという文字コードで書かれていると仮定される。 **B:** コピペでいいなら楽ちんですね。 **A:** まあutf-8とかを使わないと書けないプログラムを作るような日がいつか来るとしたら、その頃にはここに書かれてることくらい説明不要なくらい知識がついてることだろうし。 続いて4~10行目のimport。 **B:** 誰かが作ってくれたプログラムを読み込むんでしたよね。 **A:** お、よく覚えているな(例題1-1参照)。今回補足しておきたいのは、importの書き方に種類があること。今回のサンプルプログラムには3通りの書き方が出てきている。 #. import random #. from VisionEgg.Text import Text #. from VisionEgg import \* **B:** そういえばそうですね、全然気にしてなかったけど、何が違うんですか? **A:** まず最初のimport randomという書き方は、randomというモジュール中身をすべて読み込む。モジュールに含まれる関数foo()を使いたい時にはrandom.foo()という具合にモジュール名と関数名の間をピリオドでつなぐ。 **B:** ふむふむ。 **A:** 普通はこれでOKなんだが、モジュール名が長ったらしくなるといちいちパッケージ名を書くのが面倒臭くなる。 そういう時に便利なのが2番目のfrom VisionEgg.Text import Textという書き方だ。pythonでは複数の関連モジュールをひとつにまとめたものを **パッケージ** と言って、 実はVisionEggというのはパッケージなんだ。それで、このVisionEggパッケージの中にTextというモジュールが含まれていて、さらにその中にText()という関数が含まれている。正確に言うとこれは関数じゃ無いんだがそれはクラスの説明をしてからじゃないとわからんだろうなあ…。 ま、とにかくこのText()を使いたい場合、1番目の書き方だとimport VisionEgg.Textと書いて読み込んで、使う時にはVisionEgg.Text.Text()と書かないといけない。 **B:** うわ、Text.Textって何か変。 **A:** だろ? 2番目の書き方だと、「VsionEgg.TextからText」だけを読み込みますという宣言になる。以後、ただText()と書けばVisionEgg.Text.Text()を呼び出せる。サンプルプログラムの80行目、textobj = Text(...という部分がその例だな。 **B:** おお、便利だ。全部こうやって読んじゃえばいいじゃないですか。 **A:** 2つ問題がある。ひとつは複数のモジュールを読み込んで、どちらにもfoo()という関数が含まれていた場合、ただfoo()と書いてもどちらを呼び出そうとしているのかわからない。 いろんな人がいろんなモジュールを作っているから、偶然名前が重なる可能性は十分に考えられる。モジュール名が前についていれば区別できる。 **B:** あ、なるほど。 **A:** もうひとつの問題は、ひとつのモジュールから複数の関数を読み込みたい場合だ。モジュールには関数の他にもクラスや変数も定義されているからそれらを読み込みたい場合も該当するな。 2番目の方法だと、読み込みたいものの名前を全部書かないといけないんだ。せいぜい数個ならそれでも構わないんだが、20も30も読み込むとなると、名前を列挙するだけでも大変だ 。 **B:** むむむ。 **A:** もっとも、この問題は3番目の書き方をすれば回避できる。from VisionEgg import \*というのは、2番目と同じように解釈すればVisionEggから\*という関数を読み込みなさいという具合に読めるが、この\*という記号はモジュールに含まれるすべての関数や変数などに置き換えられる。 つまり、VisionEggに含まれるすべての関数や変数などを読み込むんだよ。 **B:** なんだ。じゃあ2番目の書き方は要らないじゃないですか。 **A:** いや、2番目の書き方だと必要なものだけをピンポイントに読み込むことが出来る。その方がコンピュータへの負担が少ないんだよ。 **B:** コンピュータの負担? **A:** そう。モジュールをimportするというのは、いわばそのモジュールに書かれている事をいつでも使えるように頭に叩き込んでおくことなんだよ。 B君は「試験の出題範囲は教科書の第5章」ってわかっている時に、第5章だけ覚えるのと教科書一冊まるごと覚えるののどっちが楽だ? **B:** そんなの第5章だけに決まってます。 **A:** だろ? なんでもかんでもimportしてしまうと、コンピュータはそれをいつでも使えるようにするために力を割かなきゃいけなくなるんだよ。 使う物がはっきり決まっている場合は、それだけを読み込む方が負担は小さい。 **B:** ふーん。なんだか人間くさいですねえ。 **A:** 人とコンピュータは全然違うものだけど、とても似ている側面もある。いろいろ考えてみると面白いものだよ。 さて、importの締めくくりにasというのを説明しておこう。import VisionEgg.Text as VTという具合に書くと、そのプログラムの中ではVTという名前でVisionEgg.Textを呼び出せる。 VisionEgg.Text.Text()はVT.Text()と書けるわけだな。 **B:** おお、これも便利ですね。 **A:** あんまりやりすぎると他人が読む時に分かりにくくなっちゃうかも知れないけどね。サンプルプログラムではモジュール名をきちんと覚えてもらう意味でもasは使わないことにしてる。OK? **B:** はーい。 **A:** よっしゃ。では続いてVisionEggの初期化の話をしておくぞ。これで全く説明されていない行はほとんどなくなるはず。 **B:** 全くなくなる、じゃないんですね。 **A:** 全部つぶしていくとちっとも先へ進まなくなるぞ。インターネット上とかで良質な解説が見つかりそうな話題はどんどんすっ飛ばしているから、わからないことがあれば自習するように。 **B:** へいへい。 **A:** 改めて説明すると、VisionEggというのはpygameやpyOpenGLといった画面に何かを描いたりキーボードやマウスを扱ったりするパッケージを 視覚実験で使いやすいようにしてくれるパッケージだ。VisionEggをimportすると、設定ファイルに書かれている内容が自動的に読み込まれていろいろ設定してくれる。 **B:** 設定ファイル? そんなの今までの説明で出てきましたっけ? **A:** 出てきてないな。というか、そんなファイルの存在を意識しなくても使えるのがVisionEggの便利なところなんだよ。まあ一応言っておくと普通はpythonのインストールディレクトリのLib/site-packages/VisionEgg/VisionEgg.cfgというファイルが設定ファイルだ。 こいつをテキストエディタで編集すると設定をいろいろ変えられるんだが、設定画面を表示させてそこで変更する方が初心者にも安心だろうな。 **B:** 設定画面ってどうやって出すんですか? **A:** 17行目のscreen = get_default_screen()はVisionEgg.Coreからimportした関数で、VisionEgg.cfgの設定に従って描画用のウィンドウを作成する。 設定画面を表示するように設定されていれば、この関数を実行した時に設定画面が自動的に表示されるんだよ。 .. figure:: img/01-4-01.png **B:** ああ、いつもの画面じゃないですか。何も考えずにOKしてました。 **A:** 一時的に設定を変えたい場合はここで変更してOKする。変更後の設定を標準設定にしたい場合はOKする前に隣の"Save current settings to config file"をクリックしておく。 そうするとその時設定画面に表示されている内容が設定ファイルに保存され、次回からの標準設定になる。 **B:** なるほど。ところで「設定画面を表示させるように設定されていれば」って言ってましたけど、その設定はどうするんですか? **A:** プログラム中でget_default_screen()を実行する直前にVisionEgg.config.VISIONEGG_GUI_INITという変数に1を代入しておけばいい。 もし表示させたくない場合はVisionEgg.config.VISIONEGG_GUI_INIT = 0とする。サンプルプログラムでは15行目で1を代入してるから、設定画面が表示されるわけだ。 **B:** ふむふむ。 **A:** 設定項目は全部英語で書いてるけど、これくらいは自分で調べてみるように。いずれ一覧表を作ろうとは思うけど。 あと13行目と14行目でVisionEgg.start_default_logging()とVisionEgg.watch_exceptions()という関数を呼び出しているが、これはプログラムの実行状態について記録を出力するための準備だ。 たぶん今までのサンプルプログラムを動かしてみた人は、プログラムと同じディレクトリにVisionEgg.logというファイルが出来ているのに気付いているはずだ。 ここにはプログラムの実行に問題がなかったかどうかチェックするための有用な情報がいろいろ出力されている。 こいつについてはいずれきちんと説明した方がいいんだろうけど、かなりハードルが高いのでパス。 **B:** また積み残しですか。 **A:** 間違ってないプログラムの書き方もきちんと説明が終わってないのに、間違ったプログラムを書いた時の出力を説明しても仕方ないだろ。 さて、変数screenに格納されているget_default_screen()の戻り値はVisionEgg.Core.Screenクラスのインスタンスだ。こいつをあれこれ操作するとスクリーンに働きかける事が出来る。 例えば95行目などに出てくるscreen.clear()のclear()はスクリーンを背景色で塗りつぶす関数だ。 **B:** ふむふむ…って、サンプルプログラムではclear()しか使われていないような。 **A:** そうだね。実際の描画はVisionEgg.Core.Viewportクラスから行うから、あまりプログラムの表面には出てこないな。 **B:** また新しいクラスが…。 **A:** ちょっとややこしくなってきたから、ここでVisionEggの仕組みを整理しておこうか。 .. figure:: img/01-4-02.png **B:** うわ、さらにVisionEgg.Core.Stimulusとかいうのまで増えてる。 **A:** まず、VisionEgg.Core.Screenのスクリーンは実際のPCの画面とはあくまで別のものなので、VisionEgg.Core.Screenのインスタンスの事を「スクリーン」と呼ぶことにするよ。 実際のPCの画面は文字通り「実際の画面」と呼ぶことにしよう。 **B:** そのまんまですね。なんでそんなややこしい事になってるんですか? **A:** そうだなぁ。第一にダブルバッファというテクニックを実現するには実際の画面と描画を行うスクリーンが分離していた方がいいというのがある。 これは後で説明するよ。他にはVisionEgg.Core.Screenが作るスクリーンは、設定によって画面全体と一致したり、Windowsの他のアプリケーションと 同様のウィンドウのひとつだったりするというのがあるかな。試しにサンプルプログラムの設定画面でFullscreenのチェックボックスをONにしたりOFFにしたりしてみるといいよ。 **B:** ダブルバッファ? 後で説明してくれるんですね? **A:** VisionEgg.Core.ViewportとVisionEgg.Core.Stimulusの説明が済んだらね。 さて、次は図の一番下のVisionEgg.Core.Stimulusを説明するか。こいつは文字とか、○とか□とか、そういった画面に描きたい刺激を表すクラスだ。 今回のサンプルプログラムでは、文字列をひとつ表示したいだけなので、VisionEgg.Text.Textクラスのインスタンスをひとつ生成している。80行目だね。 **B:** へ? なんでVisionEgg.Core.Stimulusクラスの話をしているのにVisionEgg.Text.Textクラスが出てくるんですか。 **A:** む。やぶへびだったかな。その質問に正確に答えるためにはクラスの **継承(インヘリタンス:例題5-3)** を理解してもらわないといけないんだが、 こういう話はきちんとした教科書で読んだ方がいい。ここではVisionEgg.Text.TextはVisionEgg.Core.Stimulusとしても使えるとだけ言っておくよ。きちんと理解したければC++やJAVAなどの **オブジェクト指向** のプログラミング言語の教科書を読んできちんと勉強する事を勧める。とりあえず動けばいいということだったら、要するにVisionEggパッケージが提供している ○やら□やら文字やらを描くためのクラスは全部ここで言うVisionEgg.Core.Stimulusの仲間なんだと思っておけばいい。 **B:** …深く考えないことにします。 **A:** OK。続いてVisionEgg.Core.Viewportクラスは、VisionEgg.Core.Stimulusクラスのインスタンスをスクリーンに描画するためのクラスだ。 なんでViewportって名前かというと、VisionEggでは3Dグラフィックスを扱う事も出来るので、個々の刺激をどういう風にスクリーンに投影するかを決められるんだよね。 Viewportは複数用意して「被験者へのメッセージ文字列は平面的に投影するけど、刺激は3D画像として投影する」なんてことも出来る。まあそんな凝った事をしなければ、Viewportのインスタンスはひとつ作れば十分だ。 今回のサンプルプログラムでは84行目でViewportのインスタンスを作っているね。 .. literalinclude:: source/01-3.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 84 **B:** viewport=Viewportとか、screen=screenとか気持ち悪いですね…。 **A:** =の左は変数名、右は関数名だ。まあ変数の名前の付け方は好みの分かれるところだな。念のため言っておくがViewport()関数は正式にはVisionEgg.Core.Viewport()だ。 from VisionEgg.Core import \*で読み込んでいるから、単にViewport()と書ける。 **B:** あ、それはわかります。 **A:** よしよし。Viewport()関数にはscreenという引数が必須だ。ここにVisionEgg.Core.Screenクラスのインスタンスを指定する。この引数で指定されたスクリーンにVisioEgg.Core.Viewportクラスは描画を行う。 **B:** 17行目でscreenという変数にスクリーンを格納しているから、ここでscreen=screenとなるわけですね。 **A:** そう。17行目でget_default_screen()の戻り値をhogehogeという変数に格納していたら、ここではViewport(screen=hogehoge)と書くわけだ。 **B:** hogehogeって…。 **A:** Viewport()関数はスクリーンさえ指定すれば呼び出せるが、実際にはstimuliという引数も指定してやらないと不便だ。 stimuliにはVisionEgg.Core.Stimulusクラスのインスタンスを並べたリストを指定する。今回のサンプルプログラム84行目では[textobj]だな。文字列ひとつだけしか表示するものがないから、 要素がひとつだけのリストになっている。 **B:** リストって要素がひとつだけでもいいんですか? **A:** もちろん。それどころか空っぽでもいい。空っぽのリストは[]と書く。空っぽのリストは結構便利に使えるのだが、それはまたいずれ。 ちなみに複数の刺激が含まれる場合、このリストに並べられた順番に描画される。図形を重ね合わせたりするときには重要なので覚えておいてほしい。 **B:** 絶対忘れてる自信があります。 **A:** こらこら。でもまあ忘れてたとしても、実際に複数の刺激を画面に描かないといけないような実験を作らないといけなくなると、すぐに思い出すよ。 まったく聞いた事がないのと、一度でも聞いた事があるのでは全然違う。 **B:** そんなもんですかね。 **A:** そんなもんさ。って言うか、そう信じてこの解説をしている。ここに書かれている事を一度に全部理解するのは無理だろうけど、一度読んでおけばきっと後で役に立つ。 **B:** なんかいつになく真剣な雰囲気。 **A:** 私はいつでも真剣だが。居眠りも鼻毛の手入れも全力投球だ。 **B:** ああ、一気に真剣な雰囲気が…。 **A:** 鼻毛は置いといて、投影方法は特に指定しなければ普通に平面として投影される。指定する時はprojectionという引数を指定する。まあこの辺は実際に必要に迫られた時に調べたらいい。 さて、Viewportのインスタンスがあれば、刺激を描画するのは簡単だ。Viewportのdraw()を呼べば全て良きに計らってくれる。例えば96行目だね。 **B:** なんだかよくわからない部分もありましたが、要するにviewport.draw()とすれば描画出来るんですね? **A:** Viewportのインスタンスが変数viewportに格納されていれば、ね。 **B:** ふぅむ。インスタンスがどうとかいうのがまだ慣れないなあ。 **A:** 慣れないうちは、get_default_screen()やViewport()の戻り値を格納する変数を自分なりに決めておいて、とにかくこう書くんだってのを覚えればいい。 そのうち余裕が出てきたら、ここを書き換えたらどうなるんだろうって感じで少しずついじってみればいいんだよ。頭で考えているより自分でいじってみた方がはるかに身につきやすい。 **B:** はーい。 **A:** あともう一息だ。上の図をよく見てほしいんだけど、VisionEgg.Core.Screenは長方形が2つ重ねてあるだろ。 **B:** はい。これ、意味あるんですか? **A:** もちろん。スクリーンに描画している最中に、描きかけの絵が画面に表示されちゃったらまずいだろ? **B:** へ? そんな事起こるんですか? **A:** スクリーンが一枚しかなければ、そういうこともある。設定にもよるが、PCは通常1秒間に50~100回くらいの頻度で画面を書き直している。 この位の頻度で絵を次々と描きかえると、人間の眼には滑らかに動いているように見える。 **B:** 映画とかと同じ原理ですよね。 **A:** そう。スクリーンを描画している最中に実際のスクリーンの描きかえが起こると、描画中の中途半端な状態のスクリーンが表示されてしまう。 それではまずいので、VisionEgg.Core.Screenは内部に前後二枚のスクリーンを持っている。描画は常に後ろのスクリーンに行って、描き終わったら前のスクリーンと入れ替えるんだ。実際の画面には前のスクリーンが表示される。 そしてまた後ろのスクリーンに描画したら、前のスクリーンと入れ替える。こうすれば、描きかけのスクリーンが見えてしまう事はない。 **B:** なんか忙しいですね。 **A:** この入れ替えを行うのが97行目のswap_buffers()だ。正式にはVisionEgg.Core.swap_buffers()ね。 **B:** なるほど、だからviewport.draw()で描画した後にswap_buffers()が来るんですね。 **A:** その通り。このあたりはお決まりのパターンなので、まるごと覚えてしまえばいい。 このように、二枚のスクリーンを使って描画する仕組みをダブルバッファといい、ゲームプログラミングなどで非常によく用いられているんだ。 **B:** なんでダブル「スクリーン」じゃなくてダブル「バッファ」って言うんですか? **A:** うーん。なんでと言われると、そう呼ぶ習慣だからとしか言えないな。一般に、キーボードやディスクなどからデータを読み取ったりする時などに、一時的にデータを保存するメモリなどの記憶領域をバッファと言うんだ。 この例の場合、VisionEgg.Core.Screenが実際の画面へデータを転送する際の一時保存スペースになっているからバッファと呼ばれてるんじゃないかな。 VisionEggのベースになっているpygameパッケージではサーフェスと呼んだりするので、インターネット上で検索したりして自習する時には注意してほしい。 あー。疲れたからちょっとここで休憩するか。 *------------------------------ 休憩中 ------------------------------* **A:** さて、そろそろ再開するか。後なにを説明してないかな…。おっとっと、この部分は説明してないな。 .. literalinclude:: source/01-4.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 77-78 **A:** len()は引数に指定されたリストの長さを返す関数。range()は自然数ひとつを引数にした場合、0から始まる長さが引数個の整数のリストを返す関数だ。 例えばrange(5)なら[0,1,2,3,4]というリストが得られる。len(sentenceList)でsentenceListの長さが得られるんだから、range(len(sentenceList))で0から始まって長さがsentenceListと同じの整数のリストが得られるわけだ。 **B:** む、難しいですね。関数は重ねて使えるんですか? **A:** あぁ、そういえばまだちゃんと説明してなかったか。関数の引数に関数が入ってる場合、引数になってる関数がまず評価されて、その戻り値が引数となる。よく使う方法なので慣れてほしい。 **B:** がんばります。 **A:** 続いて78行目だな。10行目で読み込んでいるrandomモジュールの関数shuffle()を呼び出している。この関数はリストを引数として受け取り、そのリストの要素の順番をランダムに並べ替える。 前の行で作った整数が順番に並んでいるリストを無茶苦茶に並べ替えたわけだな。 **B:** なんでそんな事をするんですか? **A:** これは次の例題で活きてくる。楽しみにしていたまえ。 **B:** じー(疑惑のまなざし) **A:** ま、まあ正直に言えば、このサンプルプログラムの中で使うつもりだったけど忘れてた。 **B:** やっぱり。これだから行き当たりばったりは。 **A:** まあ許せ。あと細かいポイントだけど、115行目で再生の待ち時間をmaxSentence \* 5.0にしている。つまり再生時間が課題文の数×5秒になるようにしているわけだ。 **B:** \*って×なんですか? **A:** そのとおり。aとbの積はa \* bと書く。さて、やっとこれで今回のサンプルの説明もおしまい。次回はターゲット語に下線を引いて、いよいよ完成させるぞ。 **B:** …。 **A:** ん? どうした? **B:** あのー。今回のタイトルは「気配りをする」だったような気がするんですが、いったい今回の内容のどこが気配りだったんでしょう。 **A:** ああ、まるっきり忘れてた。今回のサンプルプログラムを実行してもらったわかるけど、課題文が表示される前に「スペースキーを押して開始」っていう教示を表示して、 スペースキーを押したら試行が始まるようになっている。こうしないと、プログラムが起動したと思ったらいきなり第一試行が始ったり、再生時間が終了したらいきなり次の試行が始ったりするからね。 前回のプログラムでも、最低限の刺激制御は出来ていたが、素人さんを被験者として呼んできて課題をやってもらうためには、こういう気配りが必要なんだよ。最後の試行が終了した後に 「終了しました。ご協力ありがとうございました。」っていう文を表示するようにしているのもその一環だね。 **B:** なんだ、前回ずいぶん見得を切った割には大したことない気配りですね。 **A:** 何を言うか。でもプログラムの技術的には新しいところは全然なかったんで、説明すんのをすっかり忘れてたよ。 **B:** とても気配りが出来る人のセリフとは思えません。 **A:** むむぅ。今回のところは負けを認めざるを得ないかな。次回はいよいよプログラムを完成させるぞ。