基本情報技術者試験 Pythonサンプル問題

基本情報技術者試験 Pythonサンプル問題をみてみましょう。実物は以下です。
https://www.jitec.ipa.go.jp/1_13download/fe_python_sample.pdf

1.問題文:情報技術者試験(FE)午後試験 Pythonのサンプル問

問 Pythonのプログラムに関する次の記述を読んで,設問1,2に答えよ。

命令列を解釈実行することによって様々な図形を描くプログラムである。

(1)描画キャンバスの座標は,x軸の範囲が-320~320,y軸の範囲が-240~240である。描画キャンバスの座標系を,図1に示す。描画キャンバス上にはマーカがあり,マーカを移動させることによって描画する。マーカは,現在の位置座標と進行方向の角度を情報としてもつ。マーカの初期状態の位置座標は(0,0)であり,進行方向はx軸の正方向である。

f:id:black_pupil:20201122224224j:plain
   図1 描画キャンバスの座標系

(2)命令列は,命令を";"で区切った文字列である。命令は,1文字の命令コードと数値パラメタの対で構成される。命令には,マーカに対して移動を指示する命令,マーカに対して回転を指示する命令,及び命令列中のある範囲の繰返しを指示する命令がある。繰り返す範囲を,繰返し区間という。命令は,命令列の先頭から順に実行する。命令とその説明を,表1に示す。

   表1 命令とその説明
f:id:black_pupil:20201122224953j:plain

(3)命令列R3;R4;F100;T90;E0;F100;E0 (以下,命令列αという)の繰返し区間を,図2に示す。マーカが初期状態にあるときに,命令列αを実行した場合の描画結果を,図3に示す。
  なお,図3中の描画キャンバスの枠,目盛りとその値,①,②及び矢印は,説明のために加えたものである。

f:id:black_pupil:20201122225455j:plain
図2 命令列αの繰返し区間

f:id:black_pupil:20201122225522j:plain
図3 命令列αを実行した場合の描画結果

2.設問1

設問1(1)命令列αの実行が終了した時点でのマーカの位置は,図3中の【 a1 】が指す位置にあり,進行方向は【 a2 】である。

命令を順番に実行してみよう。
命令αは、命令2と3を4回繰り返した後で命令5を実行したものを、3回繰り返す。
プログラムで書いた方がわかりやすそうなので、命令αを書いてみると以下である。

import math
#座標位置をx,yとし、初期状態(x,y)=(0,0)とする。角度angleも初期値0
x,y,angle=0,0,0
#図を描くためにxとyの座標の推移を配列に入れる。初期値として、xとyを入れる
x_array=[x];y_array=[y]
#命令0により、以下を3回繰り返す
for i in range(3):
  #命令1により、以下を4回繰り返す
  for j in range(4):
    x = x + 100*math.cos(math.radians(angle))
    y = y + 100*math.sin(math.radians(angle))
    angle = angle +90
    x_array.append(x)
    y_array.append(y)
  #命令5により、同じ向きに100進める
  x= x + 100*math.cos(math.radians(angle))
import matplotlib.pyplot as plt
plt.xlim(-320,320)  #x軸の範囲指定
plt.ylim(-240,240)  #y軸の範囲指定
plt.grid() #罫線を引く
plt.plot(x_array,y_array)     #グラフの作成 (横軸の項目,縦軸の項目)
plt.show()  

わかりやすくないプログラムになってしまった。
f:id:seeeko:20201121091147p:plain

プログラムを理解して正解を考える。
命令1は、100進み(命令2)、90度回転して向きを変える(命令3)
その結果(0,0)→(100,0)→(100,100)→(0,100)→(0,0)と移動する。
命令5は100進むので、次は(100,0)からスタート
命令0によって、これが3回繰り返される。
最後は、(300,0)の位置になりa1の答えは②、向きは「x軸の正の方向」であり、これがa2の答えである。正解はウ

(2)マーカが初期状態にあるときに,図4に示す1辺の長さが100の正五角形を描くことができる命令列は,【 b 】である。

正五角形を書くので、100進むFを5回実行し、角度は360÷5=72なので、Tは72であることが、感覚的にわかるのではないか。
結果的に、R5;F100;R72;E0 正解はカ

ここで,図4中の描画キャンバスの枠,目盛りとその値は,説明のために加えたものである。

f:id:black_pupil:20201122230301j:plain
   図4 1辺の長さが100の正五角形

aに関する解答群
f:id:black_pupil:20201122230318j:plain

bに関する解答群
 ア R5; F100;T-108;E0  イ R5;F100;T-75;E0
 ウ R5;F100;T-72;E0   エ R5;F100;T-60;E0  
 オ R5;F100;T60;E0   カ R5;F100;T72;E0
 キ R5;F100;T75;E0    ク R5;F100;T108;E0


〔プログラムの説明〕
(1)関数parseは,引数として与えられた命令列を,ダブルを要素とするリストに変換する。ここで,命令列は,少なくとも一つの命令をもち,誤りはないものとする。1タプルは,1命令に相当し,命令コード及び数値パラメタから構成される。関数parseが定義された状態での,対話モードによる実行例を,実行結果1に示す。

実行結果1
>>>parse('R4;F100;T90;E0')
[('R', '4'), ('F', '100'), ('T', '90'), ('E', '0')]

(2)クラスMarkerは,マーカの現在の位置座標を属性x,yに,進行方向をx軸正方向から反時計回りに測った角度で属性angleに保持する。オブジェクトの生成時に,描画キャンバスの表示範囲を設定し,属性x,yを0,0に,属性angleを0に設定する。クラスMarkerに,マーカの操作をする次のメソッドを定義する。

forward(val)
 マーカの位置座標を,現在の進行方向にvalで指定された長さだけ進め,線分を描く。
 引数:val 長さ
turn(val)
 マーカの進行方向を,反時計回りにvalで指定された角度だけ回転させる。
 引数:val 度数法で表した角度

(3)関数drawは,引数として与えられた命令列の各命令を解釈実行し,描画結果を表示する。ここで,命令列は,少なくとも一つの命令をもち,誤りはないものとする。関数drawの概要を,次に示す。
 ① 命令列を,関数parseを利用してタプルを要素とするリストに変換する。
 ② マーカの操作は,クラスMarkerを利用する。
 ③ 繰返し区間の入れ子を扱うために,スタックを用いる。
 ④ スタックはリストで表現され,各要素は繰返しの開始位置opnoと残り回数restをもつ辞書である。
 ⑤ プログラムの位置βにあるprint関数を使って,スタックの状態変化を出力する。

 2重の繰返し区間をもつ命令列について,関数drawが定義された状態での,対話モードによる実行例を,実行結果2に示す。

実行結果2
f:id:black_pupil:20201122232057j:plain

〔プログラム〕
f:id:seeeko:20201123161223p:plain
f:id:seeeko:20201123161240p:plain

3.プログラムの解説

最終形のプログラムは以下。最後にdraw('R4;F100;T90;E0')を実行している。

import math #数学関数の標準ライブラリ
import matplotlib.pyplot as plt #グラフ描画の外部ライブラリ


def parse(s):
  return [(x[0], int(x[1:])) for x in s.split(';')] 

class Marker:
  def __init__(self):
    self.x, self.y, self.angle = 0, 0, 0
    plt.xlim(-320, 320) # x軸の表示範囲を設定
    plt.ylim(-240, 240) # y軸の表示範囲を設定

  def forward(self, val):
    #度数法で表した角度を,ラジアンで表した角度に変換
    rad = math.radians(self.angle)
    dx = val * math.cos(rad) #valは長さ。x軸は、cosで計算できる
    dy = val * math.sin(rad) #y軸の座標はsinで計算できる。 
    x1, y1, x2, y2 = self.x, self.y, self.x + dx, self.y + dy
    # (x1, y1)と(x2, y2)を結ぶ線分を描画
    plt.plot([x1, x2], [y1, y2], color='black', linewidth=2)
    self.x, self.y = x2, y2

  def turn(self, val):
    self.angle = (self.angle + val) % 360

  def show(self):
    plt.show() #描画結果を表示

def draw(s):
  insts = parse(s)
  marker = Marker()
  stack = []
  opno = 0
  while opno < len(insts):
    print(stack)       # <b>←β</b>
    code, val = insts[opno]
    if code == 'F':
      marker.forward(val) 
    elif code == 'T':
      marker.turn(val) 
    elif code == 'R':
      stack.append({'opno': opno, 'rest': val}) 
    elif code == 'E':
      if stack[-1]['rest'] > 1:
        opno = stack[-1]['opno'] 
        stack[-1]['rest'] -= 1
      else:
        stack.pop()  # stackの末尾の要素を削除 
    opno += 1
  marker.show()

draw('R4;F100;T90;E0')

プログラムが長いので、部分的に解説をする。

(1)Markerクラスのforwardメソッドによる座標の計算

forwardメソッドで、座標を計算している。

import math #数学関数の標準ライブラリ

class Marker:
  def __init__(self):
    self.x, self.y, self.angle = 0, 0, 0 #変数の定義。self.をつける必要がある。これを付けないと、fowardのメソッドでこの変数が使えない。
    print('self=',self)  #<==== 追記 参考のため
    print('self.x=',self.x)  #<==== 追記 参考のため

  def forward(self, val):
    #度数法で表した角度を,ラジアンで表した角度に変換
    rad = math.radians(self.angle) #角度(angle)をラジアン(rad)に変換する。今回はself.angle=0なので、ラジアンも0
    dx = val * math.cos(rad) #valは長さ。x軸は、cosで計算できる
    dy = val * math.sin(rad) #y軸の座標はsinで計算できる。 
    x1, y1, x2, y2 = self.x, self.y, self.x + dx, self.y + dy 
    return(x1, y1, x2, y2)  #<==== 追記

marker = Marker()  #<==== 追記 Marker()クラスに、markerというインスタンスを作成
print(marker.forward(100))  #<==== 追記 markerというインスタンスのforwardメソッドに、100という引数を渡している

結果、以下が返ってくる

self= <__main__.Marker object at 0x7fefdffc5eb8>
self.x= 0
(0, 0, 100.0, 0.0)
(2)def draw(s)における、codeとvalの値について

プログラムを簡略化した上で、codeとvalに何がセットされるかを確認する。
以下を実行してみるとわかるように、命令コード(F,T,R,E)と数値パラメタ(長さや角度)である。

insts = [('R', 4), ('F', 100), ('T', 90), ('E', 0)]
opno = 0
while opno < len(insts):
  code, val = insts[opno] #==>opnoには、0,1,2,3が順に入る
  print('code=',code,'val=',val)
  opno += 1 #次のタプルに移動するために、+1する

結果は以下。

code= R val= 4
code= F val= 100
code= T val= 90
code= E val= 0
(3)def draw(s)のwhile opno < len(insts): のループ

draw('R2;R2;F100;T90;E0;F100;E0')で考える
stack[]という配列を作る
opnoは繰り返しのセットの番号(no)と考えよう。初期値は0(opno=0)
restは、繰り返しの残り(rest)回数(code == 'R'のときのvalが、初期値)
ループが繰り返される場合は、複数のopnoとrestのセットがスタックの形で加えられる。このとき、このセットの番号は+1される。
stack[-1]['opno'] は、stack[-1]でリストの一番最後を意味する。よって、リストの一番最後のopnoを確認する。

stackの値に注目しよう
(1)1回目のループ
最初の値:stack=[]※ブログだと半角が表示されないので
code == 'R'で、stack.appendで値をセット。'opno': opno(=0), 'rest': val(=2)  →これが、2回目のループの最初に表示される
(2)2回目のループ
[{'opno': 0, 'rest': 2}] ※前回のループのよる結果
次のcode == 'R'が読み込まれるので、stack.appendで値をセット。'opno': opno(は1が足されて1), 'rest': val(=2)
opnoは、このあと3になる。
(3)3回目のループ
[{'opno': 0, 'rest': 2}, {'opno': 1, 'rest': 2}] ※前回のループのよる結果
Fの処理。opnoは、このあと3になる。  
(4)4回目のループ
[{'opno': 0, 'rest': 2}, {'opno': 1, 'rest': 2}] ※前回のループのよる結果
Tの処理。opnoは、このあと4になる。  
(5)5回目のループ
[{'opno': 0, 'rest': 2}, {'opno': 1, 'rest': 2}] ※前回のループのよる結果
Eの処理。 stack[-1]['rest']=2なので、 > 1 、そこで、opno = stack[-1]['opno']として、1がセットされる。つまり、ループの先頭の番号にセットする。
また、stack[-1]['rest'] -= 1と1つ減算されて1になる。
opnoは1が足されて2になる。つまり、ループの次の命令に戻している。
(6)6回目のループ
[{'opno': 0, 'rest': 2}, {'opno': 1, 'rest': 1}]
opnoが2なので、あらためてFの処理がなされる。(この仕組み、よくできている。素晴らしい!!)
・・・
(8)8回目のループ
[{'opno': 0, 'rest': 2}, {'opno': 1, 'rest': 1}]
Eの処理。stack[-1]['rest'] > 1: の条件に当てはまらない(stack[-1]['rest']=1)ので、 stack.pop()により、stackリストの最後の盲目を削除する
(9)9回目のループ
[{'opno': 0, 'rest': 2}]
・・・

4.設問2

設問2 プログラム中の【  】に入れる正しい答えを,解答群の中から選べ。

実行結果が以下である。
>>>parse('R4;F100;T90;E0')
[('R', '4'), ('F', '100'), ('T', '90'), ('E', '0')]

プログラムは以下

def parse(s):
  return[(x[0],x[1:]) for x in s.split(';')] 

s='R4;F100;T90;E0'
print(parse(s)) #==> [('R', '4'), ('F', '100'), ('T', '90'), ('E', '0')]

念のため、型(type)を見ておこう

print(type(s)) #==>  <class 'str'>
print(type(parse(s))) #==> <class 'list'>
print(type(('R', '4'))) #==> <class 'tuple'> 参考まで

このように [('R', '4'), ('F', '100'), ('T', '90'), ('E', '0')]は、( )によるタプルが[ ]によるリストの中に入っている。
type(('R', '4')などの各要素はタプルで、全体としてはリスト型である。


ここで,d1とd2に入れる答えは,dに関する解答群の中から組合せとして正
しいものを選ぶものとする。dに関する解答群の中で使用される標準ライブラ
リの仕様は,次のとおりである。
math,sin(x)
 指定された角度の正弦(sin)を返す。
 引数: x ラジアンで表した角度
 戻り値:引数の正弦(sin)
math.cos(x)
 指定された角度の余弦(cos)を返す。
 引数: x ラジアンで表した角度
 戻り値:引数の余弦(cos)

cに関する解答群
 ア int(x[1])  イ int(x[1:])  ウ int(x[:1])
 エ int(x[2])  オ int(x[2:])  カ int(x[:2])

dに関する解答群
f:id:black_pupil:20201122233507j:plain

eに関する解答群
 ア 0,0        イ dx,dy
 ウ self.x,self.y     エ self.x - dx,self.y - dy

fに関する解答群
 ア 0      イ code
 ウ len(insts)   エ val

gに関する解答群
 ア < 0   イ < 1   ウ == 0
 エ > 0   オ > 1

h,iに関する解答群
 ア opno = stack[-1][' opno']
 イ stack, clear() # stackをクリア
 ウ stack. pop() # stackの末尾の要素を削除
 エ stack.pop(0) # stackの先頭の要素を削除
 オ stack[-1][' opno']=opno

3.解説

4.解答例

基本情報技術者試験(FE)午後試験 Pythonのサンプル問題 解答例
f:id:black_pupil:20201122234158j:plain

                  出題趣旨
 命令列の解釈実行を行うインタプリタを作成することは,プログラミング技法の習得の点でも,プログラムの動作原理を理解する点でも,意義深い取組である。
 本問は,簡易な描画処理の命令列を解釈実行することを主題としている。
 本問では,繰返しの入れ子構造をもつ命令列の解釈実行について問うことによって,スタックを用いて実装する能力を評価する。また,マーカの移動及び回転を指示する命令の実行について問うことによって,プログラム言語Pythonの外部ライブラリや,三角関数などの数値計算を行う標準ライブラリを活用する能力を評価する。