- 原文作者:Shipeng Feng
- 译文出自:掘金翻译计划
- 译者: Zheaoli
- 校对者:Kulbear, hpoenixf
私はさまざまなテンプレートエンジンを長い間使用してきましたが、ついにテンプレートエンジンがどのように機能するのかを研究する時間ができました。
概要#
簡単に言うと、テンプレートエンジンは大量のテキストデータを扱うプログラミングタスクを完了するためのツールです。一般的に、私たちは web アプリケーションでテンプレートエンジンを利用して HTML を生成します。 Python では、テンプレートエンジンを使用したい場合、jinja や mako など、いくつかの選択肢があります。これから、tornado のテンプレートエンジンを利用して、テンプレートエンジンの動作原理を説明します。 tornado には、比較的シンプルな組み込みのテンプレートエンジンがあり、その原理を深く分析するのに便利です。
テンプレートエンジンの実装原理を研究する前に、まずはシンプルなインターフェース呼び出しの例を見てみましょう。
from tornado import template
PAGE_HTML = """
<html>
こんにちは、{{ username }}!
<ul>
{% for job in job_list %}
<li>{{ job }}</li>
{% end %}
</ul>
</html>
"""
t = template.Template(PAGE_HTML)
print t.generate(username='John', job_list=['engineer'])
このコード内の username
は動的に生成され、job
リストも同様です。 tornado
をインストールしてこのコードを実行することで、最終的な結果を確認できます。
詳細#
PAGE_HTML
を注意深く観察すると、このテンプレート文字列は 2 つの部分で構成されていることがわかります。一方は固定の文字列、もう一方は動的に生成される内容です。動的に生成される部分には特別な記号を使用してマークします。全体の作業フローの中で、テンプレートエンジンは固定の文字列を正しく出力し、マークした動的に生成される文字列を正しい結果に置き換える必要があります。
テンプレートエンジンを使用する最も簡単な方法は、以下のように 1 行の python コードで解決できます:
deftemplate_engine(template_string, **context):# process herereturn result_string
全体の作業プロセスでは、テンプレートエンジンは次の 2 つの段階に分かれて文字列を操作します:
- 解析
- レンダリング
解析段階では、準備した文字列を解析し、レンダリング可能な形式にフォーマットします。これは rendered.Consider
によって解析可能な文字列である可能性があり、解析器は言語のインタープリターまたはコンパイラーである可能性があります。解析器がインタープリターである場合、解析プロセス中にデータを格納するための特別なデータ構造が生成され、レンダラーはそのデータ構造全体を遍歴してレンダリングを行います。例えば、Django のテンプレートエンジンの解析器はインタープリターに基づくツールです。それに加えて、解析器は実行可能なコードを生成することもあり、レンダラーはそのコードを実行して対応する結果を生成します。 Jinja2 、 Mako 、 Tornado では、テンプレートエンジンは解析ツールとしてコンパイラーを使用しています。
コンパイル#
上記で述べたように、私たちは作成したテンプレート文字列を解析する必要があります。そして、tornado のテンプレート解析器は、作成したテンプレート文字列を実行可能な Python コードにコンパイルします。私たちの解析ツールは Python コードを生成する責任があり、単一の Python 関数で構成されています:
def parse_template(template_string):
# compilation
return python_source_code
parse_template
のコードを分析する前に、まずはテンプレート文字列の例を見てみましょう:
<html>
こんにちは、{ { username } }!
<ul>
{ % for job in jobs % }
<li>{ { job.name } }</li>
{ % end % }
</ul>
</html>
テンプレートエンジンの parse_template
関数は、上記の文字列を Python ソースコードにコンパイルします。最も簡単な実装方法は以下の通りです:
def _execute():
_buffer = []
_buffer.append('\n<html>\n こんにちは、')
_tmp = username
_buffer.append(str(_tmp))
_buffer.append('!\n <ul>\n ')
for job in jobs:
_buffer.append('\n <li>')
_tmp = job.name
_buffer.append(str(_tmp))
_buffer.append('</li>\n ')
_buffer.append('\n </ul>\n</html>\n')
return''.join(_buffer)
現在、私たちは _execute
関数内でテンプレートを処理しています。この関数は、グローバル名前空間内のすべての有効な変数を使用できます。この関数は、複数の string を含むリストを作成し、それらを結合して返します。明らかに、ローカル変数を見つける方がグローバル変数を見つけるよりも速いです。同時に、他のコードの最適化もこの段階で行われます。例えば:
_buffer.append('こんにちは')
_append_buffer = _buffer.append
# 繰り返し使用するために高速化
_append_buffer('こんにちは')
{ { ... } }
内の式は抽出され、string
リストに追加されます。 tornado
テンプレートモジュールでは、{ { ... } }
に記述された式には制限がなく、if および for コードブロックは正確に Python コードに変換されます。
具体的なコード実装を見てみましょう#
テンプレートエンジンの具体的な実装を見てみましょう。 Template
クラス内でコア変数を宣言し、Template
オブジェクトを作成すると、作成したテンプレート文字列をコンパイルできます。その後、コンパイル結果に基づいてレンダリングを行うことができます。私たちは作成したテンプレート文字列を一度だけコンパイルし、そのコンパイル結果をキャッシュできます。以下は Template
クラスの簡略化されたバージョンのコンストラクタです:
class Template(object):
def__init__(self, template_string):
self.code = parse_template(template_string)
self.compiled = compile(self.code, '<string>', 'exec')
上記のコード内の compile
関数は、文字列を実行可能なコードにコンパイルします。私たちは後で exec
関数を呼び出して生成したコードを実行できます。さて、parse_template
関数の実装を見てみましょう。まず、作成したテンプレート文字列を独立したノードに変換し、後で Python コードを生成する準備をします。このプロセスでは、_parse
関数が必要です。まずはそれを脇に置いておき、後でこの関数を見てみましょう。次に、テンプレートファイルからデータを読み取るための補助関数をいくつか作成する必要があります。では、私たちのカスタムテンプレートからデータを読み取るための _TemplateReader
クラスを見てみましょう:
class _TemplateReader(object):
def __init__(self, text):
self.text = text
self.pos = 0
def find(self, needle, start=0, end=None):
pos = self.pos
start += pos
if end is None:
index = self.text.find(needle, start)
else:
end += pos
index = self.text.find(needle, start, end)
if index != -1:
index -= pos
return index
def consume(self, count=None):
if count is None:
count = len(self.text) - self.pos
newpos = self.pos + count
s = self.text[self.pos:newpos]
self.pos = newpos
return s
def remaining(self):
return len(self.text) - self.pos
def __len__(self):
return self.remaining()
def __getitem__(self, key):
if key < 0:
return self.text[key]
else:
return self.text[self.pos + key]
def __str__(self):
return self.text[self.pos:]
Python コードを生成するために、_CodeWriter
クラスのソースコードを見てみましょう。このクラスはコード行を作成し、インデントを管理することができ、Python のコンテキストマネージャでもあります:
class _CodeWriter(object):
def __init__(self):
self.buffer = cStringIO.StringIO()
self._indent = 0
def indent(self):
return self
def indent_size(self):
return self._indent
def __enter__(self):
self._indent += 1
return self
def __exit__(self, *args):
self._indent -= 1
def write_line(self, line, indent=None):
if indent == None:
indent = self._indent
for i in xrange(indent):
self.buffer.write(" ")
print self.buffer, line
def __str__(self):
return self.buffer.getvalue()
parse_template
関数内で、まず _TemplateReader
オブジェクトを作成します:
def parse_template(template_string):
reader = _TemplateReader(template_string)
file_node = _File(_parse(reader))
writer = _CodeWriter()
file_node.generate(writer)
return str(writer)
次に、作成した _TemplateReader
オブジェクトを _parse
関数に渡してノードリストを生成します。ここで生成されるすべてのノードはテンプレートファイルの子ノードです。次に、_CodeWriter
オブジェクトを作成し、file_node
オブジェクトが生成した Python コードを _CodeWriter
オブジェクトに書き込みます。その後、動的に生成された Python コードの一連を返します。_Node
クラスは、特別な方法で Python ソースコードを生成します。これは一旦置いておき、後で戻ってこの関数を見てみましょう。さて、前述の _parse
関数を見てみましょう:
def _parse(reader, in_block=None):
body = _ChunkList([])
while True:
# Find next template directive
curly = 0
while True:
curly = reader.find("{", curly)
if curly == -1 or curly + 1 == reader.remaining():
# EOF
if in_block:
raise ParseError("Missing { %% end %% } block for %s" %
in_block)
body.chunks.append(_Text(reader.consume()))
return body
# If the first curly brace is not the start of a special token,
# start searching from the character after it
if reader[curly + 1] not in ("{", "%"):
curly += 1
continue
# When there are more than 2 curlies in a row, use the
# innermost ones. This is useful when generating languages
# like latex where curlies are also meaningful
if (curly + 2 < reader.remaining() and
reader[curly + 1] == '{' and reader[curly + 2] == '{'):
curly += 1
continue
break
私たちはファイル内で無限ループを続けて、指定した特殊なマーカーを探します。ファイルの終わりに達した場合、テキストノードをリストに追加し、ループを終了します。
# Append any text before the special token
if curly > 0:
body.chunks.append(_Text(reader.consume(curly)))
特殊なマーカーのコードブロックを処理する前に、静的な部分をノードリストに追加します。
start_brace = reader.consume(2)
{ {
または { %
のシンボルに出会ったとき、対応する式の処理を開始します:
# Expression
if start_brace == "{ {":
end = reader.find("} }")
if end == -1 or reader.find("\n", 0, end) != -1:
raise ParseError("Missing end expression } }")
contents = reader.consume(end).strip()
reader.consume(2)
if not contents:
raise ParseError("Empty expression")
body.chunks.append(_Expression(contents))
continue
{ {
に出会ったとき、それは後に式が続くことを意味します。私たちは式を抽出し、_Expression
ノードリストに追加するだけです。
# Block
assert start_brace == "{ %", start_brace
end = reader.find("% }")
if end == -1 or reader.find("\n", 0, end) != -1:
raise ParseError("Missing end block % }")
contents = reader.consume(end).strip()
reader.consume(2)
if not contents:
raise ParseError("Empty block tag ({ % % })")
operator, space, suffix = contents.partition(" ")
# End tag
if operator == "end":
if not in_block:
raise ParseError("Extra { % end % } block")
return body
elif operator in ("try", "if", "for", "while"):
# parse inner body recursively
block_body = _parse(reader, operator)
block = _ControlBlock(contents, block_body)
body.chunks.append(block)
continue
else:
raise ParseError("unknown operator: %r" % operator)
テンプレート内のコードブロックに出会ったとき、私たちは再帰的にコードブロックを抽出し、_ControlBlock
ノードリストに追加する必要があります。 { % end % }
に出会ったとき、それはこのコードブロックの終了を意味し、その時点で対応する関数を終了できます。
さて、前述の _Node
ノードを見てみましょう。心配しないでください、これは非常にシンプルです:
class _Node(object):
def generate(self, writer):
raise NotImplementedError()
class _ChunkList(_Node):
def __init__(self, chunks):
self.chunks = chunks
def generate(self, writer):
for chunk in self.chunks:
chunk.generate(writer)
_ChunkList
は単なるノードリストです。
class _File(_Node):
def __init__(self, body):
self.body = body
def generate(self, writer):
writer.write_line("def _execute():")
with writer.indent():
writer.write_line("_buffer = []")
self.body.generate(writer)
writer.write_line("return ''.join(_buffer)")
_File
では、_execute
関数を CodeWriter
に書き込みます。
class _Expression(_Node):
def __init__(self, expression):
self.expression = expression
def generate(self, writer):
writer.write_line("_tmp = %s" % self.expression)
writer.write_line("_buffer.append(str(_tmp))")
class _Text(_Node):
def __init__(self, value):
self.value = value
def generate(self, writer):
value = self.value
if value:
writer.write_line('_buffer.append(%r)' % value)
_Text
と _Expression
ノードの実装も非常にシンプルで、テンプレートから取得したデータをリストに追加するだけです。
class _ControlBlock(_Node):
def __init__(self, statement, body=None):
self.statement = statement
self.body = body
def generate(self, writer):
writer.write_line("%s:" % self.statement)
with writer.indent():
self.body.generate(writer)
_ControlBlock
では、取得したコードブロックを Python 構文に従ってフォーマットする必要があります。
さて、前述のテンプレートエンジンのレンダリング部分を見てみましょう。 Template
オブジェクト内で generate
メソッドを実装することで、テンプレートから解析された Python コードを呼び出します。
def generate(self, **kwargs):
namespace = { }
namespace.update(kwargs)
exec self.compiled in namespace
execute = namespace["_execute"]
return execute()
与えられたグローバル名前空間内で、 exec 関数はコンパイルされたコードオブジェクトを実行します。その後、私たちはグローバルに _execute 関数を呼び出すことができます。
最後に#
上記の一連の操作を経て、私たちはテンプレートを自由にコンパイルし、対応する結果を得ることができます。実際、tornado テンプレートエンジンには、私たちがまだ議論していない多くの機能がありますが、私たちはその最も基本的な動作メカニズムを理解しました。この基礎の上に、興味のある部分を研究することができます。例えば:
- テンプレートの継承
- テンプレートの包含
- その他の論理制御文、例えば
else
、elif
、try
など - 空白制御
- 特殊文字のエスケープ
- その他の未説明のテンプレート指令(訳注:tornado 公式ドキュメント を参照してください)