コンテンツにスキップ

Sub-Python(PyLite)の構文

bot のコードを動かしているのは PyLite という、このゲームのために書かれた Python の処理系です。 本物の Python(CPython)ではなく、Python の一部だけを実装したものです。 このページでは「どこまで書けるか」と「なぜそこで線を引いたか」を説明します。

分類書けるもの
制御構造if / elif / else / while / for ... in range() / for ... in <リスト> / break / continue / pass
演算+ - * / // % **、比較(a < b < c のような連鎖も可)、and or not、in / not in、x if 条件 else y
代入= += -= *= /= //= %= **=、タプルアンパック(a, b = b, a)
変数ローカル変数、グローバル変数(global 宣言)
関数def(引数・戻り値あり。デフォルト引数はなし)、呼び出し側のキーワード引数(sorted(xs, key=f))
データ構造list、tuple、dict(リテラル・添字・スライス)、属性アクセス(state.me.x)
組み込みmath.*、len / min / max / abs / sum / range / sorted / int / float / round / str / bool / list / tuple / dict / enumerate / zip / print、ゲーム専用のヘルパー
メソッドlist: append / pop / insert / remove / index / count / reverse / sort
dict: get / keys / values / items / pop
str: upper / lower / strip / split / join / replace / startswith / endswith / find / count

state["me"]["x"] と state.me.x はどちらでも書けます。 辞書のかっこと引用符は初めて読む人には記号が多いので、点(.)でも読めるようにしてあります。

禁止されている構文は、実行時ではなく読み込みの時点で断られます。 import os と書いた bot は「走って途中で止まる」のではなく、そもそも始まりません。

書けないもの断る理由
import / from ... importサンドボックス
openサンドボックス
eval / execサンドボックス
__ で始まる名前サンドボックス
classサンドボックス/ブロックに変換できない
lambdaブロックに変換できない
リスト内包表記ブロックに変換できない
try / exceptブロックに変換できない
デコレータ、is、delブロックに変換できない

理由は大きく 3 つに分かれます。順に見ていきます。

理由 1:安全性(サンドボックス)

Section titled “理由 1:安全性(サンドボックス)”

CodeTank Arena では、他の人が書いた bot を自分の端末で動かします。 対戦相手が CPU であっても、将来ほかの人の bot と戦うようになっても同じです。 つまり「知らない人が書いたプログラムを、自分のブラウザで実行する」ことになります。

ふつうならこれはとても危ない話です。PyLite はそこを構造で解決しています。

bot のコードが JavaScript として実行されることは、一度もありません。 PyLite はソースを自前の構文木(AST)に読み替え、それをたどって解釈するだけです。 eval も Function も使いません。

そのうえで、bot から見える世界にはゲームのヘルパーと math しか置いていません。 globalThis、window、fetch にたどり着く名前が、そもそも存在しないのです。

import を許してしまうと、この前提が崩れます。 open でファイルを読めたら、eval で好きな文字列を実行できたら、__ 始まりの名前で 内部の仕組みに手が届いたら、その 1 つで囲いが破れます。 だからパーサの時点で拒否します。「あとで検査する」より確実だからです。

import os # ✗ import は使えません
import math # ✗ これも書けません
math.sqrt(2.0) # ○ math は import なしで、最初から使えます
math.pi # ○

import math まで断るのは意地悪に見えるかもしれませんが、理由があります。 math は「読み込むモジュール」ではなく、最初からそこにある道具として置いてあるからです。 import という入口を 1 つも開けないほうが、囲いは単純で確実になります。

理由 2:ブロックと行き来できること

Section titled “理由 2:ブロックと行き来できること”

CodeTank Arena では、ブロックで組んだものが Python になり、Python がブロックに戻ります。 画面を切り替えるたびに、コードは構文木から作り直されます。

このとき、ブロックで表せない構文があると往復が壊れます。 Python 側で書いた lambda がブロックに戻せないと、ブロック画面に切り替えた瞬間に消えてしまう。 これは「生徒が書いたコードをアプリが勝手に書き換える」ということで、起きてはいけません。

リスト内包表記や lambda を断っているのは、危ないからではなくこの往復を守るためです。 表現力としては、for 文で書けば同じことができます。

# ✗ リスト内包表記は使えません
dists = [distance(0, 0, p[0], p[1]) for p in points]
# ○ for 文で書きます
dists = []
for p in points:
dists.append(distance(0, 0, p[0], p[1]))

エラーメッセージも、この方針に合わせて「代わりにどう書くか」を返します。 たとえば内包表記なら「リスト内包表記は使えません。for 文で書きましょう」と出ます。

理由 3:決定論と、命令数バジェット

Section titled “理由 3:決定論と、命令数バジェット”

同じ試合はどの端末でも同じ結果になる必要があります(決定論)。 これを守るために、PyLite は次の点を保証しています。

項目PyLite でのふるまい
dict を回す順番入れた順を保証する
sorted / list.sort安定ソートを保証する(同じ値の並びが崩れない)
set未実装。list と in で代用する
乱数random は使えない。試合ごとにシードを固定した rand() / randint() / choice() を使う

set がないのは意地悪ではありません。集合は要素を回す順番が実装に左右されやすく、 順番が変わると試合の結果が変わってしまうからです。

時間で打ち切らず、命令数で打ち切る

Section titled “時間で打ち切らず、命令数で打ち切る”

無限ループの bot を止める必要があります。 しかし「1 秒経ったら打ち切る」という作りにはできません。 速い端末と遅い端末で、止まる場所が変わってしまうからです。それは決定論が壊れるということです。

そこで PyLite は、文や式をいくつ評価したかを数えます。

項目上限超えたら
update 1 回あたり150,000 命令その tick は前回の入力を維持
setup1,000,000 命令受け付けない
1 試合での超過の許容3 回4 回目で行動停止
リスト・文字列の要素数各 100,000MemoryError
再帰の深さ100RecursionError
ソースコードの長さ32 KB受け付けない

数え方は端末によらないので、どこで走らせても同じ場所で止まります。

なお 150,000 という上限はかなり余裕があります。 毎 tick A* で全体を探索しても収まる水準なので、 ふつうに書いていてバジェットで事故ることはありません。

共通テスト用プログラム表記との対訳

Section titled “共通テスト用プログラム表記との対訳”

大学入学共通テストの『情報Ⅰ』は、Python そのものではなく疑似言語で出題されます。 CodeTank Arena は同じコードを疑似言語の表示に切り替えられるので、 授業や試験対策でそのまま使えます。

概念Python共通テスト用プログラム表記
代入x = 1x = 1(同じ)
真偽値True / FalseTrue / False(同じ)
条件分岐if 条件:もし 条件 ならば:
 〃(2 つめ以降)elif 条件:そうでなくもし 条件 ならば:
 〃(どれでもない)else:そうでなければ:
反復(回数)for i in range(a, b):i を a から b-1 まで 1 ずつ増やしながら繰り返す:
反復(リスト)for p in points:p を points の要素それぞれについて繰り返す:
反復(条件)while 条件:条件 の間繰り返す:
反復を抜けるbreak繰り返しを抜ける
次の反復へcontinue次の繰り返しへ進む
論理演算and / or / notand / or / not(同じ)
整数の割り算//÷
表示print(n)表示する(n)
要素数len(points)要素数(points)
距離を求めるdistance(a, b, c, d)距離(a, b, c, d)
向きを求めるangle_to(a, b, c, d)向き(a, b, c, d)
戻り値return xx を返す
関数定義def 名前(引数):関数 名前(引数) を次のように定義する:
ブロックの範囲字下げ左の線 |。最後の行だけ ⎿

+= のような複合代入は、共通テスト表記には無いので x = x - 1 の形にほどいて表示されます。

左が書いた Python、右がアプリの表示を切り替えたときに出るものです。

Python
def sample(points, x, enemy_dist):
total = 0
for i in range(0, 10, 1):
total = total + i
for p in points:
if p > 3:
break
while x > 0:
x -= 1
if enemy_dist < 3:
fire = True
elif enemy_dist < 8:
fire = False
else:
drive = "forward"
print(total)
return total
共通テスト用プログラム表記
関数 sample(points, x, enemy_dist) を次のように定義する:
| total = 0
| i を 0 から 9 まで 1 ずつ増やしながら繰り返す:
| ⎿ total = total + i
| p を points の要素それぞれについて繰り返す:
| | もし p > 3 ならば:
| ⎿ ⎿ 繰り返しを抜ける
| x > 0 の間繰り返す:
| ⎿ x = x - 1
| もし enemy_dist < 3 ならば:
| | fire = True
| そうでなくもし enemy_dist < 8 ならば:
| | fire = False
| そうでなければ:
| ⎿ drive = "forward"
| 表示する(total)
⎿ total を返す

Python は字下げでブロックの範囲を表しますが、共通テスト表記は左に引いた線で表します。

  • | … まだブロックの中。線の本数が、入れ子の深さ
  • ⎿ … そのブロックの最後の行。⎿ ⎿ なら、2 つのブロックがいっしょに終わる
  • もし の途中の枝(うしろに そうでなくもし や そうでなければ が続く枝)の最後の行は | のまま。⎿ が付くのは、いちばん最後の枝の最後の行だけ

上の例の | ⎿ ⎿ 繰り返しを抜ける は、「もし と p を … 繰り返す の 2 つが、ここで終わる」という意味です。

なお、関数定義の書式は公式には規定されていません。 このアプリでは、問題文でよく見る 関数 名前(引数) を次のように定義する: を採っています。

大学入試センターが公表している表記の例示にあるのは、変数・文字列・代入・算術・比較・論理・関数(要素数 表示する など)・条件分岐・繰り返し・コメントです。 それ以外(関数の定義、… を返す、True / False、繰り返しを抜ける、辞書など)は、このアプリが Python に合わせて決めた書き方です。 表示を切り替えたとき、こうした書き方が入っていると ❓ が出ます。 本番の問題では、問題文の説明にしたがってください(例示にも「問題文の説明や指示に注意して」とあります)。

処理系のエラーをそのまま出しても、学び始めた人には読めません。 PyLite は構文エラーを最初から日本語で返し、実行時のエラーも日本語に直して表示します。 行番号も出ます。

もとのエラー表示
NameError: name 'enmy' isn't definedenmy という名前が見つかりません。enemy の打ち間違いではありませんか?(5 行目)
IndexError: list index out of rangeリストの範囲外を読もうとしています。要素数より大きい番号を使っていませんか?(8 行目)
ZeroDivisionError: divide by zero0 で割ろうとしています。割る数が 0 になっていないか確かめましょう。(22 行目)