.. title:: Pythonで心理実験 - 例題1-5 例題1-5:線を引いたら完成だ ============================== **B:** いよいよ今回で完成ですね。 **A:** 細かいところに気を遣いだすと、まだまだいじれるところはたくさんあるんだけどね。 でも、とりあえず実験するだけならこれで十分なはず。これが今回のサンプルプログラムだ。 + 行番号なしのソースファイルをダウンロード→ `01-5.py `_ .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** 今回の山場はVisionEgg.MoreStimuli.Target2Dを使って線を引くことだな。逆に言うとそれ以外はほとんど新しい事がない。 VisionEgg.MoreStimuliにはArrow, FilledCircle, Rectangle3D, Target2Dといったクラスが含まれている。それぞれ矢印、塗りつぶした円、3Dの塗りつぶし長方形、2Dの塗りつぶし長方形の刺激を表している。 **B:** なんで3DはRectangleで2DはTargetなんですか? **A:** うーん、私も不思議に思ってるんだけどね。Target2Dは長方形を描くクラスなんだけど、すごく細長い長方形を描けば線を引く代わりに使える。 Target2Dの引数には以下のようなものがある。 .. csv-table:: :delim: $ anti_aliasing$アンチエイリアスを施すか否かを決める。指定しなければTrue(=アンチエイリアスを行う)。 color$色を指定する。赤、緑、青の成分を0~1の実数で指定する。指定しなければ(1.0, 1.0, 1.0)となる(白色)。なお、4つの実数を指定すると、4つめは透明度として解釈される。 on$刺激を描画するか否かを決める。指定しなければTrue(=描画する)。 orientation$刺激の回転角を実数で指定する。単位は度。指定しなければ0.0。 position$描画の基準点を決める。Viewportでprojectionを指定していなければ、スクリーンの座標と一致する。指定しなければ(320.0, 240.0) anchor$positionで指定した座標が図形のどの位置と一致するかを指定する。指定しなければcenter。 size$刺激の大きさを指定する。Viewportでprojectionを指定していなければ、スクリーンの座標と一致する。指定しなければ(64.0, 16.0)。 **B:** positionやanchorはVisionEgg.Text.Text()でも出てきましたね。 **A:** 感心感心。よく覚えてるな。こいつを使って、どうすればターゲット語の下に線を引けばいいか考えていこう。 「それは、ゆれながら水銀のように光って上に上がった」という文を例とする。この文のターゲット語は「水銀」だ。「水銀」の最初の一文字は文全体の何文字めに出てくる? **B:** 要するに「水」っていう字が何文字めに出てくるかですよね。1, 2, …。あの、「、」は数えるんですか? **A:** もちろん。 **B:** 数えるのなら、ええと…。10文字めですかね。 **A:** OK。文の左端から数えて10文字めの左端から、ターゲット語の文字数分だけ線を引けば目的達成ということになる。 **B:** そうですね。 **A:** よし。じゃあ続いて、文を表示するためのVisionEgg.Text.Textオブジェクトをstimobj、下線を表示するためのVisionEgg.MoreStimuli.Target2Dオブジェクトをlineobjとしよう。 これらのanchorをleftに指定してやれば、下の図のようにstimobjとlineobjを配置してやればいいことになる。 .. figure:: img/01-5-01.png **B:** むむ。急に難しくなってきたような。 **A:** ここでのポイントは、10文字めから線を引くためには、stimobjのanchorとlineobjのanchorが9文字分離れていないといけないことだ。 慣れないうちは頭の中で考えるだけでは間違えやすいから、こうやって図に描いてみるといいぞ。 **B:** ふーむ。文字の幅ってのはどうすればわかるんですか? **A:** ちょっとややこしいんだが、等幅のフォントを使っていること。Viewportを設定する時にprojectionを指定していないこと。 この2点をクリアしていれば、VisionEgg.Text.Text()の引数sizeに指定した値がそのまま一文字の幅になる。 **B:** 等幅のフォント? **A:** すべての文字が占有する幅が等しいフォントを等幅のフォントと言うのさ。ワープロとかで文字の位置をそろえやすいというメリットがあるけど、 実際の文字の幅はひとつひとつ異なるから、文字間隔が広すぎたり狭すぎたりして正直なところあまりきれいじゃない。特に英文では大文字のWと小文字のiが同じ幅って無理があるだろう? **B:** あー。確かに。 **A:** そこで、文字ひとつひとつに幅を設定してフォントが使われるようになった。これをプロポーショナルフォントと言う。 WindowsのMicrosoft Wordを使っているんだったら、MS明朝とMS P明朝というフォントを知っているだろう? **B:** あ、ありますあります。見比べたら違うのはわかるんですが、正確には何が違うのかよくわかんなかったんですよ。 **A:** MS明朝は等幅フォント、MS P明朝はプロポーショナルフォントなのさ。多分PはプロポーショナルのPなんだろうね。 下の図のように並べてみるとよくわかる。等幅では大文字のAと小文字のiを同じ幅にするために、Aは細くなってiは左右の文字間隔が広くなってるよね。 .. figure:: img/01-5-02.png **B:** おおー。長年の謎が。 **A:** さて、下線を引く話に戻るわけだが。プロポーショナルフォントはきれいだけれど、今回のプログラムには都合が悪いって事はわかるか? **B:** 文字によって幅が違うから、単純に文字数で計算できないってことですよね。 **A:** その通り。今回は快調だね。サンプルプログラムでは、同じく等幅のフォントであるMSゴシックを使っている。 87行目、90行目のfont_name=r'C:\\Windows\\Fonts\\MSGTHIC.TTC'という引数がそうだね。 .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 86-91 **B:** ん? 'の前についているrは何ですか? uはunicodeって教えてもらいましたが、rは聞いてないような。 **A:** 細かいところまできちんと見ているな。\\記号はエスケープ文字といって、文字列の中では特別な意味を持つ。 だから、文字列の中で\\を使いたい時は特別な書き方が必要なんだ。それはまた機会があれば説明するけど、ここの例のように文字列の先頭にrをつけておけば、 \\を特別な文字として解釈しないというご利益がある。 **B:** むむむ。よくわかりません。 **A:** 文字列はいずれきちんと解説しないといけないなあ。まあ、今回はそんなもんだと思っといてくれ。 ここのもう一つのポイントは、font_size=fontSizeという引数だ。これはText()にしかない引数で、一文字のサイズを指定している。fontSizeは83行目で24という値が代入してあるね。 **B:** ん? じゃあfont_size=24って書けばいいんじゃないんですか? **A:** まあそれでも動くんだけど、 **このように書いておくと、しばらく後で自分が書いたプログラムを読み返した時に、24って果たして何の数字だったかな?と悩まなくて済む。 さらに重要なのは、このように書いておくと、後でもうちょっと文字を小さくしたいとか、大きくしたいとか、そういう風に思った時に83行目だけを書き直せば済むんだよ。 今回のサンプルプログラムでは、この後で見ていくように、フォントサイズを使った計算があちこちの行に繰り返し出てくる。こういう複雑なプログラムを書く時には、非常に重要なことなんだ。** **B:** おお、なんか力入ってますね。 **A:** こういう事の積み重ねがプログラムの可読性を高めるのさ。可読性が高いプログラムは再利用しやすい。 再利用できるプログラムはプログラマにとって財産だからね。 **B:** はあ。 **A:** ふに落ちない顔をしてるな。解説を続けるぞ。これで文字列と下線の位置関係は解決したけど、もうひとつ問題が残っている。 それは文字列の左端をスクリーンのどこへ配置すればいいか、だ。 **B:** へ? 今まで通りじゃだめなんですか? **A:** 今までは文字列のanchorをcenterにして、(512,384)という位置に設定していた。これは1024x768の画面ではちょうど真ん中にあたる。 スクリーンの真ん中にanchorをcenterにして配置していたので、今までのサンプルでは画面中央に文が表示されていた。けれども、今回は文字列のanchorをleftにしているんだから、 文字が画面の右側に寄ってしまう。 **B:** あ、そうか。 **A:** まあ、左端の座標を100くらいに固定してしまって、左揃えで文を表示するのも一つの選択肢だ。 けれども今回は練習も兼ねて今まで通り画面中央に文を表示する事を考える。 **B:** 左揃えでいいじゃないですか…。 **A:** ん? 何か言ったか? 等幅フォントを使っていればそんなに難しい事じゃない。文全体の文字数を数えて、文字幅をかけて2で割ればいいのさ。 その値だけスクリーン中央から左へanchorをずらしてやれば目標達成だ。文字数はプログラムで簡単に数えられる。 .. figure:: img/01-5-03.png **B:** あ、思ったより簡単ですね。 **A:** だろ? 今までのプログラムでは、スクリーンの大きさを1024×768に決めつけて書いていたんだけど、今回はプログラム中でスクリーンの大きさを調べる方法も紹介しておこう。 20行目がそうだ。 .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 20 **B:** これだけでいいんですか? **A:** いいんです。 **B:** でも、=の左が気持ち悪いような。SXとSYの間のカンマは何ですか? **A:** ああ、これは=の右辺が複数の値を返す時には、=の左辺にカンマ区切りで変数を並べて書けば一気に代入出来るんだよ。 **B:** いろんな書き方があるんですね。混乱しそうだ。 **A:** 前にも言ったような気がするけど、自分がプログラムを書く時は自分が書きやすい方法で書けばいい。他人のプログラムを読んで勉強する時にはこういう書き方の知識も必要になるけど。 **B:** うーん。 **A:** 先へ進むぞ。これでSX、SYという変数にスクリーンの横幅、縦幅が代入されるので、SX/2がスクリーンの左右中心、SY/2が上下中心ということになる。 **B:** あの、/っていうのもちゃんと聞いていないような気が…。 **A:** ああ、/は割り算を示す記号だ。昔、タイプライタで÷が打てなかったので、代わりに/を使っていたのさ。前回掛け算の記号×は*と書くと言ったけど、それも同じ理由だ。 リーディングスパンテストの解説が終わったら、次はこのあたりをきちんと説明した方がいいな。 **B:** お願いします。 **A:** さあて、これで基本になる考え方の説明はおしまい。後はこれをpythonのプログラムとして表現しなければいけない。 まずは、すべての課題文について、ターゲット語が何文字めから始まって、何文字あるか数えてもらおうか。 **B:** えー。ぼくが数えるんですか。 **A:** そもそもB君のためにやってるんだから自分で数えなさい。 **B:** うへーい。 *------------------------------ 数分経過 ------------------------------* **B:** 数えてきましたよ! **A:** お、早かったな。さて、このデータをプログラムに組み込まないといけないわけだが、ひとつひとつの課題文について「課題文」、「ターゲット語の開始位置」、「ターゲット語の長さ」という情報が存在する。 このような情報をプログラム上で表現する時には、多重リストを使用すると良い。つまり、 [課題文1, 課題文1のターゲット語の開始位置, 課題文1のターゲット語の長さ] というリストを56個の課題文に対して作成し、それを **[** [課題文1, 課題文1のターゲット語の開始位置, 課題文1のターゲット語の長さ] **,** [課題文2, 課題文2のターゲット語の開始位置, 課題文2のターゲット語の長さ] **,** ... [課題文56, 課題文56のターゲット語の開始位置, 課題文56のターゲット語の長さ] **]** という具合にさらにリストでくくる。カンマや[ ]の位置に注意してほしい。このリストをsentenceListという変数に保存しているとすれば、n番目の課題文に関する情報は以下のようにアクセスできる。 多重リストの要素へのアクセスは、例題1-2の時に説明したようにvar[0][0]という感じで[ ]を続けて書く。覚えているかな? :: sentenceList[n-1][0] # n番目の課題文 sentenceList[n-1][1] # n番目の課題文のターゲット語の開始位置 sentenceList[n-1][2] # n番目の課題文のターゲット語の長さ **B:** ええっと、n-1? **A:** pythonのリストは最初の要素を0番目と数えるから、最初の課題文を「課題文1」と呼ぶならば、課題文1にアクセスするためには0番目、課題文2にアクセスするためには1番目の要素にアクセスしないといけない。 **B:** ややこしいなあ。 **A:** こればっかりは慣れてもらうしかないな。さて、このようにして作成したリストがサンプルプログラムの22行目から77行目だ。 **B:** ふむふむ…って、ぼくが必死で数えたターゲット語の開始位置とか長さのデータがすでに入力されているじゃないですか! 数えなくてもよかったんじゃないですか! **A:** B君のためのプログラムなんだから、自分でも努力しなさい。さて、このプログラムでは課題文をランダムな順番に表示するための工夫もしている。80行目と81行目だね。 .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 80-81 **B:** ああ、前回のサンプルプログラムで書くだけ書いといて使わなかった部分ですね。 **A:** …。まぁ、アレだ。前回説明したように、80行目で0から始まって長さがsentenceListと一致する整数のリストを作成し、81行目でランダムな順番に並び変えている。 このリストに格納された数の順番に課題文を表示していくんだ。例えば[37,2,15,...,49]と並び変えられた場合は、課題文38、課題文3、課題文16…と順番に表示されて、最後は課題文50で終わる。例によって1ずれていることに注意ね。 **B:** えっと、良く分かりませんが具体的にどうプログラムに書けばいいんですか? **A:** 最初の試行を0番目と数えるとして、m番目の試行ではsentenceIndex[m]に格納された値の課題文を使うということさ。だから、 :: sentenceList[sentenceIndex[m]][0] # m番目の試行の課題文 sentenceList[sentenceIndex[m]][1] # m番目の試行の課題文のターゲット語の開始位置 sentenceList[sentenceIndex[m]][2] # m番目の試行の課題文のターゲット語の長さ という具合に書けばいい。試行数は0から数え始めているからm-1じゃなくていい事に注意。 **B:** うわっ、[ ]の中に[ ]が。 **A:** リストの要素を取り出す場合、[ ]の中は最終的に整数が得られる式であればなんでもいいんだよ。関数が入っていても構わない。さて、以上を踏まえていよいよ今回の核心部分だ。 .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 116-124 **B:** うーん、目がチカチカしてきた。 **A:** 一文ずつていねいに行こうか。まず116~117行目。ここでは課題文の左端の座標を計算している。順番に式を組み立てていくと :: sentenceList[ sentenceIndex[currentSentence]][0] # currentSentence番目の試行の課題文 len( # 課題文の長さ(文字数) sentenceList[ sentenceIndex[currentSentence]][0] ) len( # 課題文の長さを2で割り、文字幅を乗じて、 sentenceList[ # スクリーン左右中心から文字列の左端までの sentenceIndex[currentSentence]][0] # 距離を計算 )/2*fontSize leftend = SX/2-len( # スクリーン左右中心から文字列の左端までの sentenceList[ # 距離を引いて左端の座標を計算し、 sentenceIndex[currentSentence]][0] # 変数leftendに格納 )/2*fontSize という具合だ。さっきの図を思い出しながらよーく考えてほしい。 **B:** むむむ…。あれ、116~117行目はSXの左とfontSizeの右にもカッコがありますが、これは? **A:** 式が長すぎて一行に収まらないから二行に分けて書いているんだけど、分けて書くためには( )や[ ]などが閉じないまま次の行へ継続する必要があるんだ。 まあ解説ページ作成のための工夫だから、カッコは外してこのように一行に書いてもまったく同じ動作だ。 :: leftend = SX/2-len(sentenceList[sentenceIndex[currentSentence]][0])/2*fontSize **B:** なるほど。 **A:** カッコをつけるとかえってややこしい場合は、行末に半角の\\をつけて文が継続している事をpythonインタプリタに教えることもできる。 カッコとどちらを使うかは好みの問題かな。あ、フォントによって\\は¥と表示されたり\と表示されたりするから注意してね。 :: leftend = SX/2- \ len(sentenceList[sentenceIndex[currentSentence]][0])/2 \ *fontSize **A:** さて、本題に戻ろう。変数leftendに文字列の左端の座標が入っているのだから、これを課題文を表示するためのVisionEgg.Text.Textインスタンスであるstimobjのpositionに代入すればいい。119行目だね。SY/2というのはY座標の指定で、スクリーンの上下中心に対応している。 118行目はわかるね? **B:** えっと、表示する課題文をstimobjのtextに設定しています。 **A:** よろしい。続いて120行目から124行目は下線の設定だ。下線を表示するためのVisionEgg.MoreStimuli.Target2Dインスタンスがlineobjに代入されているんだが、120~121行目は何をしているかわかるか? **B:** =の左がlineobj.parameters.sizeだから、これは下線の幅を決めているんですかね。 **A:** その通り。この式の右辺を順番に組み立てていくとこうなる。 :: sentenceList[ # currentSentence番目の試行の課題文の sentenceIndex[currentSentence]][2] # ターゲット語の長さ fontSize * sentenceList[ # ターゲット語の長さに文字幅を乗じて sentenceIndex[currentSentence]][2] # 下線の幅を計算 (fontSize * sentenceList[ # 長方形の幅として下線の幅、高さとして sentenceIndex[currentSentence]][2],2) # 2を指定する **B:** ふむふむ。この式の両端の( )も二行に分けて書くためのものですね? **A:** う。残念ながら違う。まだ説明していなかったけど、それは **タプル** と言って、X座標とY座標の値をひとまとめにするために使われているんだ。 **B:** タプル? **A:** リストと同じように複数の値をまとめて一つの変数に格納するための仕組みなんだが、ここでリストとタプルの違いを説明するのは横道に逸れ過ぎるなあ。 リストと同じように使えるけど、リストと違って後から要素を書きかえる事が出来ないって覚えておいてもらえば当面は大丈夫かな。 **B:** 書きかえる事が出来ない? **A:** リストならa = [1,2,3]とした後a[1]=5とすればaは[1,5,3]になる。しかしタプルの場合b = (1,2,3)としてタプルを作成すると、b[1]=5とすると「タプルの要素は書きかえられません」っていうエラーが出て怒られる。 **B:** そんなの不便なだけじゃないですか。 **A:** タプルにはタプルの利点があるのさ。まあ、今回のサンプルプログラムの場合はリストでもタプルでも、どちらでも動く。だから120~121行目の右辺は[ ]でくくっても良い。 ただし116~117行目の( )はタプルじゃないから[ ]で置き換えちゃだめだぞ。 **B:** うー。頭が混乱してきました。 **A:** まあ、たぶん心理実験のプログラムを書く程度なら、迷う時はリストにしておけば大丈夫だろう。タプルじゃないと使えない関数があるけど、それはその都度説明する。 **B:** うー、うー。 **A:** 先へ進もうか。122行目から124行目では下線の左端の位置を計算している。=の右辺を順番に組み立てていけばこうだな。 :: sentenceList[ # currentSentence番目の試行の課題文の sentenceIndex[currentSentence]][1] # ターゲット語の開始位置 fontSize * sentenceList[ # 文字幅を乗じて文字列左端から sentenceIndex[currentSentence]][1] # 下線左端までの距離を計算 leftend + fontSize * sentenceList[ # 文字列左端の座標を加えてスクリーン sentenceIndex[currentSentence]][1] # 上における下線の左端の座標を計算 (leftend + fontSize * sentenceList[ # Y座標を計算してタプルにまとめる sentenceIndex[currentSentence]][1], SY/2-fontSize/2) **A:** 先の図とよーく見比べれば、わかるんじゃないかと思う。最後のY座標を計算する時にfontSize/2を引いているのは、こうしてやらないと 下線じゃなくてターゲット語の上に打ち消し線のように線が重なってしまうからね。文字の高さの半分だけずらしてやると、ちょうどターゲット語の真下になる。これで完成だ。 **B:** ふーぅ。最後は混乱しましたが、なんとなく感じはつかめたような。 **A:** leftendを加えるとかfontSize/2を引くとか、そういう部分を自分で削ったりしてプログラムを動かしてみたら、もっとよく理解できると思うよ。ぜひ後でやってみるといい。 あと解説しないといけないのは刺激の表示のオンオフ切り替えかな。これにはVisionEgg.Core.Stimulusクラスのonパラメータを使う。 **B:** 「使う」って、どうやって使うんです? **A:** 最初にVisionEgg.MoreStimuli.Target2Dの引数の表を示したけど、そこにonパラメータの説明があったのは気付いていたかな。 on = Trueとすると刺激は画面に描画されるが、on = Falseとすると刺激は画面に描画されない。だから、被験者に見せたくない時にはFalseを設定して、ここぞという時にTrueを設定すればいいんだ。 サンプルプログラムでは113~114行目に具体的な書き方の例が出ている。 .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 113-114 **B:** textobjが表示されなくて、stimobjとlineobjが表示されるという事ですね。 **A:** そう。onパラメータの説明のついでにVisionEgg.Core.Viewport()の引数stimuliもおさらいもしておこうか。 .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 95 前回のサンプルプログラムでは、刺激はtextobjひとつしかなかった。しかし、今回はtextobj、stimobj、lineobjという三つの刺激があるので、このようにリストとしてstimuliに指定しなければいけない。 逆に言うと、このようにViewport()のstimuli引数に登録されない限り、いくら刺激を作成しても画面に描画されない。あまり考えずにプログラムを書いた時にありがちなミスだ。 **B:** つまりAさんはそういうミスをたくさんしてきたんですね? **A:** えー。あー。その、なんだ。もうひとつ注意すべき点は、このリストに並べられた順番に描画が行われるということだ。 つまり、[textobj,stimobj,lineobj]という順に並んでいるから、まずtextobjを描き、続いてstimobj、最後にlineobjを描く。もちろんonパラメータがFalseなら描画は飛ばされる。 **B:** ??? 順番に何か意味があるんですか? **A:** 今回のサンプルプログラムの場合は大した意味はないが、刺激を重ねて描く場合には大いに意味がある。例えば長方形の下から円がちょっとはみ出しているような刺激を描く場合だな。 その場合、円を先に描いてその上から長方形を描かなければならない。 **B:** あー。実験によってはそういう事はありそうですね。 **A:** 今回のサンプルプログラムでも、わざとstimobjとlineobjが重なるように配置すれば簡単に確認する事が出来るぞ。これは練習問題にしておこう。 これですべて解説終了、かな? **B:** おおー。長い道のりだった。 **A:** わからないところが残っていないか、確認してごらん。 **B:** えーとですね。えーと。えーっと…。あ。 **A:** 何かあったか? **B:** 84行目。leftendは課題文が変わる毎に計算するのに、なぜここで100を代入しているんでしょうか。100なんて値、使いました? .. literalinclude:: source/01-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: :lines: 84 **A:** あー。これはプログラムを作成する途中で、とりあえずleftendを100に決め打ちしてあれこれ動作テストしていた名残だな。 あともう一つ、私はC言語でプログラミングを覚えたから、後で使う変数は何となく宣言しておきたいという癖もある。 **B:** C言語の癖、ですか。 **A:** pythonに限らず、多くの言語ではvarという変数を使いたいと思ったら、いきなりvar = 100とかいう具合に使う事が出来る。 しかも、今までvar = 100って具合に整数を入れていたのに、次の瞬間に何食わぬ顔でvar = 'Hello, World'などと文字列を代入できたりする。 **B:** へ? それが当り前じゃないんですか? **A:** C言語では、変数は使う前に「varという変数を使いますよ」と宣言しなければいけない。しかも、「varには整数を格納しますよ」と 格納するデータの型も指定しなければいけない。 **B:** げー。不便ですね。 **A:** まあ不便と言えば不便だな。その代り、varへ代入するつもりでタイプミスしてbarへ代入してしまう、なんてミスはC言語では起こらない。 varとbarの両方の変数を宣言していたら話は別だが。他にも、変数の型が指定されている方が、機械語に翻訳する時にコンパクトで高速なコードに変換できる。 **B:** うーん、あまりピンと来ませんが、そうなんですね。 **A:** C言語が高速な理由は、pythonのように毎回翻訳しながら動かすのではなく、コンパイルと言う作業を通じて機械語に翻訳してしまってから動かすからというのもある。 まあ、いずれにしてもC言語にはC言語の強みがあり、pythonにはpythonの強みがある。 **B:** 使い分けが大事なんですね。 **A:** そうだな。まあ、話が逸れてしまったが、プログラム上重要な変数はプログラムのどこかにまとめて書いとくと、忘れずに済むという利点もあるので、 私は個人的な好みでこういう書き方をしている。繰り返し言っていることだけど、B君はB君が書きやすいように書けばいい。 **B:** うーん、どっちがいいのかよくわかりません。 **A:** そりゃ、自分でいろいろ書いてみないとわからんさ。またわからない事があったら教えてやるから、とにかく自分でやってみなさい。それでは、「リーディングスパンテストを作ってみる」企画はこれにて終了~。 **B:** ありがとうございましたー。