テンプレートエンジン
はじめに
典型的なプログラムには大量のロジックと少量のテキストデータが含まれる。プログラミング言語はそういった特徴を持つプログラムの書きやすさを重視して設計されている。しかし、少量のロジックと大量のテキストデータを扱うプログラミングのタスクも存在する。そういったタスクでは、テキスト中心の問題に適したツールが求められる。テンプレートエンジン (template engine) はそういったツールの一つである。本章ではシンプルなテンプレートエンジンを作成する。
このようなテキスト中心の問題として最もよく知られているのはウェブアプリケーションである。どんなウェブアプリケーションにも、ブラウザに読み込ませる HTML を生成する処理が重要なフェーズとして存在する。完全に静的な HTML ページはまず存在しない。ほとんどのページには「ユーザー名」などの少量の動的データが含まれ、「製品リスト」や「フレンドの新しい投稿」などの大きな動的データがほとんどを占めるページも少なくない。
一方で、ウェブアプリケーションが生成する HTML ページには静的なテキストも大量に含まれる。ページが大きくなれば、そういったテキストが数メガバイトに達することもある。ウェブアプリケーション開発者は「静的なデータと動的なデータの両方から構成される巨大なテキストを生成する最も優れた方法は何か?」という問題を解決する必要がある。さらに、静的なテキストは実際には HTML マークアップであり、チームの他のメンバーは HTML と似た書き方でテキストを作成できることを望むだろう。
簡単な例を示す。次の HTML を生成したいとしよう:
<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
<li>Apple: $1.00</li>
<li>Fig: $1.50</li>
<li>Pomegranate: $3.25</li>
</ul>
この HTML に含まれるユーザー名と製品の名前・値段は動的データである。製品の個数さえ固定されていない: 違う時間にページを開くと、異なる個数の製品が表示されるだろう。
この HTML を生成する一つの方法として、コードに埋め込んだ文字列定数を利用する方法が考えられる。このとき文字列補間などを利用して動的データを挿入することで最終的な HTML が生成される。製品リストなどの一部の動的データには反復処理が必要になので、そういった部分の HTML は個別に生成してから組み合わせることになる。
このアプローチで上記のページを生成するコードは次のようになるだろう:
# HTML 全体の骨格を表す文字列
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""
# 一つの製品に対応する HTML
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"
def make_page(username, products):
product_html = ""
for prodname, price in products:
product_html += PRODUCT_HTML.format(
prodname=prodname, price=format_price(price))
html = PAGE_HTML.format(name=username, products=product_html)
return html
これでも正しい結果は得られるものの、コードは複雑になる。最終的な HTML を構成するパーツが複数の文字列に分散し、ページのロジックを理解するのが難しい。さらに、データのフォーマットに関する詳細は Python コードに隠れて見えなくなっている。フロントエンドデザイナーが HTML ページを改変するには、Python コードに含まれる HTML を変更しなければならない。ページがこれより十倍 (あるいは百倍) 複雑だったらどうなるかを想像してみてほしい。コードはすぐに管理不能になるだろう。
テンプレート
これより優れたアプローチはテンプレート (template) を利用する。テンプレートは基本的には静的な HTML であり、動的な部分は特別な記法を使って表現される。上記の例で示した HTML を生成するテンプレートの例を次に示す:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
テンプレートでは HTML テキストが中心となり、その中に動的データを処理するロジックが埋め込まれる。このドキュメント中心のアプローチを上述したロジック中心のアプローチと比較してみてほしい。以前に示したプログラムのほとんどは Python コードであり、HTML は Python ロジックの中に埋め込まれていた。これに対してテンプレートを使うときプログラムのほとんどは静的な HTML マークアップとなる。
テンプレートが採用する「ほぼ静的」なスタイルは多くのプログラミング言語の対極にある。例えば Python ではソースファイルの大部分は実行可能なコードであり、静的なテキストは文字列リテラルとして埋め込まれる:
def hello():
print("Hello, world!")
hello()
このソースファイルを読み込む Python インタープリタは def hello():
を実行すべき命令 (の一部) だと解釈する。"Hello, world!"
に付いている二重引用符は、この部分を内側のテキストからなるリテラル文字列と解釈するようインタープリタに指示する。これはプログラミング言語の常識的な動作である: 大部分が動的で、静的な部分は二重引用符などを使った特別な記法の中に埋め込まれる。
テンプレート言語では、この関係が逆転する: テンプレートファイルは大部分が静的なテキストであり、実行される動的な部分に特別な記法が使われる。
<p>Welcome, {{user_name}}!</p>
このテキストの先頭から {{
までの部分はそのまま最終的な HTML ページの一部となる。{{
から }}
までの部分は内部の式 user_name
の評価結果に置き換わる。
Python の "foo = {foo}!".format(foo=17)
などの文字列フォーマット関数は、文字列リテラルと挿入されるデータからテキストを作成するためのミニ言語の例である。このアイデアを拡張して条件文やループを搭載したのがテンプレート言語であり、文字列フォーマット関数と程度が違うだけで本質的には同じものと言える。
「テンプレート」という名前は、似た構造を持ち詳細が異なる様々なページを生成するのに利用できる事実から来ている。
HTML テンプレートをプログラムから使うにはテンプレートエンジン (template engine) が必要になる。テンプレートエンジンは次の二つを引数に取る関数である:
- 最終的な出力の構造と静的なコンテンツを記述した静的なテンプレート
- テンプレートが参照する動的データを提供するコンテキスト (context)
テンプレートエンジンはテンプレートとコンテキストを受け取り、それらを組み合わせて完全な HTML 文字列を生成する。テンプレートを解釈し、動的な部分を実際のデータを使って置き換えるのがテンプレートエンジンとの仕事となる。
ところで、ほとんどのテンプレートエンジンは HTML だけではなく任意のテキストを生成できる。例えば、プレーンテキストのメールを生成するのに利用しても構わない。ただ、テンプレートエンジンは HTML の生成に使用されるケースが多く、特殊文字のエスケープといった HTML 特有の機能を持つこともよくある。この機能があれば、ユーザーは HTML で特別な処理が必要な文字を意識することなく値をテンプレートに挿入できる。
サポートされる構文
テンプレートエンジンによってサポートされる構文は異なる。本章で考えるテンプレートの構文は、有名なウェブフレームワーク Django が持つテンプレート言語を参考にしている。実装言語は Python であり、構文には Python と似た概念が登場する。本章の最初で簡単な例を示したが、ここでは実装する構文の全機能を簡単に説明する。
二重の中括弧で囲まれた部分にはコンテキストから取得したデータが挿入される:
<p>Welcome, {{user_name}}!</p>
テンプレートから利用できるデータはレンダリング時にコンテキストとして提供される。この点については後述する。
テンプレートエンジンはデータが持つ要素に対するアクセスを制限の緩い単純な構文で提供する。Python では、次の三つの式が全て異なる意味を持つ:
obj["key"]
obj.attr
obj.method()
私たちのテンプレート言語では、これらの操作は全てドットで表現される:
dict.key
obj.attr
obj.method
ドットはオブジェクトの属性または辞書のバリューに対するアクセスを表し、アクセス結果が呼び出し可能なときはそれが自動的に呼び出される。三つの操作に異なる構文が用意される Python と異なる構文であり、この構文を使うとテンプレートが単純になる:
<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>
フィルタ (filter) と呼ばれる関数を使うと値を改変できる。フィルタはパイプ文字 |
で呼び出せる:
<p>Short name: {{story.subject|slugify|lower}}</p>
手の込んだページではコンテキストによって HTML の構造自体が変化する場合がある。そのような場合は条件分岐を利用できる:
{% if user.is_logged_in %}
<p>Welcome, {{ user.name }}!</p>
{% endif %}
ループの機能を使えば、データの集合からページの要素を作成できる:
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>
通常のプログラミング言語と同様に、条件分岐とループを入れ子にすれば複雑な論理構造を表現できる。
最後に、テンプレートに対するドキュメントを残せるように、コメント用の構文が存在する:
{# このテンプレートは最高! #}
実装のアプローチ
大まかに言うと、テンプレートエンジンはパース (parse) とレンダリング (rendering) という二つのフェーズを持つ。
具体的に言えば、テンプレートのレンダリングは次の処理を行う:
- 動的なデータの提供元であるコンテキストを管理する。
- ロジック要素を実装する。
- ドットを使ったアクセスとフィルタを使った関数適用を実装する。
パースフェーズからレンダリングフェーズに何を渡すべきかという問題は大きな意味を持つ。レンダリングを可能とするために、パースは何を生成するべきだろうか? 主な選択肢は二つある: 通常のプログラミング言語で使われる用語を借りて、本章ではそれぞれをインタープリタモデル (interpreter model) およびコンパイラモデル (compiler model) と呼ぶ。
インタープリタモデルでは、パースがテンプレートの構造を表現するデータ構造を生成する。レンダリングはこのデータ構造を走査し、部分ごとにテンプレートの意味に従って最終的な結果を構築する。現実世界の例を挙げると、このアプローチは Django のテンプレートエンジンが採用している。
コンパイラモデルでは、パースが直接実行できるコードの一種を生成する。レンダリングはこのコードを実行して結果を作成する。コンパイラモデルを採用するテンプレートエンジンとして Jinja2 と Mako がある。
本章ではコンパイラモデルを持ったテンプレートエンジンを実装する: テンプレートは Python コードにコンパイルされ、その Python コードを実行することで結果を構築する。
ここで説明するテンプレートエンジンは元々 HTML 形式のレポートを生成する coverage.py
というプログラムで使うために書かれたものである。このプログラムでは少数のテンプレートが何度も使い回されて大量のファイルを生成されるので、テンプレートを Python コードにコンパイルした方が実行は高速だった。確かにコンパイルを行うパースは複雑で時間がかかるものの、パース結果の実行はインタープリタを使う場合と比べて高速であり、コンパイルはテンプレートにつき一度しか実行されないからである。
テンプレートを Python にコンパイルする処理は単純とは言えない。ただ、あなたが想像するほど複雑ではない。それに、開発者が口をそろえて言うように、プログラムを書くよりもプログラムを書くプログラムを書く方がずっと面白い!
これから実装するテンプレートコンパイラはコード生成 (code generation) と呼ばれる一般的なテクニックの簡単な例と言える。コード生成はプログラミング言語のコンパイラをはじめとした多くの強力かつ柔軟なツールの根底にあるテクニックである。複雑になることもあるものの、コード生成をあなたの道具箱に入れておいて損はない。
それぞれのテンプレートが使われる回数が少ないアプリケーションでは、インタープリタモデルのテンプレートエンジンの方が望ましいかもしれない。Python コードにコンパイルする時間を大量の高速なレンダリングで償却できず、単純なインタープリタを使った方が実行が高速になる可能性が残される。
Python へのコンパイル
テンプレートエンジンのコードを見ていく前に、パース処理が生成するコードの例を見てみよう。先述したように、本章で解説するテンプレートエンジンのパース処理はテンプレートを Python コードに変換する。以前に示した簡単なテンプレートをもう一度示す:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
このテンプレートを変換した Python コード (を可読性のためにフォーマットしたもの) を次に示す。少しだけ高速なコードを生成するための工夫がいくつか実装されているので、変換後の Python コードには不自然な部分がある:
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([
'<p>Welcome, ',
to_str(c_user_name),
'!</p>\n<p>Products:</p>\n<ul>\n'
])
for c_product in c_product_list:
extend_result([
'\n <li>',
to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'</li>\n'
])
append_result('\n</ul>\n')
return ''.join(result)
それぞれのテンプレートはコンテキスト (context
) を引数に取る render_function
関数に変換される。この関数は最初にコンテキストからデータを取り出してローカル変数に格納し、以降の参照を高速化する。このときローカル変数の名前には接頭辞 c_
が付けられるので、名前が衝突する心配はない。
テンプレートの処理結果は文字列であり、長い文字列を作成するときは部分文字列のリストを作ってから最後に連結すると最も高速になる (最終的な文字列に部分文字列を一つずつ連結していはいけない)。上記のコードでは文字列のリスト result
に部分文字列が追加されていく。result
の append
メソッドと extend
メソッドは何度も呼ばれるので、それぞれローカル変数 result_append
, result_extend
に束縛して高速化を図っている。最後のローカル変数 to_str
は組み込み関数 str
を束縛する。
こういった変数を普通のコードで用意することはまずないので、詳しく説明しておく。Python では result.append("hello")
のようなオブジェクトのメソッド呼び出しが二つのステップを経て実行される: まずオブジェクトの属性 (result.append
) が取得され、次に取得された属性が関数として引数 ("hello"
) と共に呼び出される。メソッド呼び出しを一つの操作と捉えることに慣れているかもしれないが、実際には二つの個別のステップが存在する。そのため最初のステップの結果を保存しておけば、その保存した結果に対して二つ目のステップを実行できる。つまり次の二つの Python スニペットは同じことをしている:
# 通常の書き方:
result.append("hello")
# こうも書ける:
append_result = result.append
append_result("hello")
テンプレートエンジンが出力するコードでは、二つ目のステップを実行する回数に関係なく一つ目のステップの結果をローカル変数に保存する。こうすると append
などの属性を取得する処理が実行されなくなるので、実行時間が少しだけ節約される。
これは通常とは異なるコーディング手法を使って実行速度をほんの少しだけ改善するマイクロ最適化 (micro-optimization) の例である。マイクロ最適化はコードの可読性や分かりやすさを損ねる場合があるので、パフォーマンスのボトルネックとなることが確かなコードに対してのみ使用が正当化される。どの程度のマイクロ最適化が正当化されるかは人によって意見が異なる。また、一部のプログラミング初心者は明らかに過剰なマイクロ最適化を行ってしまうことがある。ここに示した最適化は性能測定から実際の速度改善が (わずかでも) 確かめられたものである。マイクロ最適化は Python のマイナーな性質を利用するので学習の題材としては適しているものの、自分が書くコードでは使い過ぎないように注意する必要がある。
str
をローカル変数に束縛することもマイクロ最適化である。Python で名前は「関数単位でローカル」「モジュール単位でグローバル」「Python 組み込み」のいずれかであり、ローカルの名前はグローバルあるいは組み込みの名前より高速にアクセスできる。生成されたコードで利用されているのは、str
は必ず存在する組み込み変数であるにもかかわらず str
が使われるたびに毎回アクセスが発生する事実である。ローカル変数は組み込み変数より高速なので、str
をローカル変数に束縛することでアクセス時間が少しだけ短縮できる。
アクセスを高速化するためのローカル変数の定義が終わると、入力されたテンプレートをパースすることで作成された Python コードが始まる。作成される部分文字列は個数に応じてローカル変数 append_result
または extend_result
を使ってリスト results
に追加される。テンプレートに含まれるリテラルテキストは単純な文字列リテラルとなる。
append_result
と extend_result
を両方とも定義すると複雑性は増加するものの、ここではテンプレートをコンパイルしたコードの実行速度を最優先していることを思い出してほしい。extend_result
しか定義しないと、要素を一つだけ追加するとき無駄なリストが作成される。
{{...}}
に含まれる式は評価され、その結果を文字列に変換したものが results
に追加される。式がドットを含むときは、render_function
関数の引数に渡される do_dots
関数で評価される。ドットを含む式の評価結果はコンテキストが持つデータによって変化する: 式が表すのは属性アクセスあるいは要素アクセスであり、アクセス結果が呼び出し可能なら呼び出さなければならない。
{% if ... %}
と {% for ... %}
が表す論理構造は Python の条件文とループに変換される。これらのタグ以降のテキストの変換結果はそれぞれ if
文と for
文の本体に収められ、その本体の終端は {% end... %}
タグで表される。
テンプレートエンジンの実装
テンプレーエンジンの処理が理解できたと思うので、次は実装を見ていこう。
Templite
クラス
テンプレートエンジンの心臓部には Templite
クラスがある (lite な template だから templite だ!)。
Templite
クラスはごく少量のインターフェースを持つ: テンプレートのテキストを受け取るコンストラクタと、特定のコンテキストを受け取ってテンプレートをレンダリングを実行する render
メソッドしか持たない。コンテキストは辞書で表される。
# Templite オブジェクトを作成する。
templite = Templite('''
<h1>Hello {{name|upper}}!</h1>
{% for topic in topics %}
<p>You are interested in {{topic}}.</p>
{% endfor %}
''',
{'upper': str.upper},
)
# Templite オブジェクトをレンダリングする。
text = templite.render({
'name': "Ned",
'topics': ['Python', 'Geometry', 'Juggling'],
})
Templite
オブジェクトは構築時に渡されるテンプレートのテキストを使ってコンパイルステップを一度だけ実行し、以降の render
メソッドではコンパイル結果を使い回す。
Templite
クラスのコンストラクタは初期状態のコンテキストを表す辞書を第二引数に受け取る。この辞書は Templite
オブジェクトに保存され、レンダリング時に利用可能となる。上記の例における upper
のように全てのテンプレートで利用可能としたい関数や定数をここで設定できる。
Templite
クラスの実装を議論する前に、ヘルパークラスを一つ定義する。
CodeBuilder
本章で説明するテンプレートエンジンの主な仕事はテンプレートのパースと Python コードの生成である。CodeBuilder
は Python コードの生成を簡単にするためのクラスであり、Python コードを組み立てるときに必要なデータを管理する。
一つの CodeBuilder
オブジェクトは一つの完全な Python コードを生成する。本章のテンプレートエンジンから使われる限り、生成される Python コードは必ず一つの関数定義からなる。しかし CodeBuilder
はそのことを仮定しない。この設計によって CodeBuilder
のコードは一般的となり、テンプレートエンジンの他の部分との結合度が低下する。
本章の終盤で見るように、CodeBuilder
を入れ子してコードの特定の部分を後から作成する使い方もできる。
CodeBuilder
オブジェクトは最終的な Python コードを構成する文字列のリストを保持する。この他には現在のインデントの深さしか状態を必要としない:
class CodeBuilder(object):
""" Python のソースコードを構築するためのヘルパークラス """
def __init__(self, indent=0):
self.code = []
self.indent_level = indent
CodeBuilder
は簡単な処理しか行わない。add_line
メソッドは新しい行をコードに追加する。そのとき現在のインデントの深さに応じた個数のスペースが先頭に追加され、末尾には改行が追加される:
def add_line(self, line):
"""
ソースコードに新しい行を追加する。
インデントと改行は自動的に追加されるので必要ない。
"""
self.code.extend([" " * self.indent_level, line, "\n"])
indent
メソッドと dedent
メソッドはインデントの深さを一段階だけ深くまたは浅くする:
INDENT_STEP = 4 # PEP8 がそうしろと言っている!
def indent(self):
"""以降の行に適用されるインデントを一段階だけ深くする。"""
self.indent_level += self.INDENT_STEP
def dedent(self):
""" 以降の行に適用されるインデントを一段階だけ浅くする。 """
self.indent_level -= self.INDENT_STEP
add_section
メソッドは CodeBuilder
オブジェクトを新しく作成して返す。そのとき、返される新しい CodeBuilder
の出力は現在のソースコードの位置に挿入されるように設定される。リスト self.code
の要素は基本的に文字列であるものの、add_section
メソッドが作成する CodeBuilder
オブジェクト (「セクション」) が要素となる場合もある:
def add_section(self):
""" セクション (入れ子になった CodeBuilder) を追加する。 """
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section
__str__
メソッドは完全なソースコードを一つの文字列として返す。この処理は self.code
に含まれる文字列を連結するだけで済む。ただし、self.code
に含まれる CodeBuilder
オブジェクトに対しては __str__
メソッドを再帰的に呼び出す必要がある:
def __str__(self):
return "".join(str(c) for c in self.code)
get_globals
はコードを実行したときに定義される値を返す。つまり自身を文字列化して得られる Python コードを実行し、そのとき定義されるグローバル変数を表す辞書を返す:
def get_globals(self):
""" コードを実行し、そのとき定義されるグローバル変数からなる辞書を返す。 """
# 呼び出し側がブロックを正しく終わらせていることを確認する。
assert self.indent_level == 0
# Python コードを一つの文字列として取得する。
python_source = str(self)
# Python コードを実行し、定義されるグローバル変数を返す。
global_namespace = {}
exec(python_source, global_namespace)
return global_namespace
この get_globals
メソッドでは Python のマイナーな機能が使われている。exec
関数は文字列を Python コードとして実行し、そのときコードが定義するグローバル変数を第二引数に受け取る辞書に格納する。例えば、次のコードを実行したとする:
python_source = """\
SEVENTEEN = 17
def three():
return 3
"""
global_namespace = {}
exec(python_source, global_namespace)
exec
が返ると global_namespace['SEVENTEEN']
は 17
となり、global_namespace['three']
は three
という名前の関数となる。
本プロジェクトで CodeBuilder
クラスは一つの関数が定義されるコードを生成するために利用されるものの、このクラスの用途がそれだけに制限される理由があるわけではない。この事実を意識すれば、理解しやすい単純な実装が可能になる。
CodeBuilder
は任意の Python コードを作成するのに利用でき、テンプレートエンジンに関する知識を全く持たない。例えば関数を三つ定義する Python コードを作成すると、get_globals
はバリューに関数を持つ三つのエントリーからなる辞書を返す。Templite
が CodeBuilder
を使って生成する Python コードは関数を一つしか定義しないものの、その事実をテンプレートエンジンの実装詳細として CodeBuilder
には意識させない方が優れたソフトウェア設計となる。
CodeBuilder
から使う ── 関数を一つだけ定義する ── 上でも、get_globals
が辞書を返す設計にしておけばコードのモジュール性が増す: 定義される関数の名前を呼び出し側が知っておく必要がない。Python コードが定義する関数名は get_globals
が返す辞書から取得できる。
続いて Templite
クラスの実装を見ていきながら、CodeBuilder
がどのように使われるかを確認しよう。
Templite
クラスの実装
本章で解説するテンプレートエンジンのコードの多くは Templite
クラスに含まれる。これまで解説してきたように、このクラスはテンプレートのコンパイルとレンダリングを行う。
コンパイル
テンプレートを変換して Python 関数を定義するコードにする処理は Templite
クラスのコンストラクタで実行される。最初にコンテキストが保存される:
def __init__(self, text, *contexts):
"""
与えられた text を使って Templite オブジェクトを構築する。
context はレンダリングに利用される辞書であり、
グローバルなフィルタ変数を定義するのに利用できる。
"""
self.context = {}
for context in contexts:
self.context.update(context)
コンテキストを受け取るパラメータが *context
であることに注目してほしい。先頭のアスタリスクは可変個数の位置引数を表し、呼び出しの末尾にある全ての引数からなる含むタプルが contexts
として利用可能になる。これは引数のアンパック (unpack) と呼ばれる操作であり、このコンストラクタにはコンテキストを表す辞書をいくつでも渡せることを意味する。つまり、次の呼び出しはどれも正当である:
t = Templite(template_text)
t = Templite(template_text, context1)
t = Templite(template_text, context1, context2)
コンストラクタに渡されたコンテキストを表す辞書はタプル contexts
から利用可能になる。そこで contexts
を走査してそれぞれのエントリーを辞書 self.context
に加えることで、与えられたコンテキストを一つにまとめる。複数のコンテキストに同じキーを持つエントリーが含まれる場合は、最も後ろにあるコンテキストに含まれるエントリーが最終的に残る。
コンパイルされた関数を可能な限り高速にするために、テンプレートから参照されるコンテキスト変数は Python のローカル変数に束縛される。テンプレートの処理中に発見された変数の名前は set
を使って管理される。また、テンプレートの中で定義されるループ変数の名前も管理する必要がある:
self.all_vars = set()
self.loop_vars = set()
定義される関数の先頭部分でこれらの set
が利用されることを後に見る。続いて、先ほど書いた CodeBuilder
クラスを使って最終的なコンパイル結果である Python コードの作成を開始する:
code = CodeBuilder()
code.add_line("def render_function(context, do_dots):")
code.indent()
vars_code = code.add_section()
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
このコードは CodeBuilder
を作成し、プログラムの行をいくつか追加している。定義される Python 関数は render_function
と呼ばれ、二つの引数を受け取る: 利用できるデータ (コンテキスト) が入った辞書 context
と、ドットによる属性アクセスを実装する関数 do_dots
である。
この render_function
関数が受け取る context
は、Templite
クラスのコンストラクタに渡された辞書と、render
メソッドに渡された辞書を合わせたものである。テンプレートから利用できる全てのデータがこの辞書で表される。
CodeBuilder
クラスが実行する処理は非常に単純な点に注目してほしい: 追加される行の Python における意味 (関数定義かどうか、など) は全く解釈されない。そのため生成されたコードを読むとき、CodeBuilder
の内部動作を追うのに頭を悩ませる必要はない。
上記のコードでは vars_code
というセクションが作成されている。テンプレートで利用される変数に対応する Python のローカル変数を作成するコードが後に vars_code
オブジェクトを使って作成される。CodeBuilder
のセクションを作成する機能を利用すれば、コードの位置だけを最初に決めて内容は必要な情報が集まってから作成できるようになる。
その後の四行は固定であり、結果を収めるリスト、そのリストの append
メソッドと expand
メソッドを束縛するローカル変数、そして組み込み関数 str
を束縛するローカル変数が定義される。以前に説明したように、こうすると変数の取得ステップを飛ばすことができてレンダリングのパフォーマンスが少しだけ向上する。
result
の append
メソッドと extend
メソッドの両方を束縛するのは、生成される行が一行か複数行かに応じて最も効率的なメソッドを呼び出すためである。
続いて、出力される文字列をバッファする処理を追加する:
buffered = []
def flush_output():
""" buffered にある行を CoreBuilder に出力する。 """
if len(buffered) == 1:
code.add_line("append_result(%s)" % buffered[0])
elif len(buffered) > 1:
code.add_line("extend_result([%s])" % ", ".join(buffered))
del buffered[:]
コンパイル後の関数で最終的な出力に文字列を加えるには、結果を保持するリストに文字列を追加する関数を呼び出す必要がある。そのとき、連続する呼び出しは一つにまとめるのが望ましい。これもまたマイクロ最適化と言える。このために、出力されるべき文字列を一時的にバッファ buffered
に保存する。
buffered
リストには、最終的な Python コードが出力する文字列であって code.add_line
にまだ渡されていないものが含まれる。テンプレートのコンパイルが進むにつれて buffered
に文字列が追加され、制御フローが分岐点 (if
文やループの開始・終了地点など) に到達すると buffered
の全要素を一つの関数呼び出しでまとめて出力するコードが作成される。
上記の flush_output
関数はクロージャ (closure) である。クロージャは自身の外側にある変数を参照する関数に付けられるオシャレな名前であり、flush_output
は自身の外側にある buffered
と code
を参照する。クロージャを使うと関数の呼び出しが単純化される: flush_output
の定義に操作の対象が埋め込まれているので、呼び出すとき明示的に指定する必要がない。
flush_output
関数を実行すると、バッファされている文字列が一つだけなら append_result
(に束縛された result.append
) の呼び出しが最終的な結果に追加され、複数の文字列がバッファされているなら extend_result
(に束縛された result.extend
) の呼び出しが最終的な結果に追加される。その後バッファされた文字列のリスト buffered
はクリアされ、新しい文字列のバッファが可能になる。
これ以降のコンパイル処理は出力すべき文字列を buffered
に追加し、必要になった時点で flush_output
を呼び出して CodeBuilder
に Python コードを書き込む形で進行する。
この関数があれば、コンパイラには次のようなコードを書くことができる:
buffered.append("'hello'")
この呼び出しがあると、コンパイル後の Python コードに次の呼び出し (に等しい処理) が追加される:
append_result('hello')
この呼び出しはテンプレートのレンダリング結果に文字列 hello
からなる行を追加する。複数のレベルで抽象化がされているので、すぐに理解するのは難しいかもしれない。詳しく言えば、コンパイラが buffered.append("'hello'")
を呼び出すとコンパイル後の Python コードに append_result('hello')
が追加され、そのコードを実行すると hello
がテンプレートの処理結果に追加される。
Templite
クラスの実装に戻ろう。制御構造のパースでは、入れ子になった制御構造が適切な順序で閉じられることを確認する必要がある。このために用意される文字列のスタックが ops_stack
である:
ops_stack = []
例えば、パース中に {% if ... %}
タグを読んだときは、'if'
が ops_stack
にプッシュされる。{% endif %}
を読んだときは ops_stack
から文字列をポップして、それが 'if'
でなければエラーを報告する。
本当のパース処理はここから始まる。最初に正規表現を使ってテンプレートをトークン列に分解する。正規表現は恐ろしいツールである: 複雑なパターンマッチングを非常にコンパクトな文字列で表現でき、加えて正規表現エンジンは Python ではなく C で実装されているので実行速度も非常に優れている。テンプレートのパースで用いる正規表現を示す:
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
かなり込み入って見える。部分ごとに説明しよう。
re.split
関数は正規表現を使って文字列を分割する。キャプチャを表す括弧を持つパターンを re.split
に渡すと、返り値のリストにはマッチした文字列も含まれるようになる。上記のパターンはタグを表す文字列にマッチするので、テンプレートに含まれるタグとそれらの間にある文字列からなるリストが tokens
に代入される。
パターンの先頭にある (?s)
はフラグの設定であり、この設定があるとドットが改行にマッチするようになる。続いて三つのパターンからなる括弧に囲まれたグループが一つある: {{.*?}}
は式に、{%.*?%}
はタグに、{#.*?#}
はコメントにマッチする。全てのパターンで使われている .*?
は任意個の文字列にマッチし、そのとき最も短い文字列が優先される。
re.split
の返り値は文字列のリストである。例えば、次のテンプレートテキストが入力されたとする:
<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>
これに対する上記の re.split
の出力を次に示す:
[
'<p>Topics for ', # リテラル
'{{name}}', # 式
': ', # リテラル
'{% for t in topics %}', # タグ
'', # リテラル (空文字列)
'{{t}}', # 式
', ', # リテラル
'{% endfor %}', # タグ
'</p>' # リテラル
]
続いて分割されたトークンを走査して一つずつコンパイルを実行する。トークンは種類ごとに分割されているので、種類を確認してそれに応じた処理を実行すればよい。
以降のコンパイルはトークンを一つずつ処理する:
for token in tokens:
最初にトークンを調べ、可能な四つのケースのどれかを判定する。この判定にはトークンの最初の二文字しか必要にならない。最初のケースはコメントであり、これは簡単に処理できる: 無視して次のトークンに進めばよい:
if token.startswith('{#'):
# コメント: 無視して次のトークンへ進む。
continue
続いて {{...}}
式のケースが処理される。先頭と末尾にある二つの中括弧と空白文字を削除した文字列全体が _expr_code
メソッドに渡される:
elif token.startswith('{{'):
# 式: 変換結果の文字列を追加する。
expr = self._expr_code(token[2:-2].strip())
buffered.append("to_str(%s)" % expr)
_expr_code
メソッドはテンプレートで使われる式を Python の式に変換する。その実装については後述する。レンダリング時、式の評価結果は to_str
関数で文字列に変換されてから最終的な結果に追加される。
三つ目のケースでは多くの処理が必要になる: タグ {%...%}
である。タグは制御構造を表し、Python の制御構造にコンパイルされる。最初にバッファされた出力をフラッシュして、タグの内部にある文字列を取り出す:
elif token.startswith('{%'):
# 制御構造を表すタグ: 内部の文字列を取り出す。
flush_output()
words = token[2:-2].strip().split()
続いて、最初の単語に応じてさらに if
, for
, end
の三つのケースに処理が分かれる。次に示す if
のケースには、簡単なエラー処理とコード生成が含まれる:
if words[0] == 'if':
# if: 条件式を評価して分岐する。
if len(words) != 2:
self._syntax_error("Don't understand if", token)
ops_stack.append('if')
code.add_line("if %s:" % self._expr_code(words[1]))
code.indent()
if
タグは式を一つだけ持つべきなので、words
が二要素のリストでなければヘルパーメソッド _syntax_error
を呼び出して構文エラーの例外を送出する。words
が正しく二要素のリストなら、'if'
を ops_stack
に積んで対応する endif
タグを検出する準備を整える。その後 _expr_code
で if
タグの条件式を Python 式に変換し、条件分岐を実装する Python の if
文を構成する最初の一行を作成する。
二つ目のタグ for
は、Python の for
文にコンパイルされる:
elif words[0] == 'for':
# ループ: 式の結果を走査する。
if len(words) != 4 or words[2] != 'in':
self._syntax_error("Don't understand for", token)
ops_stack.append('for')
self._variable(words[1], self.loop_vars)
code.add_line(
"for c_%s in %s:" % (
words[1],
self._expr_code(words[3])
)
)
code.indent()
まず if
タグの場合と同様に構文をチェックして 'for'
をスタックに積む。次の行で使われている _variable
メソッドは引数が変数の名前として正当なことを確認し、以前に定義したループ変数を収める集合 self.loop_vars
に変数を追加する。コンテキストが持つ値にアクセスするときに利用される Python のローカル変数を定義するコードを後で書く必要がある。この処理を正しく行うには、テンプレートで利用される変数の名前からなる集合 self.all_vars
と、テンプレートのループで定義される変数の名前からなる集合 self.loop_vars
の両方が必要になる。
最後に for
文を始める一行を追加してインデントを深くする。テンプレートがアクセスする変数に対応する Python 変数の名前には必ず c_
を先頭に付けているので、Python 関数が利用する他の名前と衝突する心配はない。反復されるオブジェクトを表すテンプレートの式から Python の式への変換ではここでも _expr_code
メソッドが使われる。
最後の end
タグは {% endif %}
または {% endfor %}
の形をしている。コンパイル後の関数に対する効果はどちらも同じで、インデントを一段階だけ浅くすることで現在の if
または for
のブロックを終了させる:
elif words[0].startswith('end'):
# 何らかの構文の終わり: インデントを浅くする。
if len(words) != 1:
self._syntax_error("Don't understand end", token)
end_what = words[0][3:]
if not ops_stack:
self._syntax_error("Too many ends", token)
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
code.dedent()
end
タグが持つ効果の実装は一行で終わる点に注目してほしい: code.dedent
を呼び出すだけで済む。これ以外のコードはテンプレートが適切な形をしていることを確認するためのエラーチェックである。プログラムを変換するコードでは、このようにエラーチェックがコードの多くを占めることが珍しくない。
エラー処理と言えば、これまでに処理した if
, for
, end
のいずれでもないタグは未知のタグなので、構文エラーを送出する:
else:
self._syntax_error("Don't understand tag", words[0])
以上で {{...}}
, {#...#}
, {%...%}
という三つの特殊な構文が実装できた。最後にテンプレートで使われるリテラルに対応するトークンに対する処理を実装しよう。このトークンを処理するために、組み込み関数 repr
を使ってトークンを表す Python の文字列リテラルを出力のバッファに追加する:
else:
# リテラル: 空文字列でなければ出力する。
if token:
buffered.append(repr(token))
もし repr
を使わないと、コンパイル後の関数に次のような行が生まれてしまう:
append_result(abc) # エラー! abc は定義されていない。
当然、次のように引用符で囲む必要がある:
append_result('abc')
repr
関数は引数のオブジェクトを Python コードで表現するときの文字列を返す。文字列を渡すと、引用符で囲った上で次のようにバックスラッシュによるエスケープを適切に施した文字列が返る:
append_result('"Don\'t you like my hat?" he asked.')
最初に if token:
でトークンが空文字列かどうかを確認している点に注意してほしい。空文字列を出力に加える意味はないためである。入力からトークン列への変換で正規表現を使った文字列分割を使っているので、タグが連続していると間に空のトークンが生じる。ここで空文字列をチェックしておけば、コンパイル後の関数に意味のない append_result("")
が追加されるのを防止できる。
これでテンプレートに含まれる全てのトークンを処理するループが実装できた。このループが終わると、テンプレート全体の処理が完了する。この時点で ops_stack
は空でなければ閉じられていないタグがあるので、この事実を最後にチェックする。また、バッファされた出力をフラッシュする必要もある:
if ops_stack:
self._syntax_error("Unmatched action tag", ops_stack[-1])
flush_output()
以前に関数の先頭でセクションを作成していた。このセクションは、コンテキストに含まれテンプレートからアクセスされる変数を Python のローカル変数に束縛するために存在する。テンプレート全体の処理が完了して利用される変数の名前は全て判明したので、続いてこのセクションを完成させる。
ただ、定義が必要な変数名が自明に分かるわけではない。次のテンプレートを見てほしい:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price }}</li>
{% endfor %}
</ul>
このテンプレートでは user_name
と product
という二つの変数が使われている。all_vars
は {{...}}
式で使われた変数から構成されるので、二つの変数を両方とも含む。しかし、product
はループタグで定義される変数なので、関数の先頭で定義すべきなのは user_name
に対応する Python のローカル変数のみである。
テンプレートで使われる変数は all_vars
に含まれ、テンプレートの中で定義される変数は loop_vars
に含まれる。そして、loop_vars
に含まれる変数に対応する Python のローカル変数はループが始まるときに定義される。よって、関数の先頭で定義が必要なのは all_vars
に含まれて loop_vars
に含まれない変数に対応するローカル変数である:
for var_name in self.all_vars - self.loop_vars:
vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
定義が必要な名前ごとに関数の先頭のセクションに一行が追加され、コンテキストから取得した値が適切な名前をしたローカル変数に束縛される。
テンプレートから Python 関数へのコンパイルはあと一歩で完了する。これまでコンパイル後の関数はテンプレートの変換結果をリスト result
に文字列を追加してきたので、最後に連結して返す:
code.add_line("return ''.join(result)")
code.dedent()
これでコンパイル結果である Python 関数のソースコードが完成した。続いて、CodeBuilder
オブジェクトから関数オブジェクトを取り出す。CodeBuilder
クラスの get_globals
メソッドは構築された Python コードを実行する。これまでに構築してきたコードは render_function
の関数定義だけを含むので、コードを実行すると render_function
が定義される (実行はされない)。
get_globals
メソッドの返り値はコードで定義されたグローバル変数からなる辞書なので、render_function
に対応するバリューを取り出して Templite
オブジェクトの属性に保存する:
self._render_function = code.get_globals()['render_function']
これで Python 関数 self._render_function
が呼び出せるようになった。この関数はレンダリングフェーズで利用される。
式のコンパイル
コンパイル処理の重要な要素でこれまでに解説していないものが一つある: テンプレートの式を Python の式にコンパイルする _expr_code
メソッドである。テンプレートに含まれる式は一つの名前だけを含む単純なものである場合もある:
{{user_name}}
一方、属性アクセスやフィルタが組み合わさった複雑な式である場合もある:
{{user.name.localized|upper|escape}}
_expr_code
メソッドはあらゆる式を適切に処理できなければならない。あらゆる言語の式と同じように、本章のテンプレートの式は再帰的な構造を持つ: 大きな式は小さな式から構成される。例えば完全な式はパイプで二つの部分式に分割され、最初の部分式はドットでさらに二つの部分式に分割されるかもしれない。よって _expr_code
の自然な実装は再帰的な呼び出しを利用する:
def _expr_code(self, expr):
""" expr に対応する Python 式を生成する。 """
最初は式がパイプを含むケースを処理する。このときは式をパイプで分割して部分式のリストを作成し、まず _expr_node
の再帰的な呼び出しで最初の部分式を Python 式に変換する。
if "|" in expr:
pipes = expr.split("|")
code = self._expr_code(pipes[0])
for func in pipes[1:]:
self._variable(func, self.all_vars)
code = "c_%s(%s)" % (func, code)
以降の部分式は関数の名前であり、一つ前の部分式の結果を入力した結果が次の部分式に渡される。関数名は self._variable
の呼び出しを通して all_vars
に追加され、コンパイル後の関数の先頭で適切な名前の付いたローカル変数に束縛される。
式がパイプを持たないなら、続いてドットによる属性アクセスを処理する。このときは式をドットで分割して部分式を取得し、最初の部分式を _expr_code
の再帰的な呼び出しで Python 式に変換する。最初のドット以降の部分は repr
関数で文字列リテラルに変換されてからコンマを間に入れて連結される:
elif "." in expr:
dots = expr.split(".")
code = self._expr_code(dots[0])
args = ", ".join(repr(d) for d in dots[1:])
code = "do_dots(%s, %s)" % (code, args)
ドットを含む式がどのようにコンパイルされるかを理解するには、テンプレートの式における x.y
が Python の x['y']
または x.y
のいずれかエラーを出さない方を意味することを思い出してほしい。さらに、式の評価結果が呼び出し可能なら呼び出すという規則もある。このため、実際の動作はコンパイル時には決定せず、実行時になるまで分からない。そのため、ドットを含む式は do_dots
関数の呼び出しにコンパイルされる。例えば式 x.y.z
は do_dots(x, 'y', 'z')
となる。do_dots
関数は様々な手法のアクセスを成功するまで試し、取得された値が呼び出し可能なら呼び出し、アクセスまたは呼び出しの結果を返す関数である。
do_dots
関数はコンパイル後の Python 関数を呼び出すときに引数として渡される。その実装は後述される。
_expr_code
メソッドの最後の部分は、式がパイプもドットも含まないケースを処理する。このとき式は名前だけからなる。このときは self._variable
を呼び出して名前を all_vars
に記録し、接頭辞 c_
を付けた名前の Python のローカル変数を通してアクセスする:
else:
self._variable(expr, self.all_vars)
code = "c_%s" % expr
return code
ヘルパー関数
コンパイルの実装で使ったヘルパー関数の実装を示す。_syntax_error
メソッドは人間が読めるエラーメッセージを組み立てて例外を送出する:
def _syntax_error(self, msg, thing):
""" msg と thing から作成したエラーメッセージを持つ構文エラーを送出する。 """
raise TempliteSyntaxError("%s: %r" % (msg, thing))
_variable
メソッドは変数名の検証と、コンパイル中に構築される名前の集合に対する要素の追加を行う。変数名が Python の識別子として正当かどうかを正規表現を使って検証し、それから集合に名前を追加する:
def _variable(self, name, vars_set):
"""
name を利用された変数名として記録する。
name を変数名の集合 vars_set に追加する。
name が Python の識別子名として正当でなければ例外を送出する。
"""
if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
self._syntax_error("Not a valid name", name)
vars_set.add(name)
以上で、テンプレートをコンパイルするコードが完成した!
レンダリング
最後に残ったのはレンダリングのコードである。テンプレートは Python の関数にコンパイルされるので、レンダリングですべき処理は多くない。コンテキストをセットアップして、コンパイル結果の Python 関数を呼び出せばよい:
def render(self, context=None):
"""
テンプレートを context の下でレンダリングする。
context はレンダリングで使われる値を収めた辞書である。
"""
# これから使うコンテキストを作成する。
render_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, self._do_dots)
Templite
クラスのコンストラクタでコンテキストを表す辞書を作成したことを思い出してほしい。ここではそれをコピーして、引数に受けとった追加のコンテキストを (もし存在するなら) そこに加えている。辞書のコピーは同じテンプレートの何度もレンダリングしたときデータの干渉を防ぐため、辞書の統合はコンテキストを表す辞書が一つしか使えないために必要になる。こうして Templite
オブジェクトの構築時に渡されたコンテキストと render
メソッドの呼び出し時に渡されたコンテキストは一つに統合される。
コンパイル時に Templite
クラスのコンストラクタで構築された self.context
のエントリーが render
メソッドに渡される context
によって上書きされる可能性がある点に注目してほしい。ただ、この事実が利用される可能性は低い: コンストラクタで設定されるコンテキストではフィルタや定数といったグローバルに近い変数が定義され、render
メソッドに渡されるコンテキストでは一度のレンダリングに特有のデータが定義されることが多い。
その後コンパイル結果の self.render_function
が呼び出される。第一引数には完全なコンテキスト context
が、第二引数にはドットの意味論を実装する関数 self._do_dots
が渡される。
def _do_dots(self, value, *dots):
""" ドットの意味論を実行時に評価する。 """
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
コンパイル時にテンプレートの式 x.y.z
は Python の式 do_dots(x, 'y', 'z')
に変換される。_do_dots
関数はドットでつながった名前を走査し、それぞれの名前を使ってまず属性の取得を試みる。属性の取得が失敗した場合は、名前を使ってバリューの取得を試みる。この処理があると、テンプレートの式 x.y
が Python で x.y
と x["y"]
の両方 (エラーを出さない方) を意味できるようになる。各ステップにおいて、もし取得した値が呼び出し可能なら、それを呼び出した結果で値が置き換えられる。ドットでつながった名前を全て処理したとき最終的に残る値が式全体の評価結果となる。
ここでもまた Python の引数アンパックの機能を使っている。つまり、最後の引数の名前 *dots
の先頭にアスタリスクが付いているので、_do_dots
メソッドを呼び出すとき value
より後ろの引数には任意個の値を渡すことができる。このため、テンプレートに含まれるドットでつながった式がどれだけ長くても同じ _do_dots
メソッドで処理できる。
self._render_function
を呼び出すときドットを含む式を評価する関数を引数として渡す設計になっているものの、この引数には必ず同じ値 self._do_dots
が渡される。self._do_dots
と同様の処理を実装する関数の定義をコンパイル結果のコードに含める設計も不可能ではない。ただ、そうすると全てのテンプレートのコンパイル結果に同じ十行程度のコードが加わることになる。さらに、このコードはテンプレートの動作の定義するものであり、特定のテンプレートの詳細を定義するものではない。そのためドットを含む式を評価するロジックをコンパイル結果のテンプレートに含めない方が自然な設計となる。
テスト
本章で解説したテンプレートエンジンには、全ての基本的な機能やエッジケースでの振る舞いをカバーするテストが付いている。実はテストを含めると、本プロジェクトのコードは 500 行の制限を少しだけオーバーしてしまう: テンプレートエンジンは 252 行、テストは 275 行で書かれている。この比率 (実際のコードよりテストの方が多い) は豊富なテストを持つコードで典型的である。
省略された機能
本格的なテンプレートエンジンが提供する機能は本プロジェクトで実装したものよりずっと多い。コードを短くするため、次に示すような興味深いアイデアは実装できなかった:
- テンプレートの継承・インクルード
- カスタムタグ
- 自動的なエスケープ
- 引数を持ったフィルタ
else
やelif
などの複雑な条件分岐- 二つ以上のループ変数を持ったループ
- 空白の制御
そうだとしても、本章で実装した単純なテンプレートエンジンは有用である。実際、私は coverage.py
というスクリプトで HTML レポートを生成するときに利用した。
まとめ
わずか 252 行で、十分な機能を持つ単純なテンプレートエンジンを実装できた。現実のテンプレートエンジンが持つような豊富な機能は持たないものの、そのコードは「テンプレートを Python 関数にコンパイルして、それを実行することで結果のテキストを生成する」という基礎的なアイデアを示している。