はじめに
IPA(FE試験の実施機関)が公表しているPythonのサンプル問題を解説してみました。誤りや分かりにくい箇所は順次訂正補足するので,質問やコメントいただければ幸いです。
Pythonの問題なのでPythonの知識はもちろん必要ですが,その他に中学校レベルの数学(図形)と,高校レベルの数学(三角関数)の知識が必要です。
設問1
空欄a1,a2
命令列α(R3;R4;F100;T90;E0;F100;E0)を実行後の、マーカの位置(空欄a1)と、進行方向(空欄a2)が問われています。
命令列αは,図2で示されたように二重の繰返し区間で構成されます。命令0(R3)で示される外側の繰返し区間を繰返し0(緑),命令1(R4)で示される繰返し区間を繰返し1(赤)とします。
命令列αを実行すると,最初に線分を描くのは命令2(F100)です。その後,命令3(T90)で反時計回りに90°回転します。これを命令1で4回繰り返すので,正方形を描きます(下図の赤点線)。その後,命令5(F100)で,すでに描かれた線分を上書きして,正方形の右下にたどり着きます。この正方形の右側にたどり着くまでが,命令0(R3)の繰返し1回分です(下図の緑点線)。
命令0の繰返しの2回目・3回目を実行すると正方形が一つづつ増えます。
命令列αの実行後のマーカの位置は,一番右の正方形の右下なので,図3の②の位置です(選択肢a1)。マーカの向きは右方向,つまりx軸の正方向(選択肢a2)です。したがって,選択肢ウ(①,x軸の正方向)の組み合わせが正答です。
解答:選択肢ウ(a1:②,a2:x軸の正方向)
空欄b
図4の正5角形を描く命令列が問われています。
命令列を考える前に,中学校レベルの数学の復習をします。多角形(今回は正五角形)の外角の和は360°である,という性質を思い出してください。正五角形の場合,外角は360°÷5=72°です。
図4の正五角形を描くには,100進んで72°反時計周りに回転するのを5回繰り返します。
100進む命令はF100,72°反時計周りに回転する命令はT72です。これを5回繰り返すにはR5とE0で囲えばよいので,命令列はR5;F100;T72;E0です。したがって,選択肢カが正答です。
設問2
空欄c
def parse(s):
return [(x[0], 空欄c ) for x in s.split(';')]
空欄Cは,関数parseの戻り値を答える穴埋め問題です。この設問では,問題文で示された関数parseの仕様を理解すること,理解した仕様をPythonの文法知識で実装すること,の2点が求められています。かなりの難題です。
問題文から関数Parseを理解する
関数Parseは,与えられた文字列(例えば'R4:F100:T90:E0')を解析(parse)して,プログラムが解釈しやすい形式に変換する関数です。問題文の該当箇所を示します。
「引数として与えられた文字列」とは,'R4:F100:T90:E0'です。タプルは4つあり,('R',4)と(’F',100)と('T',90)と('E',0)です。1タプル(例えば('R',4))が1命令にあたります。Rが命令コード,4が数値パラメタです。これらのタプル(4つ)をリストに入れて戻り値として返します。
文法
この設問に必要なPythonの文法知識は,以下の点です。
- 内包
- タプル
- リスト
- ループ
プログラムの該当箇所(parse関数)を示します。
def parse(s):
return [(x[0], 空欄c ) for x in s.split(';')]
returnの1行だけで,「引数で与えられた命令列を,タプルを要素とするリストに変換する」を処理します。複雑な処理ですが,一つ一つの要素に分解しながら理解しましょう。
命令の文字列として'R4:F100:T90:E0'が与えられたとします。この文字列は変数sに代入されています。
まず,splitです。splitは,文字列を分割してリストとして返す関数です。文字列sに対してsplit関数を使うと,こんなリストが返されます。区切り文字として';'を指定したので,R4とF100とT90とE0二分割されて,リストとして表示されます。
次は,for文です。このfor文は,「内包」という使い方をします。for文の処理対象xについて,x[0]と空欄cをタプルとして出力します。何を言っているか分からないと思うので,splitの結果である['R5', 'F100', 'T72', 'E0']を例に順に説明します。まず,シンプルな命令です。
これは,配列の内容をそのままリストとして返すのと同じです。一番左のxは,xをそのままリストの要素として出力するだけなので,結果としては同じリストが表示されます。これではあまり面白くありません。では,問題文にあわせてx[0]にしてみるとどうでしょうか?
x[0]は,変数x(文字列)の1文字目を表します。ですので,各要素の一文字目がリストとして返されます。
2文字目を表示したければx[1]なので,こちらも試してみましょう。
2文字目が表示されました。
問題文では,2文字目以降の値は「数値パラメタ」として扱います。ですので,2文字目以降を数字として取り出す方法を考えます。2文字目以降を取り出す場合にはx[1:]を利用します。最初の1は2文字目,:のあとに何も指定しない場合には「それ以降全て」を意味します。試しに内包を使ってx[1:]の結果がどうなるか試してみましょう。
無事に数字が取り出せました。ただ,この時点では文字列です。int関数を使って,数字の文字列を数値に変換します。
無事に数字を取り出せました。
では,先頭文字(x[0])と2文字目以降の数字をタプルとして返すように少しだけ変えてみます。
ということで,空欄cには選択肢イの「int( x[1:] )」が入ります。
空欄d1,d2,e
問題文では,クラスMakertのforwardメソッドについて以下の説明があります。
forward(val)
マーカの位置座標を,現在の進行方向にvalで指定された長さだけ進め,線分を描く。
引数: val 長さ
ということで,この設問は「線分の描き方」に関して問われています。高校レベルの数学の知識(三角関数の基本)が必要です。
前提として,self.xとself.yにはマーカの現在の位置座標が,self.angleには現在の進行方向の度数表示(90°とか180°など)が入っています。
forwardメソッドの動作を図にしたのが下図です。
赤い矢印が,描きたい線分です。左下の(x1,y1)を起点として,右上の(x2,y2)までの線分を描きます。
ここからプログラムの解説です。
rad = math.radians(self.angle)
この行では,変数angleに格納された値(度数法で格納された進行方向)を,弧度法に変換して変数radに格納します。度数法とか弧度法とは,角度の表記方法です。Pythonの数学ライブラリ(math.*)では,角度に弧度法を使うため,ここでmath.radiansメソッドによって弧度法(ラジアン表記)に変換しておきます。
dx = val * 空欄d1
dy = val * 空欄d2
上図内の線分のx軸方向の長さdxと,y軸方向の長さdyを求めます。三角関数の定義から,dx=val * cos(rad) ,dy = val * sin(rad)です。したがって,空欄d1にはmath.cos(rad)
,空欄d2にはmath.sin(rad)
が入ります。
空欄eは,x1とy1に代入する値が問われています。x1とy1は何かというと,その下の行にコメントで「線分を描画」と説明されているのがヒントです。線分の始点の座標がx1とy1です。
マーカの現在地は,変数self.xと変数self.yに格納されています。ですので,self.xとself.yが線分の始点になります。したがって,x1にはself.x,y1にはself.yが入ります。
空欄eの後に続くself.x+dx
とself.y+dy
も大きなヒントになります。dxとdyは,描く線分のx軸方向の長さと,y軸方向の長さでした。ですので,self.x+dxからdxを引いた値(つまりself.x)が線分の始点のx座標,self.y+dyからdyを引いた値(つまりself.y)が線分の始点のy座標と読み取ることもできます。
ということで,空欄eには選択肢ウの「self.x , self.y」が入ります。
空欄f
関数drawの穴埋め問題です。関数drawについては,問題文に説明があるので,プログラムと問題文を対応づけて読みます。
(3)関数drawは,引数として与えられた命令列の各命令を解釈実行し,描画結果を表示する。(略)関数drawの概要を,次に示す。
① 命令列を,関数parseを利用してタプルを要素とするリストに変換する。
② マーカの操作は,クラスMakerを利用する。
③ 繰返し区間の入れ子を扱うために,スタックを用いる。
④ スタックはリストで表現され,各要素が繰返しの開始位置opnoと残り回数restを持つ辞書である。
⑤ プログラムの位置βにあるprint関数を使って,スタックの状態変化を出力する。
注目する点は,①のinstsです。実行結果2の命令列「R2;R3;E0;E0」をparse関数の引数とした結果[('R',2),('R',3),('E',0),('E',0)]のリストが,instsに代入されます。instsはリストになるので,例えばinst[0]の値はタプル('R',2),inst[1]の値はタプル('R',3)となります。ですので,たとえばopnoが0(初期値)だった場合に code, val = insts[opno]
を実行すると,codeには’R',valには2が代入されます。つまり,codeに命令コード,valに数値パラメタが代入されます。
なお,opnoは「現在何番目の命令列を実行しているか」を示す変数です。
さて,空欄fです。
if code == 'F':
marker.forward( 空欄f )
elif code == 'T':
marker.turn( 空欄f )
elif code == 'R':
stack.append({'opno':opno, 'rest': 空欄f })
命令コード(code変数)が'F'や’T'や'R'の場合に,Markerクラスの各関数に渡す引数が問われています。さきほどの code, val = insts[opno]
で,codeと対になるのがvalだと判明しているので,比較的直感でも分かると思います。空欄fにはvalが入ります。
例えば命令コードが'F'の場合には表1で示されたとおり,数値パラメタ(val)で指定された長さの線分を描きます。関数forwardの説明にも「valで指定された長さだけ進め,線分を描く」とあり,forwardの引数にvalを指定すればよいことが分かります。(命令コード'T'にはturn関数が対応します)
したがって,空欄fは選択肢エ(val)が正答です。
空欄g,h,i
elif code == 'E':
if stack[-1]['rest'] 空欄g :
空欄h
stack[-1][['rest'] -= 1
else:
空欄i
命令コードが'E',つまり繰返しの終点の場合の処理が問われています。このif文をざっくり解説すると,
- if文の条件が真の場合(繰返しの最後ではない場合)には,繰返しの先頭に戻る。
- それ以外の場合(else)の場合,繰返しを終了させる
という意味です。
では詳しく見ていきましょう。stack[-1]は,リストstackの一番最後の要素を意味します。例えばstackの内容が[{'opno':0 , 'rest':2},{'opno':1,'rest':1}]
だった場合,stack[-1]は最後の要素である{'opno':1,'rest':1}
です。
stackの最後の要素(一番内側の繰返し)のrestが1より大きければ(つまり,2以上であれば),繰返しを行うためにopnoに繰返しの先頭の値(この場合,'opno':1,プログラム的にはstack[-1]['opno'])を代入します。この動作によって,命令列の実行位置を繰返しの先頭に戻します。ただし,それだけでは無限ループになってしまうので,stack[-1][['rest'] -= 1
によって繰返しの残り回数を1減らします。
restが1の場合には,それ以上の繰返しは必要ないので,繰返しの終了処理を行います。繰返しを終了させるには,リストstackの最後の要素を削除します。リストの最後の要素を削除するにはpop()を使います。
これらの動作を空欄に当てはめると,空欄gには選択肢オの「>1」が,空欄hには選択肢アの「opno = stack[-1]['opno']が,空欄iには選択肢ウの「stack.pop()」がそれぞれ入ります。
補足説明
内包の補足説明
内包とは,ある配列Bに対して,条件Cに合致する要素を取り出し,その要素に対して処理Aを行った結果をリストとして返す,という文法です。
[xに対する処理A for x in 配列B if xの条件C]
と書きます(全体を[]で囲うことに注意してください)。これは,配列Bのうち条件Cを満たす要素について,処理Aの結果をリストとして返す,と言う意味です。
例えば,0~9の偶数に対して,その偶数を2乗した値をリストとして返す書き方は以下です。
もちろん,配列を初期化し,forループの中にif文を書いて,配列にappendするやり方でも同じ事ができます。ただ,その方法だと4行程度は必要になってしまいます。これをたった1行で書けるのはかなり強力です。
ちなみに,空欄cは内包のうちif部分がない簡略化した使い方でした。
度数法と弧度法
中学までの数学では角度を度数法(90°など)で表記しますが,高校からの数学では弧度法(π/2など)になります。特に微分積分との相性がよいためです。単位は度数法の場合°,弧度法の場合[rad](ラジアン,と読む)を使います。
度数法と弧度法の変換は次式で行います。
1 [rad] = 180°/π
1° = π/180 [rad]
ややこしいので,代表的な値だけ覚えておけば大丈夫です。例えば,45°=π/4[rad],90°=π/2[rad],180°=π[rad],360°=2π[rad],などです。