例題24-2:「PsychoPy Builderで作る心理学実験」練習問題解説

A: やっと「PsychoPy Builderで作る心理学実験」の練習問題の解答例がアップロードされたので少し補足しておきたい。

B: へ?ぼくらがするんですか?

A: しないのか?

B: いや、アレはあくまで僕らとは別のコンテンツなのかな…と思っていましたので。筆者の中ではその辺の線引きどうなってんでしょ。

A: どうなってんだろうね。それはともかく、「PsychoPy Builderで作る心理学実験」はPsychoP 1.79.01の頃に作成したけど現行バージョンは1.81または1.82なので、スクリーンキャプチャは新しいバージョンで撮ってます。また、ダウンロード用の解答例も一部は新しいバージョンで作りなおしているので1.79では開けないかも知れません。それではあまり時間が無いのでスタート。

第3章: 練習試行を挿入する

A: 最初の練習問題だが…、これは正直解説することが無いな。「PsychoPy Builderで作る心理学実験」でも書いた通り、「同一ルーチンを複数回フローに挿入する」ってのが実際にはどんな時に使えるのかという例になっている。操作としては単にフローにルーチンを挿入するだけで、ごく基本的なものだ。

B: …。

A: …。

B: …へ? もう終わり?

A: 終わり。練習試行のループの設定を確認して、本試行の条件ファイルがそのまま流用されていることも確認しておいてくださいね。それでは次。

第4章: ブロックの順序を指定できるようにする

B: これはきちんと解説してくれなきゃでしょ。

A: うむ。まあ解答はZipファイルを見てくれた通りなんだが、ここでは多重ループを組む時に、内側と外側のループの条件ファイルにどのようにパラメータを割り振ればよいかということを補足しておきたい。

B: ほう。それはどういう?

A: この表を見て欲しい。第4章の練習問題では、外側のループでブロックを、内側のループでブロック内の試行の繰り返しを実現する。変化するパラメータがどちらのループで変化するのかをまとめたのがこの表だ。

../_images/24-2-01.png

B: ふむふむ。

A: 練習問題では、「±70度条件」といった具合に次のブロックの条件を示す文字列を描画するのが目標だった。これは、ブロック間で変化するが同一ブロック内の試行の繰り返しでは変化しないパラメータとなっている。このようなパラメータは、ブロックの設定をする条件ファイル、すなわち外側の条件ファイルに記述する。

B: 「外側のループで変化する」の列にしか○がついてない場合は外側の条件ファイルでってことですね。

A: そう。ちなみにこの章の練習問題では上記の表で左側にしか○がつかないパラメータをどこで設定するかを考えてもらうために「教示を表示する」というのを求めている。

B: へえ。一応考えてるんですね。

A: 続いてテスト刺激の傾きだが、これはブロックが変化しても一貫している。当然試行を繰り返すループ、すなわち内側のループの条件ファイルで定義する必要がある。

B: ですね。

A: で、問題はコンテクスト刺激の傾きなのだが、これはブロック間でも同一ブロックの試行間でも変化する。

B: へ? ブロック間でしか変化しないんじゃないですか?

A: コンテクスト刺激が「+70度で固定」といった具合に全く変化しないのならB君の言うとおりである 。しかし、この実験ではコンテクスト刺激が「70度または-70度」といった具合に試行間でも変化するのだから、ブロック間と同一ブロックの試行間の両方で変化している。

B: あ、そうか。確かに。

A: このようなパラメータは内側のループの条件ファイルで定義すればいいのだが、ブロック間での切り替わりを実現するためには複数の内側ループ用条件ファイルが必要となる。 従って、このような表を書いた時に両側の列に○がつくパラメータが一つでもあれば、第4章で紹介した複数個の内側ループ用条件ファイルを外側ループの条件ファイルで切り替えるテクニックが必要となる

B: ほう。逆に言えば、「+70度で固定」とかにしちゃえばこのテクニックが不要ということですか?

A: そういうデザインの実験を作る場合であれば、な。その通り。今回の練習問題の場合はcontextDirがコンテクスト刺激の向きを決定するパラメータだから、contextDirの定義を内側ループの条件ファイルから外側ループの条件ファイルへ移せばよい。内側ループの条件ファイルを外側ループで切り替える必要はなくなったから、cndFileNameは不要となる。

../_images/24-2-02.png

B: ふうむ。ちょっと解説ページを設けた意義が感じられてきたっぽいぞ。

第5章: 値の範囲を制限する

A: さて、この第5章の練習問題なんだが、どうもどのバージョンで修正されたのかわかんないんだがGratingComponentのColorに1より大きい値を指定した時のバグが現行(1.82.01)では生じないんだ。というわけでいきなりこの練習問題の意義が問われる事態となったわけだが…。

B: でもこのテクニックは重要ですよね。意味あると思いますが。

A: ほう。まだ解説していないのにテクニックの重要性がわかる、と。ぜひB君が解説して見給え。

B: へへへ。これはヒントで表5-2の関数を使えと言っているんですから、ずばりmax()でしょう。max(t/6.0,1)とすれば、tの値が6.0を超えても…って、あれ?

A: (冷めた顔で)min()、な。

B: そうそう、min()ですよ、min()! min(t/6.0,1)とすれば、tの値が6.0を超えても1を超えることはありません。逆にmax(t/6.0,0.5)とすれば、tが3.0未満でも0.5以下になることはありません。値を一定の範囲内に収める必要がある時にとても便利なのです!

A: …まったくB君の言う通りなのだが、minとmaxが逆だったので7点。

B: 10点満点で?

A: 100点満点。

B: くっ、厳しい!

A: というわけで、min(t/6.0, 1)に書き換えるというのがこの練習問題の答え。解答例のZipファイルでは、もうひとひねりしてblankルーチンを削除してなおかつ1.0秒の試行間隔を確保するという例を示しているので参考にしてほしい。

B: ポイントはtargetStimのColorプロパティですね。$min(max((t-1)/6.0,0),1)。

A: 第5章5.3節で紹介した「関数の引数に関数を書く」の例にもなっている。今度こそこの式を説明して見給え。

B: ええと、まず内側のmax((t-1)/6.0-1,0)を見ます。(t-1)/6.0はt=1で0.0、t=7で1.0となりますから、ルーチン開始1秒後に0.0、7秒後に1.0となります。6秒間かけて0から1になる、という要件を満たしています。

A: うむ。で?

B: でも、このままではルーチン開始0秒から1秒の間に負の値になってしまいます。それを避けるためにmax()を用いて、0を下回らないようにします。それがmax((t-1)/6.0-1,0)。

A: で?

B: max((t-1)/6.0-1,0)だけだと今度は7秒以上経過した時に1.0を超えてしまいますので、1.0に抑え込むためにmin()を使います。これはさっきと同じ理屈です。min(ほげ, 1)の「ほげ」の部分にmax((t-1)/6.0-1,0)を代入すると、min(max((t-1)/6.0,0),1)となります。

A: ふむ。なかなかやるじゃないか。「ほげ」という変数名が気になるが合格としておいてやろう。

B: ふふふ。これがぼくの実力ですよ。

A: ところでB君の言うように書き換えると、分析方法がちょっと変わってくるのはわかってるか?

B: え? なぜ?

A: 今までt=3.0で変化の中点だったのがt=4.0になるだろ。値が1ずれるんだから、分析時に1引くなりなんなりしないといけない。

B: む。それは確かに。

A: そういうわけで微妙な改造だが、その代わりにBlankルーチンを使う場合と違ってルーチン終了時と開始時の処理が無いから、試行間隔の時間的な正確さを求めるのであれば、こちらの方がほんの少しだけ正確だろうとは思う。本当にわずかな差だが。

B: うーん。奥が深いなあ。

第6章: 複雑な条件ファイルを作成する

A: この練習問題は…。PsychoPyに関する部分は実にしょーもない。単にtrialルーチンのCodeコンポーネントに入力するif文の条件式が key_choice.corr になるだけだ。

if key_choice.corr:
    feedbackMsg = u'正解'
else:
    feedbackMsg = u'不正解'

B: …これだけ?

A: これだけだとあんまりなので、条件ファイルで使っているテクニックを解説しておこう。Zipファイルに含まれているexp06cndp1.xlsxは「眼鏡をかけていて眉が上がっている(論理積)」顔画像が正事例となる条件ファイルだ。exp06cndp2.xlsxは「顔が丸い、または眉毛が下がっている(論理和)」顔画像が正事例となっている。

B: ふむふむ。

A: 正事例となる顔画像ファイルの行にleft、それ以外の行にrightと入力していくわけだが、ファイル名と正事例との対応がややこしくて入力ミスが起こりやすい。B君、わかるか?

B: へ?なにが?

A: ファイル名と正事例の対応だよ。

B: ええと、眼鏡をかけているんだからファイル名の数値の1桁目が1。そして眉が上がっているんだから4桁目が0。

A: それはAND?OR?

B: 「かつ」だからAND。

A: その通り。もうひとつは?

B: 顔が丸いのは2桁目が0。眉が当たっているのは4桁目が1。「または」だからOR。

A: その通り。ではこれらの条件を満たしているファイル名がどれか、exp06cndp1.xlsxにずらっとファイル名の中からすぐに見分けられるか?

B: leftって入力済みの行じゃないんですか?

A: そーゆー話の腰を折ることを言うな。ファイル名を見てぱっと見分けがつくかって言ってんだよ。

B: そんなのつくわけないじゃないですか。

A: だろ。そこでExcelの式だ。exp06cndp1.xlsxのcorrectAnsの列に入力されている式を見たまえ。

B: へ?これ式なんですか? =IF(AND(MID(A2,5,1)="1",MID(A2,8,1)="0"),"left","right")。ええと、これは…

A: MID(A2,5,1)はセルA2の文字列の5文字目から1文字取り出す。ファイル名の先頭に"FACE"と入っているので、5文字目は1桁目だ。同様にMID(A2,8,1)は4桁目。これらがそれぞれ1、0と等しいか比較している。そしてAND()を使って、これらが同時に真である時にはleft、そうでない時にはrightとなるようにしている。

../_images/24-2-03.png

B: いや、それは自力でもわかりますが、 条件ファイルの値って、Excelの式でもいい んですねえ。

A: うむ。そのことを知っておくと、複雑な条件ファイルがぐっと作りやすくなる。ぜひ覚えておきたいところだ。

第7章: if文とユーザー定義変数を使いこなす

B: ま、これは実にありがちな練習問題ですね。ぼくの見立てでは、Shiftキーで切り替える方が難しいですね。

A: ほほう。それはなぜ?

B: なぜって、今10ピクセル伸縮するのか、2ピクセル伸縮するのかを保持する変数を用意する必要があるじゃないですか。面倒くさい。

A: ふむ、それもそうだが、ここでShiftキーを使えという練習問題を持ってきたのにはもう一つ理由があると聞いておるぞよ。

B: へ? Shiftキーに意味があるんですか?

A: Shiftキーはよくキー名を間違えるのだ。 shiftという名前のキーは無く、左シフトキーはlshift、右shiftキーはrshiftという名前なのだ

B: あー。それは確かに引っかかるかも。

A: で、練習問題では「Shiftキーを押すと」としか書いていない。参加者にそう教示したら大抵の人は左shiftを押すだろうが、右shiftを押されても文句は言えない。そこで、論理演算子orを使って'lshift' in theseKeys or 'rshift' in theseKeysという条件式で検出する。

B: こんなの左Shiftだけ使ってって教示すればいいじゃないですか。

A: これは練習なんだよ、練習。そんなこと言い出したらここまでの練習問題も「そんなの○○でいいですよね」ばっかりだろ。あと練習問題にはprobeLenの長さを確認して長すぎたり短すぎたりしたら「1行で」修正するというのもあるが、こっちはわかるな?

B: これは第5章の練習問題で出てきたmin()とmax()を組み合わせろという意図ですよね。わかりますよ。

A: うむ。

B: じゃあ次の練習問題ですか…って、あれ? 第8章の練習問題は?

A: 作者がいろいろと追いつめられまくっているからここで「いったん中断」だと聞いておるぞよ。いつ再開するのか知らんが。ていうか、練習問題の解説よりも1.81以降のバージョンにあわせて全面的に改訂せんにゃいかんと思うのだがね。

B: んー。なんだか雲行き怪しいですね。じゃ、とりあえずここで「つづく」ですかね?

------------------------------ 後日 ------------------------------

第8章: expInfoダイアログを用いた柔軟な動作の切り替え

A: やっぱりやり残しがあると気になるのでざっと済ませちゃうぞ。B君、つきあえ。

B: えー。今日は卵が安いんでこれを印刷したら帰りたいんですが。

A: これって、なんだこりゃ。論文じゃないな。

B: 検索でひっかかったんですけど、結構詳しく解説されているっぽいので。

A: ふうん。まだかなりページ数あるじゃん。さっさと終わるから付き合え。

B: 仕方ないですねえ。じゃあ30分以内で。

A: よっしゃ。じゃあいくぞ。まずは8章。expInfoダイアログでプローブの動きを切り替えることと、パス上にあるか否かでプローブの色を変えること。

B: expInfoダイアログにUDと入力したら上下反転、LRと入力したら左右反転でした。if文で反転の切り替えをすればいいんですよね。

A: まあそうだな。とみに、そのif文はCodeコンポーネントのどこに置く?

B: へ?どこって、trialルーチンのEach Frameじゃないんですか? マウスカーソルの反転は毎フレームしなきゃいけないので。

if expInfo['direction (UD/LR/other)'] == 'UD':
    px = mousePos[0]
    py = -mousePos[1]
elif expInfo['direction (UD/LR/other)'] == 'LR':
    px = -mousePos[0]
    py = mousePos[1]
else:
    px = mousePos[0]
    py = mousePos[1]

A: 反転を毎フレームしないといけないというのはB君の言うとおりだが、 反転方向は実験を通じて変化しない ので、符号を変数に保持すれば毎フレームif文を実行する必要はない。Begin Expeimentの時点でこのように適当な変数に、ここではudcoeffとlrcoeffという変数に符号を保持しておく。

B: 反転なら-1、反転しないなら1ですね。

if expInfo['direction (UD/LR/other)'] == 'UD':
    udcoeff = -1
    lrcoeff = 1
elif expInfo['direction (UD/LR/other)'] == 'LR':
    udcoeff = 1
    lrcoeff = -1
else:
    udcoeff = 1
    lrcoeff = 1

A: そしてこの変数を利用すれば、毎フレームif文を実行する必要がない。

mousePos = mouseTrial.getPos()
px = lrcoeff * mousePos[0]
py = udcoeff * mousePos[1]
if goalDisc.contains([px,py]):
    continueRoutine = False

B: むむむ。なるほど。セコい改善な気がしますが、毎フレームの処理が少なくなるのは大きいですよね。これでどのくらい動作が速くなるんですか?

A: 私の古いCore i5のノートPCで0.6μsecくらい速い。

B: えっ、それだけしか違わないんですか。微妙…。

A: 私はもっと差が小さいだろうと思っていたが。ま、今どきのPCのパワーだったらよほど毎フレームの描画処理が重くない限り、こんな細かい点には気を遣わなくていいんだろうけどね。年寄りはそういうのを気にしてしまうのよ。

B: 年寄ねえ。

A: あともう一つはパスに乗っかってるか否かで色を変える処理か。まあこれは解説は不要でしょう。プローブのColorを$probeColorしておいて、Every Frameで以下のコードを実行すればいい。

onPath = False
probeColor = 'black'
for path in [path1, path2, path3, path4, path5]:
    if path.contains([px, py]):
        onPath = True
        probeColor = 'red'

B: じゃ、これで第8章は終わりですかね。

A: あー。練習問題として出題されていたのはこれで終わりだけど、ひとつだけ補足。えーと、ごほん。トピック8-1のsetPos()が使えない問題に関してはPsychoPy 1.82.01で大きく改善されました。座標値が整数の場合に限り、setPos()を使用することが出来ます。整数しか使えないと単位がnormとかheightの場合にとっても困りますが、開発版ではもう小数でも動作するようになっているので時期リリースでは問題なく使えるようになるでしょう。というわけで、setPos()を使って各試行の開始時に強制的にプローブを移動させる例を示します。以下のコードをreadyルーチンのEach Frameで実行してください。参加者がマウスを操作してもフレーム毎に強制的に開始位置へマウスカーソルを戻しますので、実験参加者側から見るとまったくマウスによる移動が出来なくなっているように見えます。こうなるとマウスのクリック時にプローブがスタート地点上にあるか否か判定する必要がないので、単にクリックを確認したらcontinueRoutine=FalseするだけでOKです。

B: おおう。これはなかなか。やっぱこうあるべきですよねえ。

  • exp08_setPos.psyexp (PsychoPy 1.82.01以降で動作:exp08_practice.zipに含まれるexp08cnd.xlsxが必要です)

px = startPos[0]
py = startPos[1]
mouseReady.setPos((lrcoeff*int(px),udcoeff*int(py))) # position must be integer in 1.82.01
if mouseReady.getPressed()[0]==1:
    continueRoutine = False

第9章: 複雑なフロー制御

B: さて、やってきちゃいましたよこの章が。この章はホントに難解でした。こんなの、Builderで作る必要あるんですか?

A: ふっ、それはとある 偉い先生 から「こういう実験ってBuilderで出来るの?」と聞かれたから仕方がないのだよ。誰もあのお方に逆らうことは出来ない。

B: 偉い先生って誰なんですかねー。僕が知ってる人ですか?

A: いやいや。畏れ多くてとてもとても。

B: よくわかんないなあ。

A: さて、そんなことはさておき、練習問題の解答。第9章には3つの練習問題があるので、ひとつずつ順番に解説する。まず問1は教示を表示しなさいというもの。当然教示を表示することが目的なのではなく、教示の表示を終えて実験を開始した時刻からの経過時間を試行中に表示する方法を考えるのが目的だ。

B: ふむふむ。

A: まず、一番簡単だが避けるべき方法は、instructionルーチンが終わった時点でグローバルクロックをリセットしてしまう事だ。なぜ避けるべきかわかるかね?

B: グローバルクロックをリセットすると困るから?

A: 正解!…と言いそうになったけど、どう困るか答えないと意味ないだろ。

B: はっはっは。わかりません。

A: …。グローバルクロックはBuilderの実験を動作させるために使われる内部変数であり、不用意に値を変更すると実験が正常に動作しなくなる可能性がある。この手の内部変数はあくまで参照するにとどめて、値を変更するべきではない。

B: だ、そうですよ。皆さんわかりましたか?

A: まったくこいつは…。まあいい。じゃあどうすればいい?

B: 時計をもう一個用意すればいいんですよね。myClock = psychopy.core.Clock()とかして、myClock.reset()するんでしょう。でしょ?

A: ぶぶー。間違い。

B: え? いや、これで出来るでしょう? ほら、こうやって…って、あれ? psychopy.core.Clockなんて知らないって言われてしまう!

A: そら見たことか。Builderで作った実験をコンパイルしてみるとわかるが、Builderで作った実験ではfrom psychopy import coreとしてpsychopy.coreが読みこまれているのだ。ということは、psychopy.core.Clockと書かずにcore.Clockと書かなければいけない。

B: 騙された…。

A: 誰が騙したか。人聞きが悪い。一般論として、測らないといけない時間が複数ある場合は複数個の時計を用意するという発想は正しい。が、ここではBuilderのそんな裏側まで知らずに実現する方法を考えたい。

B: どうするんです?

A: 教示の表示を終えた時刻を変数に保持しておけばいいのだよ。まずinstructionのルーチンのEnd Routineで以下のコードを実行する。

instFinishTime = globalClock.getTime()

B: 待ってください。んじゃ、経過時間の表示をこうすればいいんでしょう。trialルーチンのclockTrialのTextプロパティで、globalClock()の戻り値からinstFinishTimeを引くんです。

$'%.1f' % (globalClock.getTime()-instFinishTime)

A: おう。その通り。

B: ふぅ、なんとか挽回したぞ。

A: んじゃ、問2にいくか。

B: あれ?強化してくんないんですか?

A: (無視して)問2では選択画面でのキー押しでVIスケジュールのパラメータが変化する。第9章の本文ではVIとVRが切り替わる例を示したが、この問2ではVIのインターバルが変わるだけで他は全く同じ。こんな場合は、nRepsを操作するまでもなく条件ファイルの切り替えで対処できるという例だね。

B: ぶつぶつ。

A: まあ、「Zipファイルに含まれている解答例を見てください:でいいんだが、一応ポイントを挙げておくとchoiceルーチンのEnd Routineで以下のように二つの条件ファイルを切り替える。続くvi_trialsループでは変数conditionを条件ファイルとして参照するわけだね。

if 'left' in theseKeys:
    condition = 'exp09p2_left.xlsx'
elif 'right' in theseKeys:
    condition = 'exp09p2_right.xlsx'

B: ぶつぶつ。

A: なんだ。何をぶつぶつ言ってんのさ。

B: ぼくが望ましい行動をしたらAさんは強化すべきだと思います。

A: 強化か。それもいいかも知れんが、早く終わらないと卵の特売に間に合わないんじゃなかったのか。

B: あーっ!!卵! Aさん、ほら急いで急いで!

A: やれやれ。問3は難問かも知れないが、本文にほとんど答えを書いているようなもんだから、分かった人も多いだろう。なあB君?

B: ほらっ、早く、早く!

A: …こりゃ使い物にならんな。印刷も終わったようだし、後は一人でやるからもう帰りたまえ。

B: あっ、ありがとうございます! …って、プリンター用紙切れて止まってる!

A: 今日印刷しなきゃいかんのか?

B: はい! どうしよう、困ったな

A: 私がやっておいてやるからさっさと行って来い。

B: ありがとうございます! じゃっ!(ばたばたばた)

A: …本当にやれやれだな。さて、えー、ごほん。では読者のみなさま、問3の解説です。これはtrialsループをbreakで中断すれば達成できるので、ポイントはtrialsループを中断するためにはどこにbreakを置くべきかを判断できるかどうかです。第9章の図9-11を見ると、trialルーチンのBegin RoutineやEnd Routineにbreakを置くと、trialsループを構成するforをbreakすることが出来るのがわかります。ですから、Begin RoutineやEnd Routineにbreakを置けばtrialsループを中断することが出来ます。以下のコードをtrialsルーチンのBegin Trialsに書けば達成です。

if globalClock.getTime() > limitTime:
    break

第10章: リストと乱数に慣れる

A: さて、プリンタ用紙もセットしたし、最後の第10章の練習問題に進みましょう。課題は3つ。第1にOpacityに値を指定することで不要な刺激を消すこと。第2に刺激の位置を無作為にずらすこと。第3に刺激の位置を記録ファイルに出力すること。まずOpacityの操作ですが、これはpsyexpファイルをエディタでいじってimage00からimage14のOpacityをopac[i] (i=0~14)というリストで指定できるようにしましょう。それが出来たら、trialルーチンに置いてあるCodeコンポーネントのBegin Experimentに以下のようにopac[]の初期化コードを追加します。初期値は1.0でいいでしょう。

imagefile = []
pos = []
ori = []
opac = []
posindices = range(36)
for i in range(15):
    imagefile.append('o.png')
    pos.append([0,0])
    ori.append(0)
    opac.append(1.0)

A: さらにBegin Routineでopacに1.0または0.0を指定していきます。第10章本文で紹介した方法で、posに大きな値を与える代わりにopacに0.0を指定すればOKです。ここでついでに刺激の位置を無作為にずらす処理もしてしまいましょう。10*randint(0,4)-15で-15、-5、5、15の値が無作為に得られますから、この乱数をposのX成分、Y成分に足せばよいだけです。

for i in range(15):
    if i==0:
        imagefile[i]=firstItem
    else:
        imagefile[i]=otherItems
    if i<numItems:
        xoffset = 10*randint(0,4)-15
        yoffset = 10*randint(0,4)-15
        pos[i] = [100*(posindices[i]%6-3)+50+xoffset,
                     100*(posindices[i]/6-3)+50+yoffset]
        opac[i] = 1.0
    else:
        opac[i] = 0.0
    ori[i] = randint(0,4)*90

A: これでOpacityの問題と位置ずらしの問題はクリア。後は刺激位置の記録ですが、これはもう第10章にほとんど答えが書いてありますね。trialルーチンのEnd Routineで以下のようにaddDataするだけです。このpos[:numItems]というスライスをぱっと見ただけで理解できる、必要な時にぱっと思いつくようになりたいところですね。

trials.addData('StimPos',pos[:numItems])

A: さて、以上で解説は終わりですが、最後にひとつ。これは第6章で補足しておくべきだった話題ですが、筆者が第6章を書いている時に、pythonで複数行にわたる文字列を書くことが出来るのを忘れていたようです。第6章では

$u'「'+expInfo['word']+u'」は人の顔を形容する言葉です。
呈示された顏の絵が当てはまるならカーソルキーの左、
当てはまらないなら右を押してください。'

A: とTextに書くとエラーになることについて触れ、このような複数行にわたる文字列は認められないので

$u'「'+expInfo['word']+u'」は人の顔を形容する言葉です。\n'+
u'呈示された顏の絵が当てはまるならカーソルキーの左、\n'+
u'当てはまらないなら右を押してください。\n'

A: と言う具合に分割して + 演算子でつなぎましょう、と解説しました。まあこれはこれで動作するのでいいんですが、Pythonには複数行にわたる文字列を表現する方法があり、そいつを使うともっとすっきり書くことが出来ます。具体的には、'''文字列'''とか、"""文字列"""という具合に、シングルクォーテーションまたはダブルクォーテーションを3つ並べて文字列を囲みます。第6章の文だとこのような感じですね。

$u'「'+expInfo['word']+u'''」は人の顔を形容する言葉です。
呈示された顏の絵が当てはまるならカーソルキーの左、
当てはまらないなら右を押してください。'''

A: まああまり次から次へと新しいテクニックを紹介すると解説が長くなりすぎてしまうので、第6章はあれでよかったかなと思いますが、第6章のトピックくらいでは触れておくべきでしたね。というわけで、第10章まで練習問題を解説しました。読者の皆さんの参考になりましたら幸いです。ちょうどB君の資料の印刷も終わったようだし、今日はこの辺りで。B君の資料は机の上に置いておけばいいかな。今日はもう帰らないと。さようなら~。

------------------------------ 1時間後 ------------------------------

研究室の扉の前に、結局卵を買い損ねて資料を取りに戻ってきたら研究室の鍵を研究室の中に忘れているのに気付いて、呆然と立ち尽くすB君の姿がありました。

おしまい。