7. キーボードで刺激を調整しよう―ミューラー・リヤー錯視

7.1. この章の実験の概要

この章では有名な錯視のひとつであるミューラー・リヤー錯視を題材として、調整法の手続きをBuilderで実現する方法を解説します。この章まで進んできた皆さんはそろそろ教示画面の作成は各自でできるでしょうから、重要な部分だけを取り上げましょう。 図7.1 に実験に用いる刺激を示します。スクリーン上に左右に並んでテスト刺激とプローブが表示されます。テスト刺激はミューラー・リヤー錯視図形で、水平線(以下主線と呼びます)の長さは0.2、矢羽の長さは0.05です。主線と矢羽のなす角度(以下夾角と呼びます)として0度から30度間隔で150度まで、6種類の図形を用います。0度の時は矢羽と主線がぴったり重なって長さ0.2の水平線だけに見える点に注意してください。プローブは水平な線分で、キーボードのカーソルキーの左右を使って長さを調節することができます。実験参加者は主線とプローブの長さが同じに見えるようにプローブの長さを調整して、スペースキーを押して報告します。この時のプローブの長さと主線の長さの差で錯視量を評価しようというのが本実験の狙いです。試行開始時のプローブの長さが毎回同じだと、参加者が何回キーを押せば主線とプローブが同じ長さになるかを学習してしまう恐れがありますので、試行開始時のプローブの長さは170、190、210、230pixの中から無作為に選択します。テスト刺激、プローブともスクリーンの中央の高さで、水平方向の中心がスクリーン中央から0.2離れた位置に提示されるものとします。

_images/muller-lyer.png

図7.1 実験に用いる刺激。テスト刺激の水平線(主線)の長さを0.2、矢羽の長さを0.05で固定し、夾角を0度から150度まで変化させます。参加者は主線の長さとプローブの長さが等しく見えるようにプローブの長さを調整します。試行開始時のプローブの長さは170、190、210、230pixのいずれかです。

図7.2 に実験手続きを示します。教示画面は省略しましたので、いきなり最初の試行から始まります。各試行の最初に0.5秒間空白のスクリーンを提示した後、テスト刺激とプローブを提示します。テスト刺激は半数の試行で右側に、残り半数の試行で左側に提示され、順番は無作為に決定します。実験参加者がカーソルキーの右を押したら刺激の長さを5pix長く、左を押したら5pix短くします。参加者がスペース―を押したら試行は終了です。テスト刺激(6種類)×テスト刺激の位置(2種類)×プローブの初期長さ(4種類)=48通りの条件を3試行ずつ、合計144試行を行ったら実験は終了です。

_images/muller-lyer-procedure.png

図7.2 実験の流れ。

実験手続きは単純なので、前章までに解説したテクニックで十分実現できるはずです。問題は、「カーソルキーで刺激を調節してスペースキーで試行を終了する」という手続きをどうやってBuilderで実現するかです。Keyboardコンポーネントでは、 [Routineを終了] プロパティを用いてキーが押されたときにルーチンを終了させるか否かを指定できます。しかし、特定のキーが押されたときだけ終了させるといった指定はできません。 第6章 で学んだif文を使うとキーに応じた処理を実行することが出来ます。

それでは実験を作成していきましょう。以下の解説では、Builderで実験を新規作成し、以下の作業を行ってexp07a.psyexpの名前で保存したものとします。 この章から既出のコンポーネントのプロパティについてはタブを省略しますのでご注意ください

  • 準備作業
    • この章の実験のためのフォルダを作成して、その中にexp07.psyexpという名前で新しい実験を保存する。

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

    • 「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Polygonコンポーネントを1つ配置し、 [名前]testline として以下のように設定する。
      • PsychoPy 2024以降の場合
        • 「基本」タブの [開始] を「時刻 (秒)」の 0.5 とし [終了] を空白にして [形状] を直線にする。

        • 「レイアウト」タブの [サイズ [w, h] $](0.2, 0) とする。 [位置 [x, y] $](testPos, 0) にして、「繰り返し毎に更新」に設定する。

        • 「外観」タブの [枠線の色]white にする(標準でwhiteのはずである)。 [枠線の幅]2 にする。

      • PsychoPy 2024より前のバージョンの場合
        • 「基本」タブの [開始] を「時刻 (秒)」の 0.5 とし [終了] を空白にして [形状] を長方形にする。

        • スクリーンの解像度から、数pix程度の高さになる値をheight単位で計算する。この値をXとして、「レイアウト」タブの [サイズ [w, h] $](0.2, X) とする。具体例を挙げると、1920×1080のスクリーンであればheight単位で1.0 = 1080pixであり、X=0.002なら1080×0.002=2.16pixとなりほぼ2pixとなるので、 [サイズ [w, h] $](0.2, 0.002) とする。

        • 「レイアウト」タブの [位置 [x, y] $](testPos, 0) にして、「繰り返し毎に更新」に設定する。

        • 「外観」タブの [塗りつぶしの色]white にする(標準でwhiteのはずである)。 [枠線の色]None にする。

    • testlineをコピーして probe の名前で貼り付けて以下のように設定する。
      • 「レイアウト」タブの [サイズ [w, h] $](probeLen, 0) にして、「フレーム毎に更新する」を設定する。 [位置 [x, y] $](-testPos, 0) にする。

    • testlineをコピーして arrowTL の名前で貼り付けて以下のように設定する(TL=Top Left)。
      • 「レイアウト」タブの [サイズ [w, h] $](0.05, 0) とする。 [位置 [x, y] $](testPos-0.1, 0) にして [位置揃え] を中央左にする。 [回転角度]-angle にして「繰り返し毎に更新」に設定する(angleにマイナス記号がついていることに注意)。

    • arrowTLをコピーして arrowBL の名前で貼り付けて以下のように設定する(BL=Bottom Left)。
      • 「レイアウト」タブの [回転角度]angle にする(angleにマイナス記号がついてない注意)。

    • arrowBLをコピーして arrowTR の名前で貼り付けて以下のように設定する(TR=Top Right)。
      • 「レイアウト」タブの [位置 [x, y] $](testPos+0.1, 0) にして [位置揃え] を中央右にする。

    • arrowTRをコピーして arrowBR の名前で貼り付けて以下のように設定する(BR=Bottom Right)。
      • 「レイアウト」タブの [回転角度]-angle にする(angleにマイナス記号がついていることに注意)。

    • ここまでの作業でtestline, probe, arrowTL, arrowBL, arrowTR, arrorBRの6個のPolygonコンポーネントが配置されることになる。Builderのメニューの「実験」の「実験内を検索...」からangleとtestPosを検索して、 図7.3 のように設定されていることを確認する。

    • Codeコンポーネントを1つ配置して、以下の設定を行う。
      • [Routine開始時]probeLen = initProbeLen と入力する。

      • trialルーチン内での順序を一番上にする。

    • Keyboardコンポーネントを1つ配置して、以下の設定を行う。
      • [名前]key_response にする。

      • [開始] を「時刻 (秒)」の 0.5 とし、 [終了] を空白にする。

      • [Routineを終了] のチェックを外す。

      • [検出するキー $]'left','right','space' にする。

      • 「データ」タブの [記録] を「なし」にする。

      • trialルーチン内での順序を一番上にする。結果として Keyboardルーチン、Codeコンポーネント、その他のPolygonコンポーネントの順に並んでいる ことを確認すること。

  • trialsループ(作成する)

    • [Loopの種類] をfullRandomにする。

    • [繰り返し回数 $]3 にする。

    • [条件]exp07cnd.xlsx と入力する。

  • exp07cnd.xlsx(条件ファイル)

    • testPos、angle、initProbeLenの3パラメータを設定する。

    • 実験手続の内容を満たすように、2種類のtestPos(-0.2、0.2)、6種類のangle(0、30、60、90、120、150)、4種類のinitProbeLen(0.17、0.19、0.21、0.23)の組み合わせを入力する。2×6×4=48行の条件ファイルとなる(1行目のパラメータ名を除く)。testPosはテスト刺激の中心のX座標を表しており、プローブ刺激の位置はtestPosの符号を反転すれば得られるのでパラメータとして用意する必要はない。

_images/check-parameters.png

図7.3 Polygonコンポーネントの設定が複雑で入力ミスしやすいが、「実験内を検索...」でangleやtestPosを検索すると確認しやすい。

教示などを省略したので以上で単純なフローの実験となりました。また、古いバージョンのPsychoPyではPolygonコンポーネントの [位置 [x, y] $] は図形の中心を指していなければいけなかったので複雑な計算が必要だったのですが、 [位置揃え] が選択できるようになって非常に楽になりました。 ただし、PsychoPy 2024より前のバージョンでは線分を描画する際の [位置揃え] の処理がおかしいので非常に細長い長方形を使って線分を描画しています。

現状のままではtrialコンポーネントに置いたKeyboardコンポーネントは何の機能も持っていませんが、これからCodeコンポーネントを使ってプローブ長の調整や決定の処理を組み込んでいきましょう。

7.2. Codeコンポーネントを使って刺激のパラメータとルーチンの終了を制御しよう

Codeコンポーネントでは、押されたキーを判別してカーソルキーの右であればプローブの長さを5pix長く、左であれば5pix短くし、スペースキーであればルーチンを終了します。第6章 を読んだ皆さんであれば「if文を使うとよい」ということはすぐにわかると思いますが、今回は処理が複雑です。第6章 では

  1. 押されたキーの名前が変数correctAnsの値と一致している

  2. 押されたキーの名前が変数correctAnsの値と一致していない

の2通りに分岐しましたが、今回は

  1. 押されたキーの名前が'left'である

  2. 押されたキーの名前が'right'である

  3. 押されたキーの名前が'space'である

  4. 押されたキーの名前がいずれにも一致しない(つまりいずれも押されていない)

の4通りに分岐しなければいけません。このように3通り以上の分岐を処理するために、Pythonのif文ではelifという語を使うことができます。elifを使ったif文の書式は以下の通りです。n個の式を連ねて書くことができます。最初はif、最後はelseで、それ以外はすべてelifでなければいけません。

if 式1:
    式1が真の時の処理
elif 式2:
    式1が偽で式2が真の時の処理
(中略)
elif 式n:
    式1から式n-1が偽で式nが真の時の処理
else:
    式1から式nがすべて偽であった時の処理

最初に真になった式に対応する処理だけが実行されますので、例えば式1が偽で式2が真であれば、「式1が偽で式2が真の時の処理」だけが実行されます。その後に続く式3、式4…が真であっても、対応する処理は一切実行されません。

押されたキーの名前が変数keyに入っているとすれば、今回の処理は以下のように書けます。上記の書式でn=3の場合に該当します。

if key == 'left':
    プローブの長さから 0.005 を引く
elif key == 'right':
    プローブの長さに 0.005 を足す
elif key == 'space':
    ルーチンを終了する処理

最後のelseはどこへいった?と思われるかもしれませんが、elseに相当するのは「カーソルキーの左、右、スペースキーのいずれも押されていない場合」で、今回の実験ではこの場合なにもコードを実行する必要がありません。 else節で何もすることがない場合、else節は省略されます。

プローブの長さを変更する処理については、プローブ刺激に対応するPolygonコンポーネントprobeの [サイズ [w, h] $] に(probeLen, 0)と書いているのですから、変数probeLenの値を増減すればいいだけです。0.005を加えるには probeLen += 0.005 、0.005を引くには probeLen -= 0.005 です。

ルーチンを終了させる処理については、まだ解説していないBuilderの機能を使用する必要があります。Builderには、1フレーム描画する毎に1回、continueRoutineという変数を確認し、値がFalseであれば直ちにルーチンを終了するという機能があります。if文を用いて、ルーチンを終了させたい条件を満たした時にcontinueRoutine=Falseという文を実行すれば、ルーチンを終了させることができるわけです。

以上を踏まえて、if文の処理内容を記述すると以下のようになります。

if key == 'left':
    probeLen -= 0.005
elif key == 'right':
    probeLen += 0.005
elif key == 'space':
    continueRoutine = False

残るは押されているキー名の取得です。第6章 の内容を踏まえると、Keyboradコンポーネントの [名前] がkey_responseですから、key_responseのデータ属性keysを利用すればよいだけのような気がします。しかし、 表6.3 をよく読みなおしてほしいのですが、keysには押されたキー名が格納されているのではなく、そのルーチンでKeyboardコンポーネントが保存するキー名が格納されています。今回の実験ではkey_responseの [記録] を「なし」に設定しているのですから、データ属性keysにはキー名が格納されません。

ではどうにすれば良いかといいますと、Builderが自動的に用意する変数theseKeysを利用します。theseKeysは最後に実行したKeyboardコンポーネントの結果を格納している変数です。 [検出するキー $] に記述されたキーがいずれも押されていなければ空の(要素数ゼロの)リスト、押されていたキーがあれば、それらの名前をすべて列挙したリストが格納されています。theseKeysと複数形になっているのは、同時に複数のキーが押される場合があるからです。キーの同時押しについての詳細は「 7.6.1:複数キーの同時押しの検出について 」を参考にしてください。

theseKeysを利用するにあたって、ポイントが2つあります。まず、theseKeysは「最後に実行したKeyboardコンポーネントの結果」を格納するものですから、ルーチンが開始された直後にはまだtheseKeysという変数自体が存在しません。そこで、Codeコンポーネントを使って [Routine開始時] にtheseKeysを用意しておく必要があります。まだキーは押されていないのですから、空のリストを代入しておけばよいでしょう。以下のコードをtrialsルーチンの [Routine開始時] に追加しておいてください(すでに probeLen = initProbeLen という文が入力されているはずです)。

theseKeys = []

第2のポイントは、押されたキーの確認方法です。theseKeysの中身はリストですから theseKeys == 'left' という具合に直接文字列と比較するのではなく、theseKeysに格納されたリストの中に'left'という文字列があるかを検査しなければいけません。Pythonにはこの用途にうってつけの in という演算子があります。in演算子はx in aという形で使用して、aの中にxがあればTrue、なければFalseとなります。theseKeysとin演算子を用いてif文を書くと以下の通りになります。

if 'left' in theseKeys:
    probeLen -= 0.005
elif 'right' in theseKeys:
    probeLen += 0.005
elif 'space' in theseKeys:
    continueRoutine = False

これでif文は完成しましたが、問題はこのif文をCodeコンポーネントのどこへ入力すれば良いのかという点です。ここで覚えておいてほしいのが、「あるルーチンを実行している最中に何かをするのであれば、コードを入力する欄は [フレーム毎] でなければいけない」ということです。今回のコードはルーチンの実行中に押されたキーを判別して処理を振り分けるのですから、 [フレーム毎] に入力しなければいけません。タイプミスしないように気を付けて [フレーム毎] にこのif文を入力してください。leftやright、spaceの前後のシングルクォーテーションや、if、elifが出て来る最後のコロン、if、elif以外の行は行頭にスペースが必要な点などが間違えやすいポイントです。

以上でCodeコンポーネントによる刺激パラメータとルーチン終了の制御ができるようになりました。作業内容をexp07.psyexpに保存して実行してみましょう。カーソルキーの左右を押すとプローブの長さが変化し、スペースキーを押すと次の試行へ進むはずです。残念ながらカーソルキーを押しっぱなしにしていても連続的に長さは変化しませんので、何度もカチカチとボタンを押して長さを調節する必要があります。

一見、これで 図7.1 および 図7.2 に示した実験が完成したように思えます。しかし、実験を最後まで実行するかEscキーで中断してtrial-by-trial記録ファイルを確認してみるとわかりますが、probeLenの列に試行開始時の値が出力されていて、参加者が調整後のprobeLenの値が記録ファイルのどこにも出力されていません。Builderは、Codeコンポーネントで独自に使用した変数や、ルーチン開始後に変更されたパラメータの値を記録ファイルには出力しないのです。自動で出力してくれたらいいのにと思われるかもしれませんが、本当にそんなことをしたら記録ファイルが分析に不要な変数だらけで大変なことになりかねません。probeLenの値を出力させるためには以下の2つの方法があります。。

  1. probeLenが保存すべき変数であることをBuilderに教えるためにVariableコンポーネントというコンポーネントを使用する。

  2. Codeコンポーネントを使ってprobeLenを実験記録ファイルに出力するコードを追加する。

筆者のお勧めは2のCodeコンポーネントを使う方法です。次節でこちらの方法を解説します。 1の方法については「 7.6.4:Variableコンポーネントによる変数の値の出力 」を参考にしてください。

チェックリスト
  • 3通り以上の分岐を処理させるif文を書くことができる。

  • リストの中にある要素が含まれているか否かで処理を分岐させることができる。

  • Codeコンポーネントからルーチンを終了させることができる。

7.3. Codeコンポーネント使って独自の変数の値を記録ファイルに出力しよう

Builderが実験記録ファイルに出力する変数はどのように管理されているのでしょうか。ここで思い出していただきたいのは、条件ファイルで定義した変数はすべて実験記録ファイルに出力されるという点です。条件ファイルはループのプロパティ設定ウィンドウで指定するのですから、実験記録ファイルに出力される値を管理しているのはループであるはずです。

では、Builderにおけるループの「実体」とは何でしょうか。その答えは [Loopの種類] によって異なるのですが、この本で使用しているrandom、sequential、fullRandomの場合はいずれもpsychopy.data.TrialHandler (以下TrialHandler)というクラスのインスタンスです。TrialHandlerはその名が示す通り、試行を制御するためのクラスです。繰り返しの度にパラメータの値を更新したり、パラメータや反応を実験記録ファイルに保存したりするBuilderの機能はこのクラスによって実現されています。

表7.1 にTrailHandlerの主なデータ属性を示します。これらのデータ属性を利用すると、画面上に「現在第n試行」、「残りn試行」といったメッセージを提示することができます。例えばtrialsという名前を付けたループに対応するTrailHandlerのインスタンスはCoder上では変数trialsに格納されているので、trials.thisNと書くと現在trialsループで何回繰り返しを終えているかが得られます。 表7.1 に書かれているように1回目の繰り返しではthisN=0なので、1を加えてtrials.thisN+1とすれば「第n試行」のnに当てはめる数値が得られます。

表7.1 TrialHandlerの主なデータ属性

データ属性

概要

thisIndex

条件ファイルの何行目の条件がこのループで用いられているかを示します。ただしパラメータ名の行は行数に数えず、1行目を0と数えます。Trial-by-trial記録ファイルのthisIndexと同じです。

nTotal

このループで実行される繰り返しの総数。

nRemaining

このループで実行される残りの繰り返し回数。1回目の繰り返しの実行中にnTotal-1で、繰り返しの度に1ずつ減少します。

thisN

このループで実行済みの繰り返し回数。1回目の繰り返しの時に0で、繰り返しの度に1ずつ増加します。Trial-by-trial記録ファイルのthisNと同じです。

thisRepN

Trial-by-trial記録ファイルのthisRepNと同じです。

thisTrialN

Trial-by-trial記録ファイルのthisTrialNと同じです。

ちょっと脱線なのですが、第5章 の復習がてら実際に「第○試行」とTextコンポーネントを使って提示する場合の注意点を述べておきましょう。trials.thisN+1は数値ですので、そのまま文字列と結合することができません。ですからTextコンポーネントの [文字列] に$'第' + (trials.thisN+1) + '試行'と書くと当然エラーになります。ここで 第5章 において実験情報ダイアログからターゲットの大きさや偏心度の値を得たときの処理を思い出してください。あの時は実験情報ダイアログの値は文字列で、刺激のパラメータとして利用する時には数値に変換しないといけないのでした。今回はこの逆で、数値であるtrials.thisN+1を文字列にしないといけません。第5章 で紹介したように、この変換には関数str( )を用います。$'第' + str( trials.thisN+1 ) + '試行'とすれば、TextコンポーネントのTextの値として使用できます。

話を元に戻しましょう。TrialHandlerクラスのインスタンスに実験記録ファイルへ出力する変数を追加するには、TrialHandlerのメソッドを使用する必要があります。 表7.2 に主なTrialHandlerのメソッドを示します。メソッドの引数の書き方がPsychoPyのヘルプドキュメントと異なりますが、この点について解説するにはPythonの文法に関する詳しい解説が必要です。興味がある方は「 7.6.2:メソッドの第一引数について(上級) 」をご覧ください。 表7.2 の最初に挙げられているaddData( )が今回の目的を達成するためのメソッドです。第6章 でも少しだけ例が出ていたのですが、メソッドはデータ属性と同様にインスタンスが格納されている変数とメソッド名をドット演算子で連結して呼び出します。trialsループに出力する変数を追加する場合はtrials.addData( )と書くわけです。 表7.2 に書かれている通り、addData( )には引数が必要です。第1引数には、trial-by-trial記録ファイルやxlsx記録ファイルに置いてその値が出力される列の名前(記録ファイルの1行目の見出し)を文字列として渡します。第2引数には、出力したい変数や値を記述します。今回の例では、responseという列名で実験参加者が調整した後のprobleLenの値を出力してみることにしましょう。記入すべき文は以下の通りです。

trials.addData('response', probeLen)

この文をCodeコンポーネントに追加すればいいのですが、どの欄に追加すればいいでしょうか。参加者がスペースキーを押して反応を確定した後に保存しないと意味がありませんから、 [Routine終了時] に追加するのが正解です。追加して変更を保存してください。

表7.2 TrialHandlerの主なメソッド。

メソッド, 概要

addData(thisType, value)

実験記録ファイルに出力する値を追加します。thisTypeに実験記録ファイルにおける列名、valueに値を指定します。

getEarlierTrial(n)

n回前に用いられたパラメータを得ます。1回前であればn=-1という具合に負の整数で指定します。nが省略された場合はn=-1と見なされます。n回前が存在しない(2回目で-5を指定するなど)場合はNone、存在する場合はn回前のパラメータが辞書オブジェクトとして得られます。

getFutuerTrial(n)

n回後に用いられるパラメータを得ます。1回後であればn=1という具合に正の整数で指定します。nが省略された場合はn=1と見なされます。n回後が存在しない場合はNone、存在する場合はn回後のパラメータが辞書オブジェクトとして得られます。

保存したら実験を実行してみましょう。終了後にtrial-by-trial記録ファイルとxlsx記録ファイルを開くと、 図7.4 のようにresponseという名前の列が存在していて、そこに調整後のprobeLenの値が出力されているのがわかります。xlsx記録ファイルにはKeyboardコンポーネントの出力と同様に平均値や標準偏差も出力されています。

_images/adddata-output.png

図7.4 addData( )メソッドによる変数の出力。

これでこの章の実験は完成です。ですが、せっかく 表7.2 にaddData( )以外のTrialHandlerのメソッドを紹介しましたので、少し触れておきましょう。 表7.2 に書かれている通り、TrialHandlerにはgetEarlierTrial( )とgetFutuerTrial( )というメソッドがあります。これらのメソッドを使うと、それぞれ現在のループのn回前、およびn回後の繰り返しで条件ファイルから読み込まれたパラメータのどの値が用いられた(用いられる)かを知る事ができます。例えばtrialsループの内部のルーチンにCodeコンポーネントを配置して、 [Routine開始時][フレーム毎][Routine終了時] のいずれかで以下の文を実行すると、2回前の繰り返しで用いられたパラメータが変数prevParamに格納されます。

prevParam = trials.getEarlierTrial(-2)

prevParamに格納されているのは辞書オブジェクトです。 第4章第5章 でも触れたように、辞書オブジェクトとは実験情報ダイアログの値を保持するのにつかわれているデータ形式です。ですから、実験情報ダイアログと同様に、以下のように書くとangleというパラメータの値を取り出すことができます。

prevParam['angle']

記憶課題の一種に「左右の選択肢のうち、n試行前に提示されていた刺激と一致する選択肢を選べば正解」という課題(n-back課題)がありますが、 [Loopの種類] にrandomやfullRandomを選んでいる場合、n試行前に提示した刺激は無作為に決定されているので条件ファイルで正答を定義することができません。そのような場合にgetEarlierTrial( )メソッドは非常に有効です。

チェックリスト
  • TrialHandlerのインスタンスから現在ループの何回目の繰り返しを実行中かを取得できる。

  • TrialHandlerのインスタンスから現在ループの繰り返し回数が残り何回かを取得できる。

  • TrialHandlerのインスタンスから現在ループの総繰り返し回数を取得できる。

  • 上記3項目の値を使って「現在第n試行」、「残りn試行」、「全n試行」といったメッセージをスクリーン上に提示できる。

  • Codeコンポーネントを用いて、実験記録ファイルに出力するデータを追加することができる。

  • 現在実行中の繰り返しのn回前、n回後に使われるパラメータを取得するコードを記述することができる。

7.4. プローブの長さが一定範囲に収まるようにしよう

すでにこの章で目的とする実験は完成しているのですが、if文の練習を兼ねて少し改造してみましょう。exp07.psyexpでは、実際にする人がいるかどうかは別として、テスト刺激に重なったりスクリーンからはみ出してしまったりするくらいプローブを大きくすることができてしまいます。また、プローブをどんどん小さくしていけばいずれ長さは0になり、負の値になってしまいます。長さは負の値をとることができませんので、そこで実験はエラーとなり停止してしまいます。このような事態を避けるために、if文を使ってプローブの長さが0.05から0.35の範囲を超えて短くしたり長くしたりできないようにしてみましょう。

exp07.psyexpをBuilderで開いてtrialルーチンのCodeコンポーネントを開いてください。 [フレーム毎] に入力してあるコードによってプローブの長さが変わるのですから、ここのコードを書きかえると長さを一定の範囲に制限することができるはずです。ひとつの問題を解決するための方法を一度に何通りも紹介するのはよくないかも知れませんが、ここではif文の練習なので2通りの方法を考えます。

_images/restrict-probe-length.png

図7.5 プローブの長さを制限する方法その1。exp07.psyexpのCodeコンポーネントに書いたコードに組み込む処理を日本語で記入したものを左側に、組み込む処理に対応するコードを右側に示しています。

第一の方法は、長さを0.005増加させた時に0.35より大きくなっていないか確認して、なっていれば0.35に修正し、長さを0.005減少させた時に0.05未満になっていないか確認して、なっていれば0.05に修正するというものです。入力済みのコードに日本語で処理を書きこむと 図7.5 の左のようになります。ご覧のとおり、if文で分岐した後にまた「もし~なら…」という分岐処理が含まれる形になっています。if文では、このような「入れ子」になった条件分岐も書くことができます。「probeLenが0.05未満なら0.05にする」という部分だけを考えると、これは0.05以上なら何もしないということですから、elseは省略できて 図7.5 右上のように書けます。同様に「probeLenが0.35より大きければ0.35にする」という処理も 図7.5 右下のように書けます。 図7.5 左側のコードの日本語で記入した部分に、 図7.5 右側の対応するコードを埋め込むと、 図7.6 左に示すコードが得られます。これだけで完成です。

_images/restrict-probe-length-code.png

図7.6 if文を組み込んで得られたコード。if、elif、elseの及ぶ範囲は、下方向に向かってこれらの語が出現した行と字下げ幅が狭いか同じ行に出会うまでです。if (2)が半角スペース4文字字下げされているので、if (2)の次の行はif (2)よりさらに4文字字下げして8文字字下げとなります。

図7.6 左のコードをもう少ししっかり見ておきましょう。 図7.6 左のコードの冒頭部分を拡大したのが 図7.6 右です。ifが及ぶ範囲は字下げの量で決まります。Pythonはifを発見すると、コードを下へ読み進めていって、これらの語が出現した行と字下げ量が同じか少ない行の直前の行までをifの条件式が及ぶ範囲と見なします。このことを念頭に置いて 図7.6 右のコードを見ると、1行目のif文(if (1)とします)の及ぶ範囲は5行目のelif文の手前まで、すなわち4行目までであることがおわかりいただけると思います。if (1)の条件式が真であれば、4行目までのコードが実行されます。一方、3行目のif文(if (2)とします)の範囲はどこまでかと言いますと、これも5行目のelif文の手前まで、すなわち4行目までです。ですからif (2)の条件式が真であれば、4行目だけ実行されます。もちろんif (2)はif (1)の範囲に入っているので、if (2)が実行されるためにはif(1)が真でなければいけません。if (1)が真でif (2)が偽であれば、2行目だけが実行されます。elif、elseが及ぶ範囲もifと同様に決まります。なお、本書では字下げをすべて4文字としているので 図7.6 の説明で問題ないのですが、web上で誰かが書いたPythonのコードを流用する時にはそのコードが異なる字下げルールを使っているかもしれません。そのようなコードをコピーするときの注意点を「 7.6.3:Pythonコードの字下げについて」に記しておきますので参考にしてください。

では、 図7.6 左のコードをtrialルーチンのCodeコンポーネントの [フレーム毎] に入力しましょう。すでに細字の部分は入力済みのはずなので太字部分を入力するだけでいいはずです。入力したら実験を保存して実行し、プローブの長さが一定以上伸びたり縮んだりしないことを確認してください。プローブ長が0.05や0.35に達するまでカーソルキーを連打するのが面倒くさい!という方は実験をいったん終了して、Codeコンポーネントを編集してカーソルキーの左右を押したときにprobeLenが増減する量を±0.005から±0.05などに変更して実行してみましょう。

続いて第二の方法の解説です。第一の方法ではprobeLenの長さを増減した後に範囲外に出てしまっていないかを確認しましたが、第二の方法では一連のif-elif-elseが終わってからprobeLenを確認します。 図7.7 にこの方法を用いたコードを示します。ここでのポイントは、ifに対応するelif、elseが置ける範囲です。 図7.7 に記した通り、1行目のif文(if (1)とします)に対応するelif、elseが置けるのは、if (1)と字下げが同じでelif、else以外から始まる行が出てくるか、if (1)より字下げが少ない行が出てくる直前の行までです。 図7.7 のコードでは、2つ目のif (if (2)とします)が出現した時点でif (1)が終了していますので、if (1)から続く一連のif、elifの結果がどうであろうと必ずif (2)の条件式は評価されます。elifで条件式を列挙した場合は手前のifやelifで真になった時に全く評価されなかったのと対照的です。

_images/restrict-probe-length-code-2.png

図7.7 プローブの長さを制限する方法その2。ifに対応するelif、elseを置ける範囲に注意。

exp07c.psyexpはexp07a.psyexpを別名で保存して作成したので、trialルーチンのCodeコンポーネントの [フレーム毎]図7.7 のコードのif (2)の手前まですでに入力済みのはずです。そこへ、 図7.7 のコードの最後の4行(if (2)に対応する部分)を追加入力してください。continueRoutine = Falseという行とif (2)の最初の行の間は空白行を入れても入れなくても動作しますが、1行空白を入れておいた方が後から見直した時にここで新たなif文が始まることがわかりやすくてよいでしょう。入力を終えたら、exp07c.psyexpを保存して実験を実行してみてください。exp07b.psyexpの時と同様に、プローブの長さが一定以上伸びたり縮んだりしないはずです。キーを連打するのが面倒な方はやはりexp07b.psyexpの時と同様に、一回のキー押しでprobeLenを増減する量を大きくして試してみましょう。

以上でif文の練習は終わりですが、最後にひとつ補足しておきます。第一の方法は入れ子になったif文の練習のためにまず「カーソルキーの左が押されたか」を判定してprobeLenの長さを変更してから「probeLenが0.05未満か」を判定するという二段階の判定を行いましたが、第6章 で学んだ論理演算子を使えば一度に判定することができます。probeLenの初期値は0.17、0.19、0.21、0.23の4通りしかなくて、±0.005ずつしか増減しないのですから、0.005を引いて0.05未満になるのはprobeLenが0.05の時のみです。ということは、「カーソルキーの左が押されていて、なおかつprobeLenが0.05より大きい」時にはprobeLenから0.005を引いても0.05を下回ることはありません。したがって、論理演算子andを使って一つの条件式として記述できます。

if 'left' in theseKeys and probeLen > 0.05:
    probeLen -= 0.005

同様に、probeLen に0.005を加えて0.35より大きくなるのはprobeLenが0.35に達しているときだけですから、「カーソルキーの右が押されていて、なおかつprobeLenが0.35未満」の時にはprobeLenに0.005を足しても0.35を超えません。したがって、この条件はandを使って一つの式として記述できます。

elif 'right' in theseKeys and probeLen < 0.35:
    probeLen += 0.005

この方法の弱点は、probeLenの増減量が可変である時にはかえって複雑になってしまうことです。その場合は 図7.6図7.7 に示した方法を用いた方がすっきりとしたコードが書けます。この「増減量が可変」な実験を作成することは練習問題としておきましょう。 なお、この章で作成した実験は 第8章 で活用しますので残しておいてください。

チェックリスト
  • if文の中に入れ子上にif文を組み込んだコードを記述することができる。

  • ifやelifの条件式が真であった時に実行されるコードがどこまで続いているかを判断することができる。if、elifの条件式が全て偽でelseまで進んだときに実行されるコードがどこまで続いているかを判断することができる。

  • 一連のif-elif-elseの組み合わせがどこまで続いているかを判断することができる。

  • 論理演算子を用いて複数の条件式をひとつの式にまとめることができる。

7.5. 練習問題:プローブ刺激の伸縮量を切り替えられるようにしよう

exp07a.psyexpをベースにして、プローブの伸縮量(probeLenの増減量)を切り替えられるようにしてみましょう。二通りの実現方法を挙げますので、ぜひ両方の方法の実現に挑戦してください。

  • 実現方法その1

    • Shiftキーを押すと、伸縮量が±2と±10で切り替わる。余力がある人はTextコンポーネントを使って現在の伸縮量を画面上に提示すること。

      • ヒント1:伸縮量の絶対値を保持する変数を一つ用意して、Shiftキーが押されたら値を切り替える。

      • ヒント2:伸縮量の絶対値を保持する変数には、ルーチン開始時に初期値を与える必要がある。

  • 実現方法その2

    • Xキーを押すと-10、Cキーを押すと-2、ピリオド( . )キーを押すと+2、スラッシュ( / )キーを押すと+10伸縮する。

      • この方法についてはヒントなし。

「どちらもあっさりできてしまった」という方は、以下の問題にも取り組んでみてください。

  • 実現方法その1、その2共通

    • 図7.7 の例のように、キー入力に関する処理を終えた後にprobeLenが範囲を超えていないかを確認して必要があれば値を修正すること。ただし、その際if文を使わずに、第5章 に出てきた関数を使って「1行で」処理を記述すること。

      • ヒント:二種類の関数を使う必要がある。

7.6. この章のトピックス

7.6.1. 複数キーの同時押しの検出について

キーボードは製品によってテンキーと呼ばれる独立した数字や四則演算のキーがあったり、音量を調節するためのキーがあったり、いろいろなものがありますが、特殊なものを除けば80個以上のキーがあります。これらのキーは物理的には複数個同時に押すことはできますが、文書を書くなどの一般的な用途では「PとSとYのキーを同時押しする」といった具合に複数の文字キーを押すことはありません。ですから、市販されているキーボードの中には、ShiftやCtrlといった特殊なキーを除いて、複数キーの同時押しを検出することを前提に設計されていないものがあります。例えば筆者が使用しているキーボードの中には、F、G、H、Jのキーを同時に押すとFとJのみ、GとHのみといった具合にいずれか2つのみしか同時に認識しないものがあります。一方、これらの4個のキーの同時押しを認識させることができるキーボードもあります。

通常の文書入力では4つのキーの同時押しを検出できなくても困ることはまず無いのですが、キーボードを使用して操作するアクションゲームの場合は大きな問題になり得ます。ですから、ゲーム用と銘打って販売されているキーボードは多くのキーを同時押しできるように設計されています。中にはすべてのキーの同時押しを検出できる製品もあります。

Builderは、同時押しに対応したキーボード、対応していないキーボードのどちらが接続されていて同じコードで処理できるように、変数theseKeysに押されたキー名を必ずリストとして保存するように作られています。複数キーを同時押ししているにも関わらずtheseKeysに押したキー名が含まれていない場合は、そのキーの組み合わせが使用中のキーボードで検出できない組み合わせである可能性があります。

7.6.2. メソッドの第一引数について(上級)

TrialHandlerの主要クラスメソッドの表( 表7.2 )において、getEarlierTrial( )の引数はnの1個だけしか示されていません。しかし、Pythonインタプリタ上でpsychopy.dataをimportしてhelp(psychopy.data.TrialHandler)を実行してgetEarlierTrial( )のヘルプを見ると、selfとnという2つの引数が記載されています。 表7.2 で第一引数selfを省略している理由について簡単に解説します。

公式ヘルプに記載されている第一引数selfですが、これはTrialHandlerに限らずすべてのクラスのメソッドに必ず存在します。これは「インスタンス自身」を指し示す引数です。C言語やC++言語を御存知の方には、「インスタンスへのポインタが渡される」と言えばわかりやすいかも知れません。クラスの定義に関するPythonの文法を解説していないのでどうしても不正確な説明にしかならないのですが、このselfはインスタンスが自分自身に格納されたデータ属性の値を知るために必要なもの、と思っておいてください。

Pythonの文法では、メソッドを呼び出す時にselfは省略して記述すると定められています。ですから、引数がselfのみしかないfooというメソッドを呼び出す場合はfoo( )という具合に括弧の中は空白にします。TrialHandlerのgetEarlierTrial( )を呼び出す場合には、このメソッドにはselfとnという2つの引数があるので、selfを省略してgetEarlierTrial(n)と書きます。 表7.2 では、実際にコードを書くときの表記と一致させることを重視してgetEarlierTrial( )の引数としてnのみを記載しています。

なお、同じくヘルプのaddData( )メソッドの引数を見ると、self、thisType、valueに加えてさらにposition=Noneと書かれています。これはデフォルト値付き引数と呼ばれるもので、「引数positionが渡されなかった場合はNoneが渡されたと解釈する」ということを意味します。ですから、本文中でtrials.addData('response', probeLen)としたようにpositionに相当する引数を渡さなくてもエラーにならなかったのです。getEarlierTrial( )の引数nもn=-1という具合にデフォルト値付き引数として定義されているので、本文中や 表7.2 で述べたようにnを省略することができるのです。

7.6.3. Pythonコードの字下げについて

第6章 において、Python Enhancement Proposals (PEP)という公式文書で半角スペース4文字の字下げが推奨されていることを紹介しました。PEPはPythonの言語仕様やPythonプログラマのコミュニティ向けの情報などを記述した文書の集合で、その中のPEP-8というPythonのコードの書き方を定めた文書に「半角スペース4文字を使いなさい」と記されています。

しかし、Python以外のプログラミング言語では字下げにTab文字や8文字の半角スペースなど、さまざまな字下げが使用できるためか、Pythonでも半角スペース4文字以外の字下げを使用できるようになっています。 図7.8 は、2文字や6文字の字下げが混在しているコードの例を示しています。 図7.8 左のように字下げが混在していてもそれぞれのブロック内で字下げが統一されていれば動作します。例えば 図7.8 の(3)は直前のifに対する字下げが6文字、(4)は直前のelseに対する字下げが4文字であり、一連のif-else文にも関わらず字下げが一致していません。しかし、(3)、(4)のブロック内でそれぞれ字下げが一貫しているのでPythonは適切にこのコードを解釈して実行することができます。それに対して 図7.8 の(5)では、最初の2行の字下げが4文字であるにもかかわらず最後のi+=1の字下げは3文字であり、(5)のブロック内で一貫していません。従って、 図7.8 右のコードはエラーとなり実行できません。

_images/indent-in-python-code.png

図7.8 4文字以外の半角スペースによる字下げ。それぞれのブロック内で字下げが一貫していればエラーにはなりません。

スペースとTab文字が混在しているとさらに事態は複雑になります。基本的には、Tab文字は半角スペース8文字に置き換えられます。しかし、半角スペース8文字未満にTab文字が続く場合は、半角スペースとTab文字を合わせて半角スペース8文字と解釈されます。 図7.9 の例をご覧ください。 図7.9 左のコードの最終行は、4文字の半角スペースより前にTab文字がありますから、Tab文字が半角スペース8文字分に解釈されて合計半角スペース12文字と解釈されます。従って、 図7.9 左のコードはエラーとならず実行できます。一方、 図7.9 右の最終行は、左と同じTab文字と半角スペース4文字の組み合わせなのですが、半角スペースがTab文字より前にあります。この場合、半角スペースとTab文字を合わせて半角スペース8文字と解釈されますので、直前のif文と字下げ量が同じとなってしまいエラーになります。

以上のように、Pythonのスクリプトでは字下げは半角スペース4文字でなくても動作します。しかし、混乱を避けるためにはやはりPEP-8に従って半角スペース4文字で統一するべきだと思われます。

_images/tab-in-python-code.png

図7.9 Tab文字による字下げ。基本的にはTab文字は半角スペース8文字に置換されると考えておけばよいですが、左の例のように半角スペースの後ろにTabがある場合は半角スペースとTabをまとめて8の倍数個のスペースとして解釈されてしまいます。

7.6.4. Variableコンポーネントによる変数の値の出力

Variableコンポーネントはコンポーネントペインの「カスタム」カテゴリにあります( 図7.10 )。 BuilderはユーザーがCodeコンポーネントで独自に追加した変数を把握できないので、実験記録ファイルに値を出力することもしませんし、コンポーネントの [名前] や条件ファイルのパラメータ名と重複していないかどうかも確認しません。いずれも初心者にはわかりにくく、特に後者(重複を確認しない)は熟練者でもなかなか気づかないような「意図しない動作」の原因になりかねません。 Variableコンポーネントを使うと、Builderがその変数の存在を把握できるので、 [名前] の重複も確認されますし、Codeコンポーネントを使わずに値を設定、変更したり実験記録ファイルに出力させたりすることができます。

_images/variable-component.png

図7.10 Variableコンポーネントは「カスタム」カテゴリにある。

表7.3 にVariableコンポーネントの主なプロパティを示します。 [フレーム更新開始時の値 $][フレーム更新時の値を保存] は直感的ではないので注意が必要ですが、それ以外は難しい点はないと思います。ルーチン上でのコンポーネントの並び順に従って処理されるので、Variableコンポーネントで値を設定した変数を利用するコンポーネントがある場合は、Variableコンポーネントより下になるように配置することに気をつけてください。 本章の実験で使用するなら、「 7.3:Codeコンポーネント使って独自の変数の値を記録ファイルに出力しよう 」の作業をする前の時点でtrialコンポーネントにVariableコンポーネントを設置して、 [名前] をprobeLen、 [Routine開始時の値 $] にinitProbeLenと設定すると良いでしょう。そして、調整後の値を実験記録ファイルに出力するには [Routine終了時の値を保存] をチェックしてください。

表7.3 Variableコンポーネントの主要なプロパティ。

[名前]

他のコンポーネントと同様、このコンポーネントの名前ですが、同時に変数の名前となります。つまりfooという [名前] でVariableコンポーネントを配置すると、実験内でfooという変数が使用できるようになります。

[実験開始時の値 $]

実験開始時の値を指定します。Codeコンポーネントで定義するなら「実験開始時」のタブで初期化したい変数ならここで定義します。

[Routine開始時の値 $]

Routine開始時の値を指定します。Codeコンポーネントで定義するなら「Routine開始時」のタブで初期化したい変数ならここで定義します。

[フレーム更新開始時の値 $]

フレーム更新開始時の値を指定します。ここでいう「フレーム更新開始時」とは正確にはルーチンペイン上で当該Variableコンポーネントの処理順がまわってきたタイミングを指します。もしルーチンペイン上でVariableコンポーネントより上に他のコンポーネントが配置されている場合は、そちらの処理が先に行われます。

[実験開始時の値を保存]

[実験開始時の値 $] に設定した値を実験記録ファイルに出力したい場合にチェックします。

[Routine開始時の値を保存]

[Routine開始時の値 $] に設定した値を実験記録ファイルに出力したい場合にチェックします。

[フレーム更新時の値を保存]

「最初」、「最後」、「すべて」、「なし」から選択します。保存しない場合は「なし」にしてください。Variableコンポーネントを使って値を設定していないと保存されないので注意してください。

[Routine終了時の値を保存]

ルーチンが終了した時点のその変数の値を実験記録ファイルに出力します。Variableコンポーネントを使って値を設定していないと保存されないので注意してください。

[実験終了時の値を保存]

フローの最後まで実験が進行して終了処理をおこなう時点でのその変数の値を実験記録ファイルに出力します。Variableコンポーネントを使って値を設定していないと保存されないので注意してください。

本文のCodeコンポーネントを使う方法と、Variableコンポーネントを使う方法のどちらを使うかは好みで決めてよいですが、それぞれに長所短所があります。Variableコンポーネントの長所は先ほど書いた通り、名前が重複していないかBuilderがチェックしてくれることと、これだけのためにCodeコンポーネントを使う必要がないことです。ルーチンペインにVariableコンポーネントが配置されていれば、その名前の変数が使用されいていることが一目瞭然というのも利点です。自分が作った実験でも数か月経ったら詳細を忘れてしまったりするものですし、研究室で先輩から後輩へと引き継いでいくような場合には「わかりやすい」ということは特にありがたいでしょう。

Variableコンポーネントの短所は、複数のルーチンにまたがって使用しなければいけない変数の扱いが難しくなる場合があることです。例えばpracticeというルーチンにresponseという名前のVariableコンポーネントを置いたら、trialというルーチンに同じresponseという名前のVariableコンポーネントを置くことはできません。Builderの実験においてコンポーネントの名前はグローバルなものなので、どちらか一方のルーチンに配置しておけばもう一方でもその変数にアクセスできますが、 [Routine開始時の値$] などのプロパティを使って値を設定したり、 [Routine終了時の値を保存] を使って値を保存したりできるのはコンポーネントが置かれているルーチンのみです。そういった処理が必要な場合は結局Codeコンポーネントを使用しなければいけません。Variableコンポーネントで「こういう名前の変数を使用している」とアピールしつつ、複数ルーチンにまたがる処理はCodeコンポーネントを使うといった組み合わせも可能なので、うまく活用してください。