LoginSignup
51
56

More than 1 year has passed since last update.

Jinjaテンプレートの書き方をがっつり調べてまとめてみた。

Last updated at Posted at 2022-04-24

本記事はQiitaの「データに関する記事を書こう!」イベント用の記事となります。

最近聞くことが多くなってきたJinjaの書き方について整理&まとめてみました。

Jinjaって何?(どんなところで使われているのか)

Djangoとかを普段使っている方はお馴染みの{% for i in range(10) %}とか{{ any_variable }}みたいな書き方を任意のテキストファイルに対して反映できるテンプレートエンジンのライブラリです。初版リリースはこの記事を執筆している時点で3年弱くらい前なので比較的新しいライブラリになります。

以下に書かれているようにDjangoのテンプレートの機能がベースとなっており、Djangoに慣れている方であればほぼ学習コスト0で書けるような書き方になっています。

Djangoのテンプレートエンジンにインスパイアされているので、パッと見はDjangoのデフォルトのテンプレートエンジンと同じです。Jinja2はDjangoのテンプレートエンジンより機能が豊富で、HTMLやXMLの記述専用のテンプレートエンジンではなくLaTexやメールといった他の用途にも使うことができます。
Djangoの1.8より公式にサポートされています。(ライブライのインストールは必須です)

また、Flaskなどでも使われています。

Flaskは自身のテンプレート・エンジンとしてJinja2を活用します。違うテンプレート・エンジンを使うことも当然自由ですが、それでもFlask自体を実行するためにJinja2をインストールする必要があります。

データエンジニアリング界隈で大分聞くようになってきたdbtなどでもSQLにて利用することができるようです。

先日開催されていたデータエンジニアリングの勉強会でもdbtに絡んで少し触れられていました。

DjangoやFlaskなどだとHTML等がメインとなりますが、他の任意のテキストでも使用できるため、例えばSQLにてループや変数、if文、マクロなどを使って記述の重複を減らしたりSQL単体だと記述が複雑になったり保守がしんどくなるケースなどで緩和することができます。

この辺りのSQLでのJinja利用もdbtなどの利用が増えてくるに連れて広がってくるのではという感じがします。

この記事を書いている時点でGitHubのpublicで且つスターが1件以上ある約600万弱くらいのリポジトリ内でDjangoがスター数が6.3万強くらいで71位くらい、Flaskが5.8万強くらいで87位くらいだった点、データエンジニアリング界隈などではdbtが大分目立つようになってきたことを考えるとそれらで使われている(もしくはほぼ同じ記法が使える)JinjaはPythonのweb界隈やデータエンジニアリングなどを担当されていらっしゃる方は触れる or 触ったことがある方も多いのではないでしょうか。

ちなみにですが弊社では最近横断のBIツール上のSQLエディタでもJinjaの記法とそのテンプレートのリアルタイムプレビューに対応しました。PythonやDjangoを普段から使っており学習コストがほぼ0でさくっと使えてすぐに手に馴染む感じで気に入っています。BigQueryやRedshiftなどでも独自のループや条件分岐の記法がある?ようですが、弊社だとバックエンドで今のところAthena(Presto)をメインで使っているので拡張的に使えて助かっています。

Jinjaライブラリの名前の由来

余談ですがJinjaという名前はどうやらTemplateライブラリということで日本語の神社(Jinja)の英語のtempleに由来しているそうです。

Templateとtempleの発音が似ているので、templeからの連想でJinja(神社)と命名された。

英語圏の有名ライブラリなどでしばしばJinjaと見かけるのは日本人の身からするとなんだか少し不思議な感じがしますね。

本記事の注意書き

あまりに長くなるのでJinjaの機能全てに触れたりはしません。例えば主にSQLとかで使いたい・・・という感じなので使わないと思われる継承とかもスキップしています(Djangoプロジェクトなどでは継承等は良く使いますが・・・)。

JinjaのPythonへのインストール

Jinjaはpipでさくっとインストールすることができます。pipコマンドで指定する時にはJinja2というライブラリ名が該当します。

本記事では記事執筆時点で最新の3.1.1のバージョンを使っていきます(Pythonは3.8を使用しVS CodeのJupyter上で動かしていきます)。

$ pip install Jinja2==3.1.1
Successfully installed Jinja2-3.1.1

JinjaをPythonで軽く動かしてみる

早速少しJinjaをPython上で動かしてみます。

import時のパッケージはjinja2となります。また、本記事では主にTemplateクラスを使用していきます。Templateのコンストラクタのsource引数で対象の文字列を指定し、renderメソッドでテンプレートの記述を展開します。

内容はSQL想定で、シンプルにwithで値を設定し、それをLIMIT句で変数として参照しているだけです。

from jinja2 import Template

sql: str = (
    '{% with limit_num = 10 %}'
    '\nSELECT * FROM any_db.any_table LIMIT {{ limit_num }}'
    '\n{% endwith %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

正常に展開されました。

SELECT * FROM any_db.any_table LIMIT 10

変数の参照

Jinjaの記法では変数の参照は括弧を2重にする形で{{ 変数名 }}と書きます。前節での{{ limit_num }}といった記述であればlimit_numという変数にアクセスしていることになり、テンプレートの展開時にその部分が変数の値にて展開されます(前節で言えば変数の値は10だったので{{ limit_num }}の部分が10になります)。

なお、{{limit_num}}といったように{{などの括弧と変数名の間にスペースは入れなくとも動きます。このあたりは好みやプロジェクトのスタイルルール次第かなと思います。

プログラム側から変数の値を指定する

DjangoとかでHTMLテンプレートへパラメーターを渡したりなどに慣れている方はお馴染みな感じですが、Pythonのプログラム上の変数などをテンプレート上の値へ直接展開することができます。renderメソッドの引数でキーワード引数や辞書などで指定します。

キーワード引数で指定する例 :

from jinja2 import Template

sql: str = (
    'SELECT * FROM any_db.{{ table_name }} LIMIT {{ limit_num }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render(table_name='any_table', limit_num=10)
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 10

辞書で指定する例 :

from jinja2 import Template

sql: str = (
    'SELECT * FROM any_db.{{ table_name }} LIMIT {{ limit_num }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render({'table_name': 'any_table', 'limit_num': 10})
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 10

Jinjaでの制御文の書き方

Jinjaでは各種制御(if文やforループ、変数定義やマクロなど)は{% 制御文 %}といった形で{%%}で制御文を囲む形で書きます。

また、多くの制御文では制御文の終わりに{% end... %}という記述が必要になります(例: endifendforなど)。例えばif文の条件文の終了時点では以下のように書きます(ifブロックの終わりの指定のようにお考えください)。

{% if limit_num >= 10 %}
-- ここに条件を満たした場合の記述を書きます...
{% endif %}

変数などと同様に{%などの後のスペースを省略してもスペースを入れた場合と同じ動作をします(例 : {%endif%})。

変数の定義

以降の節ではsetとwithの変数定義の仕方について説明します。

setを使った変数の定義

setを使った変数宣言の場合、宣言以降で一通り該当の変数を使える形になります。{% endset %}といった記述は不要です。{% set 変数名 = 変数の値 %}といった書き方をします。

from jinja2 import Template

sql: str = (
    '{% set limit_num = 10 %}'
    'SELECT * FROM any_db.any_table LIMIT {{ limit_num }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 10

withを使った変数の定義

withを使った変数宣言の場合、終了箇所を指定するための{% endwith %}の記述が必要になります。with内のスコープでのみ該当の変数が有効になるためendwith以降のテンプレートでは対象の変数を参照することができなくなります。jsで言うとsetがvar、letがwithとかにちょっと近いかもしれません。

変数への値の設定の記述の仕方はsetとほぼ同じです。{% with 変数名 = 値 %}といった書き方をします。

from jinja2 import Template

sql: str = (
    '{% with limit_num = 10 %}'
    'SELECT * FROM any_db.any_table LIMIT {{ limit_num }}'
    '{% endwith %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 10

もし{% endwith %}の記述を忘れた場合にはエラーになります。

from jinja2 import Template

sql: str = (
    '{% with limit_num = 10 %}'
    'SELECT * FROM any_db.any_table LIMIT {{ limit_num }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
TemplateSyntaxError: Unexpected end of template. Jinja was looking for the following tags: 'endwith'. The innermost block that needs to be closed is 'with'.

また、{% endwith %}の後に参照しようとしても値を取得することはできません。エラーにはならないものの空文字のような扱いになります。以下のコード例{% endwith %}の外側で変数に参照ができないためにLIMIT 10とはならずにLIMIT のみで数値部分が欠落していることが確認できます。

from jinja2 import Template

sql: str = (
    '{% with limit_num = 10 %}'
    'SELECT * FROM any_db.any_table '
    '{% endwith %}'
    'LIMIT {{ limit_num }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 

文字列の変数の定義

文字列の変数を設定するには対象の文字列部分をクォーテーションで囲みます。

from jinja2 import Template

sql: str = (
    "{% set table_name = 'any_table' %}"
    'SELECT * FROM any_db.{{ table_name }} LIMIT 10'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

シングルクォーテーションでもダブルクォーテーションでもどちらでも動きます。以下ではダブルクォーテーションを使用していますが同じ挙動になります。

from jinja2 import Template

sql: str = (
    '{% set table_name = "any_table" %}'
    'SELECT * FROM any_db.{{ table_name }} LIMIT 10'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 10

クォーテーションで囲まれていない場合にはエラーにはならないものの値への参照はできない(空文字のような挙動になる)ようです。以下のコード例ではテーブル名部分が欠落していることが確認できます。

from jinja2 import Template

sql: str = (
    '{% set table_name = any_table %}'
    'SELECT * FROM any_db.{{ table_name }} LIMIT 10'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db. LIMIT 10

真偽値の変数の定義

真偽値はPythonと同じようなTrue / Falseと先頭を大文字にする形であったり、true / falseといった小文字表記でも動作するようです。ただしTRUE / FALSEといった全て大文字だと正常に動かないようです。

SQLのテキストではtrueもしくはTRUEといった形で書くことが多いでしょうから、SQLとかではテンプレートもtrueで統一とかしても良いかもしれません。

以下では「LIMIT句を使うかどうか」という真偽値としてuse_limit_clauseという変数を設定しています。まずはTrueの指定で試してみます。if文の記述を使っていますがこの辺りは後々の節で詳しく触れます。

from jinja2 import Template

sql: str = (
    '{% set use_limit_clause = True %}'
    'SELECT * FROM any_db.any_table '
    '{% if use_limit_clause %}'
    'LIMIT 10'
    '{% endif %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

以下のようにLIMIT句込みで出力されます。

SELECT * FROM any_db.any_table LIMIT 10

今度はuse_limit_clause変数にFalseを指定してみます。

from jinja2 import Template

sql: str = (
    '{% set use_limit_clause = False %}'
    'SELECT * FROM any_db.any_table '
    '{% if use_limit_clause %}'
    'LIMIT 10'
    '{% endif %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

以下のようにLIMIT句が消えました。

SELECT * FROM any_db.any_table 

変数にtrueの小文字を指定してみます。

from jinja2 import Template

sql: str = (
    '{% set use_limit_clause = true %}'
    'SELECT * FROM any_db.any_table '
    '{% if use_limit_clause %}'
    'LIMIT 10'
    '{% endif %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

こちらも正常に動作してLIMIT句が出力されていることが確認できます。

SELECT * FROM any_db.any_table LIMIT 10

最後にTRUEと大文字で変数に値を指定してみます。

from jinja2 import Template

sql: str = (
    '{% set use_limit_clause = TRUE %}'
    'SELECT * FROM any_db.any_table '
    '{% if use_limit_clause %}'
    'LIMIT 10'
    '{% endif %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

LIMIT句が消えてしまいました。TRUEといった大文字の指定だと正常に動かないことが確認できます。

SELECT * FROM any_db.any_table 

リスト(配列)の変数の定義

リスト(配列)の定義には[]の括弧を使い、各値の間にコンマを挟みます。[100, 200, 300]といったような書き方になります。

以下のコード例ではSQLのカラム名部分をリストの変数として['column_1', 'column_2', 'column_3']と定義しています。forループの記述などを使っていますがこの辺りは後の節で詳しく触れます。

from jinja2 import Template

sql: str = (
    "{% set columns = ['column_1', 'column_2', 'column_3'] %}"
    'SELECT '
    '{% for column_name in columns %}'
    '{{ column_name }}'
    '{% if not loop.last %}, {% endif %}'
    '{% endfor %}'
    ' FROM any_db.any_table LIMIT 10'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT column_1, column_2, column_3 FROM any_db.any_table LIMIT 10

辞書の変数の定義

辞書の変数を定義するにはPythonと同様に{}の括弧を使い、コロンの左側にキー名、右側に値を設定し、各値の間はコロンで区切ります(例 : {'any_key_1': 'any_value_1', 'any_key_2': 'any_value_2'})。

以下のコード例ではWHERE句に指定する条件のカラム名を辞書のキー、条件値を辞書の値に設定しています({'date': '2022-01-01', 'device_type': 'Android'}部分)。

from jinja2 import Template

sql: str = (
    "{% set conditions = {'date': '2022-01-01', 'device_type': 'Android'} %}"
    'SELECT * FROM any_db.any_table'
    '\nWHERE '
    '{% for column_name, condition_value in conditions.items() %}'
    '{% if not loop.first %}'
    '\nAND '
    '{% endif %}'
    "{{ column_name }} = '{{ condition_value }}'"
    '{% endfor %}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)

辞書のキーと値がWHERE句にそれぞれ展開されていることを確認できます。

SELECT * FROM any_db.any_table
WHERE date = '2022-01-01'
AND device_type = 'Android'

基本的な計算

Jinjaの記述内で加算や乗算などの基本的な各計算を行うことができます。それぞれ以下の記号を使います。

  • 加算: +
  • 減算: -
  • 除算: /
  • 除算(切り捨て): //
  • 剰余: %
  • 乗算: *
  • 累乗: **

以下のコード例では{% set value_2 = value_1 + 20 %}といったように加算計算を行って結果を表示しています。

from jinja2 import Template

txt: str = (
    "{% set value_1 = 10 %}"
    "{% set value_2 = value_1 + 20 %}"
    "結果の値は{{ value_2 }}です。"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
結果の値は30です。

インデックス参照

リスト(配列)のインデックス参照

リストの配列の特定のインデックスの値を参照する場合には変数に[インデックス番号]という形で添え字を付けます(例 : limit_numbers[0])。Julia言語とかPrestoとかのSQLだとインデックスは1からスタートしたりしますがJinjaではPythonと同様にインデックスは0からのスタートとなります。

以下のコード例ではlimit_numbersというリストの先頭の値([0])をLIMIT句に設定しています。

from jinja2 import Template

sql: str = (
    "{% set limit_numbers = [100, 200, 300] %}"
    'SELECT * FROM any_db.any_table LIMIT {{ limit_numbers[0] }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 100

また、以下のようにインデックスに別の整数の変数を指定することもできます。set target_index = 1と対象のインデックスの変数を定義してlimit_numbers[target_index]という記述で配列の特定のインデックスにアクセスしています。

from jinja2 import Template

sql: str = (
    "{% set limit_numbers = [100, 200, 300] %}"
    "{% set target_index = 1 %}"
    "SELECT * FROM any_db.any_table LIMIT {{ limit_numbers[target_index] }}"
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 200

インデックスには負の値も指定できます。-1でリストの最後の値、-2でリストの最後から2番目の値・・・となっていきます(以下のコード例ではlimit_numbers[-1]と指定してリストの最後の値を参照しています)。

from jinja2 import Template

sql: str = (
    "{% set limit_numbers = [100, 200, 300, 300] %}"
    'SELECT * FROM any_db.any_table LIMIT {{ limit_numbers[-1] }}'
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 300

辞書の特定のキーの値の参照

辞書の特定のキーの値もリストと同様に[キーの値]という形で[]の括弧を使う形でアクセスができます。文字列や整数などがキーに設定できます。

以下のコード例では100200といったキーを持つlimit_clausesという辞書の変数を使用して、{{ limit_clauses[100] }}といったキーの値の指定を行って辞書の特定の値を参照しています。

from jinja2 import Template

sql: str = (
    "{% set limit_clauses = {100: 'LIMIT 100', 200: 'LIMIT 200'} %}"
    "SELECT * FROM any_db.any_table {{ limit_clauses[100] }}"
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 100

[100]といったように括弧を使わずに.を使ってアクセスする書き方も使えます。例えば.100と書いても前述のコードと同じ挙動になります。

from jinja2 import Template

sql: str = (
    "{% set limit_clauses = {100: 'LIMIT 100', 200: 'LIMIT 200'} %}"
    "SELECT * FROM any_db.any_table {{ limit_clauses.100 }}"
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 100

リストと同じようにキーには別の変数も設定することができます。

from jinja2 import Template

sql: str = (
    "{% set limit_clauses = {100: 'LIMIT 100', 200: 'LIMIT 200'} %}"
    "{% set target_key = 200 %}"
    "SELECT * FROM any_db.any_table {{ limit_clauses[target_key] }}"
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT * FROM any_db.any_table LIMIT 200

スライス制御

リストの配列はPythonと同じように[開始インデックス:終了インデックス]といったような書き方でスライスなどができます。終了インデックスの値と同じインデックスは結果には含まれません(Pythonと同様に開始インデックス~終了インデックス - 1の範囲の値となります)。

そのほかにも開始インデックスのみ指定したり終了インデックスのみ指定する書き方、その他3つ目の数値を指定して飛び飛びの対象のインデックスの値を参照したり・・・といったこともできます。

スライス結果は配列の値になります。

開始インデックスのみ指定する例([3:]という指定をしているのと開始は0のインデックスとなるので4番目以降の値が残ります) :

from jinja2 import Template

txt: str = (
    "{% set array = [100, 200, 300, 400, 500, 600, 700] %}"
    "{{ array[3:] }}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
[400, 500, 600, 700]

終了インデックスのみ指定する例([:3]という指定をしているので0~2のインデックスの値が残ります) :

from jinja2 import Template

txt: str = (
    "{% set array = [100, 200, 300, 400, 500, 600, 700] %}"
    "{{ array[:3] }}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
[100, 200, 300]

開始と終了両方を指定する例([2:5]という指定をしているので2~4のインデックスの値が残ります) :

from jinja2 import Template

txt: str = (
    "{% set array = [100, 200, 300, 400, 500, 600, 700] %}"
    "{{ array[2:5] }}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
[300, 400, 500]

飛び飛びの形で各インデックスの値を参照したい場合には3つ目の数値を指定します。例えば3つ目の数字に2を指定すれば1つ飛ばしに、3を指定すれば2つ飛ばしで各インデックスの値を参照します。

以下の例では[1:7:2]という指定をしているので1~6のインデックス範囲で1つ飛ばしとなるため結果は[200, 400, 600]というリストになります。

from jinja2 import Template

txt: str = (
    "{% set array = [100, 200, 300, 400, 500, 600, 700, 800, 900] %}"
    "{{ array[1:7:2] }}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
[200, 400, 600]

1つ目と2つ目の数値の指定が不要な場合には数値を省略してコロンだけ書く形(例 : [::2]など)でも書くことができます。

from jinja2 import Template

txt: str = (
    "{% set array = [100, 200, 300, 400, 500, 600, 700, 800, 900] %}"
    "{{ array[::2] }}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
[100, 300, 500, 700, 900]

ループ制御

for文によるループ

Jinjaでループの記述を使うには{% for ... in リストやrange関数など %}といった記述をします(例 : {% for value in [100, 200, 300] %})。ループの終わりの箇所には{% endfor %}の記述が必要になります。

range関数やリスト、辞書に対するループはそれぞれ後の節で詳しく触れます。

from jinja2 import Template

txt: str = (
    "{% for value in [100, 200, 300] %}"
    "{{ value }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
100 200 300 

range関数によるループ

単純にN回ループを回したいとか、もしくは連番を得たい場合にはPythonと同様にfor文でrange関数を指定します。第一引数にループを回したい件数の整数を指定します。

ループ上の値(インデックス)は0から第一引数の整数 - 1の範囲で設定されます。例えば第一引数に10を指定した場合0~9の範囲の10件の値でループが実行されます。

from jinja2 import Template

txt: str = (
    "{% for i in range(10) %}"
    "{{ i }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
0 1 2 3 4 5 6 7 8 9 

インデックスの開始値を変更したい場合には第一引数に開始値、第二引数に最後の値 + 1した値を指定します。例えばrange(5, 10)とすれば5~9の5件のループが実行されます。

from jinja2 import Template

txt: str = (
    "{% for i in range(5, 10) %}"
    "{{ i }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
5 6 7 8 9 

飛び飛びにループの値を設定したい場合には第三引数を指定します。2を指定すれば1つ飛ばしに、3を指定すれば2つ飛ばしにループの値が設定されます。飛び飛びになる分最終値にも早く到達するためループの回数自体も減ります。

以下のコード例では第三引数に2を指定しているので1つ飛ばしとなり2, 4, 6, ...という結果になります。

from jinja2 import Template

txt: str = (
    "{% for i in range(2, 11, 2) %}"
    "{{ i }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
2 4 6 8 10 

リスト(配列)に対するループ

リストの値に対するループも設定できます。以下の[100, 200, 300]部分のように直接for文内にリストの定義を書くことができます。

from jinja2 import Template

txt: str = (
    "{% for value in [100, 200, 300] %}"
    "{{ value }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
100 200 300 

リストの変数を渡すこともできます。

from jinja2 import Template

txt: str = (
    "{% set arr = [100, 200, 300, 400, 500] %}"
    "{% for value in arr %}"
    "{{ value }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
100 200 300 400 500

辞書に対するループ

辞書に対するループはリストと同様にfor文の中で直接辞書を定義する形でも変数を指定する形でも両方とも動きます。

辞書の値単体で指定した場合にはループの値はキーの値となります。以下のコードでは辞書のキーのa, b, cのそれぞれが出力されていることを確認できます。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for value in dict_val %}"
    "{{ value }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
a b c 

ループの値を辞書のキーではなく値で行いたい場合には辞書の指定時にvalues()メソッドを付けます。以下のコード例ではa, b, cといったキーの代わりに100, 200, 300といった辞書の値が出力されていることを確認できます。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for value in dict_val.values() %}"
    "{{ value }} "
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
100 200 300 

辞書のキーと値を同時にループの値に展開したい場合には辞書にitems()メソッドを付けます。key, valueという形で値の方はコンマ区切りで指定します。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for key, value in dict_val.items() %}"
    "key: {{ key }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
key: a, value: 100
key: b, value: 200
key: c, value: 300

ループの現在のインデックス番号を取得する(0~)

ループ処理で0(もしくは1)からの連番のリストなどを指定した場合以外(辞書であったり連番になっていないリストなど)でもループ回数のインデックス(何回目のループなのか)を取りたいことがあります。

そういった場合にはJinjaではPythonで言うところのenumerate的な関数は無く、代わりにloop.index0という記述をループ内で使います。Djangoだとforloop.counter0とかになっていたりでこの辺は差があるので注意が必要です(Jinjaの記事を見ていたらDjango側の記述で書かれているものがあり、普通に同じかと思ったもののエラーになったので何故なのか数分悩みました・・・。もしかしたらJinjaのメジャーバージョンアップで途中から変わった・・・とかもあるのかもしれません?)。

index0という形で末尾に0が付いていることからも分かる通り、この記述の場合インデックスは0からスタートします。以下の例では[100, 200, 300]というリストに対してループを回し、ループの各インデックスを取得しています。

from jinja2 import Template

txt: str = (
    "{% set list_val = [100, 200, 300] %}"
    "{% for value in list_val %}"
    "ループのインデックス: {{ loop.index0 }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
ループのインデックス: 0, value: 100
ループのインデックス: 1, value: 200
ループのインデックス: 2, value: 300

辞書などに対するループでももちろん参照できます。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for key, value in dict_val.items() %}"
    "ループのインデックス: {{ loop.index0 }}, key: {{ key }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
ループのインデックス: 0, key: a, value: 100
ループのインデックス: 1, key: b, value: 200
ループのインデックス: 2, key: c, value: 300

ループの現在のインデックス番号を取得する(1~)

ループのインデックスが0からではなく1からの連番で欲しい場合にはloop.index0の代わりにloop.indexを使います(PythonとかではなくSQL(Presto)とかだとインデックスが1~になるのでこちらの方が自然な場合があります)。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for key, value in dict_val.items() %}"
    "ループのインデックス: {{ loop.index }}, key: {{ key }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
ループのインデックス: 1, key: a, value: 100
ループのインデックス: 2, key: b, value: 200
ループのインデックス: 3, key: c, value: 300

ループの現在の末尾からのインデックス番号を取得する(0~)

私は今のところそういった使い方をしないといけないケースは稀・・・なのですが、ループのインデックスを降順に取得することもできます。そういった場合には(インデックスを0からスタートしたい場合には)loop.revindex0といったようにrevを付ける形にします。

たとえば3件の値を持つデータであれば最初は2、次は1、最後に0・・・といったようにループのインデックスが降順になります(ループの値自体は変わらずに順番に先頭から処理されていきます)。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for key, value in dict_val.items() %}"
    "末尾からのループのインデックス: {{ loop.revindex0 }}, key: {{ key }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
末尾からのループのインデックス: 2, key: a, value: 100
末尾からのループのインデックス: 1, key: b, value: 200
末尾からのループのインデックス: 0, key: c, value: 300

ループの現在の末尾からのインデックス番号を取得する(1~)

index0indexの関係のように、ループのインデックスを降順且つ1以上の値で取りたい場合にはrevindex0の代わりにrevindexを指定することで取ることができます。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for key, value in dict_val.items() %}"
    "末尾からのループのインデックス: {{ loop.revindex }}, key: {{ key }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
末尾からのループのインデックス: 3, key: a, value: 100
末尾からのループのインデックス: 2, key: b, value: 200
末尾からのループのインデックス: 1, key: c, value: 300

ループの最初かどうかの真偽値を取得する

ループ内ではループの先頭かどうかの真偽値を取得することができます。後の節で詳しく触れますがif文などの条件分岐などと組み合わせたりして制御を行うことができます。対象の真偽値はループ内でloop.firstという記述で参照することができます。

from jinja2 import Template

txt: str = (
    "{% set dict_val = {'a': 100, 'b': 200, 'c': 300} %}"
    "{% for key, value in dict_val.items() %}"
    "{% if loop.first %}"
    "ループの先頭です。"
    "{% else %}"
    "ループの先頭ではありません。"
    "{% endif %}"
    "key: {{ key }}, value: {{ value }}\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
ループの先頭です。key: a, value: 100
ループの先頭ではありません。key: b, value: 200
ループの先頭ではありません。key: c, value: 300

ループの最後かどうかの真偽値を取得する

ループの最後かどうかの真偽値はloop.lastで取れます。Pythonだとリストの値などで末尾にコンマが付いていても通りますが、SQLとかだと例えばSELECT句での各カラム名の指定で末尾にコンマが入っているとエラーになってしまうので最後だけコンマを設定しないようにする・・・といったようなケースで便利です。

from jinja2 import Template

sql: str = (
    "{% set columns = ['user_id', 'time', 'device_type'] %}"
    "SELECT "
    "{% for column in columns %}"
    "{{ column }}"
    "{% if not loop.last %}"
    ", "
    "{% endif %}"
    "{% endfor %}"
    "\nFROM any_db.any_table"
)
template: Template = Template(source=sql)
rendered_sql: str = template.render()
print(rendered_sql)
SELECT user_id, time, device_type
FROM any_db.any_table

ネストしたループにおける外側のループを参照する

ループを2重に(もしくは私はあまり使うことはありませんが3重以上に)にする必要がある時があります。

そういった場合にはloopの参照は内側のループになります。一方で外側のループのインデックスが取りたい・・・とか外側のループの最後に処理を加えたい・・・といった場合には外側のループを参照したいことがあります。

Djangoとかだとloop.parentloopみたいな形で外側のループにアクセスができましたが、Jinjaの場合は公式ドキュメントや外側のループ内でそのループの値(loop)を別の変数名に設定してしまいましょう、といったような感じのようです。

参考:

例えばloopという自動で設定される変数は内側のループで上書きになってしまうので、外側のループ内で{% set parentloop = loop %}といったような記述をして別の変数名(この例ではparentloopという名前)に設定しておく、といった具合です。

以下のコード例では[{'a': 100, 'b': 200}, {'a': 300, 'b': 400}]という辞書を含んだリストの変数を定義し、外側のループではリストに対してループを行い、内側のループではさらに辞書とキーの値に対してループを行っています。

外側のループ内で{% set parentloop = loop %}という形でparentloopという変数名で外側のループの変数を設定しているので内側のループ内でparentlooploopという名前でそれぞれのループにアクセスできます(内側のループ内で外側のループのインデックスと内側のループのインデックスをそれぞれ出力しています)。

from jinja2 import Template

txt: str = (
    "{% set list_val = [{'a': 100, 'b': 200}, {'a': 300, 'b': 400}] %}"
    "{% for dict_val in list_val %}"
    "{% set parentloop = loop %}"
    "{% for key, value in dict_val.items() %}"
    "parent loop index: {{ parentloop.index0 }}, "
    "child loop index: {{ loop.index0 }} "
    "dict key: {{ key }}, dict value: {{ value }}\n"
    "{% endfor %}"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
parent loop index: 0, child loop index: 0 dict key: a, dict value: 100
parent loop index: 0, child loop index: 1 dict key: b, dict value: 200
parent loop index: 1, child loop index: 0 dict key: a, dict value: 300
parent loop index: 1, child loop index: 1 dict key: b, dict value: 400

特定の各値の繰り返しの値を取得する

loopのcycleメソッドを使うと各引数に指定した各値をループ内で順番に繰り返す制御を行うことができます。loop.cycle(任意の個数の引数)といったように書きます。ループ内で各引数の最後に到達した場合は最初の引数の値が参照されます。

以下の例では引数に'偶数', '奇数'の2つの値を設定しているため、ループの最初では偶数、次は奇数、その次は偶数・・・と順番にループの値を取ることができます。

from jinja2 import Template

txt: str = (
    "{% for i in range(10) %}"
    "ループのインデックス: {{ i }}, インデックスは{{ loop.cycle('偶数', '奇数') }}です。\n"
    "{% endfor %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
ループのインデックス: 0, インデックスは偶数です。
ループのインデックス: 1, インデックスは奇数です。
ループのインデックス: 2, インデックスは偶数です。
ループのインデックス: 3, インデックスは奇数です。
ループのインデックス: 4, インデックスは偶数です。
ループのインデックス: 5, インデックスは奇数です。
ループのインデックス: 6, インデックスは偶数です。
ループのインデックス: 7, インデックスは奇数です。
ループのインデックス: 8, インデックスは偶数です。
ループのインデックス: 9, インデックスは奇数です。

条件分岐

if文

if文による条件分岐を使うには{% if 条件式や真偽値など %}といった記述が必要になります。if文の終了部分には{% endif %}の記述が必要です。

条件を満たした場合そのif文内の記述は残り、条件を満たさなければその部分の記述はテキストから消えます。

等値条件(例 : if value == 10)や非等値条件(例 : if value != 10)、大なり小なりの条件(例 : if value > 10if value >= 10if value < 10if value <= 10)、各真偽値やもしくは他の型の値に対する判定、たとえば0かどうか、空文字かどうか、空のリストかどうか、空の辞書かどうかなどほぼPythonと同じような感じで使えます。

以下のコード例ではvalueという変数に100の値を設定して、その値が200以上であるか、100以上であるかの2つの条件でif文を設定しています。後者の100以上の条件のみ満たされるため、値は200以上になっています。という出力は残りません。

from jinja2 import Template

txt: str = (
    "{% set value = 100 %}"
    "{% if value >= 200 %}"
    "値は200以上になっています。"
    "{% endif %}"
    "{% if value >= 100 %}"
    "値は100以上になっています。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
値は100以上になっています。

elif文

if文の条件を満たさなかった場合に別の条件分岐を設定する・・・といった際にelif文が使えます。
{% if 条件式 %} -> {% elif 条件式 %} -> {% endif %}的な順番で書いていきます。{% endelif %}的な記述は不要で最後の{% endif %}のみ書く形になります。

以下のコード例はif文の節の内容とほぼ同じですが2つ目のif文の代わりにelif文を使っています。

from jinja2 import Template

txt: str = (
    "{% set value = 100 %}"
    "{% if value >= 200 %}"
    "値は200以上になっています。"
    "{% elif value >= 100 %}"
    "値は100以上になっています。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
値は100以上になっています。

else文

if文やelif文の各条件を満たさない場合の制御を行う場合にはelse文が使えます。

{% if 条件式 %} -> {% elif 条件式 %} -> {% else %} -> {% endif %}といった順番で書いていきます。間のelifは省略してif -> elseとしたり、もしくは複数のelifを挟んだりすることも可能です。

以下のコード例ではvalue変数に50を設定してif文ではその値が200以上か、elif文ではその値が100以上か、else文ではそれ以外の場合・・・としています。変数の値は50なのでelseの部分を通ります。

from jinja2 import Template

txt: str = (
    "{% set value = 50 %}"
    "{% if value >= 200 %}"
    "値は200以上になっています。"
    "{% elif value >= 100 %}"
    "値は100以上になっています。"
    "{% else %}"
    "値は100未満の値です。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
値は100未満の値です。

条件指定で使えるオペレーターやキーワード

前節までである程度触れてきましたが、以下のオペレーターなどが使用することができます。SQLとかでの否定条件としての<>的な指定は対象外です(!=のみのサポートです)。

  • 等値条件: ==
  • 非等値条件: !=
  • 超過条件: >
  • 以上の条件: >=
  • 未満の条件: <
  • 以下の条件: <=
  • AND条件(複数の条件を全て満たした場合にTrue): and
  • OR条件(いずれかの条件を満たした場合にTrue): or
  • 変数などの否定: not
  • 指定値がリストや辞書のキー・タプルなどに含まれるかどうか: in

notinだけ次の節で補足します。

変数の否定条件(not)

通常の左右の値の比較(例 : value == 10など)であれば!=の記号を使えば否定が行えますが、変数に対して否定を行いたい場合にはnotを変数名の前に付けます。

以下の例ではif文とnotを使用してリストが空の場合の制御を行っています。

from jinja2 import Template

txt: str = (
    "{% set value = [] %}"
    "{% if not value %}"
    "リストは空になっています。"
    "{% else %}"
    "リストは空になっていません。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
リストは空になっています。

指定値がリストや辞書のキー・タプルなどに含まれるかどうかの判定(in)

Pythonと同じ感じですが、inを使うことでリストや辞書のキー・タプル内に指定値が含まれるかどうかの真偽値を取得することができます。

例えば1 in [1, 2, 3]といったように書きます。1が指定値となります。[1, 2, 3]というリストになっており1は含まれるので結果はTrueとなります。

from jinja2 import Template

txt: str = (
    "{% if 1 in [1, 2, 3] %}"
    "1という値はリストに含まれています。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
1という値はリストに含まれています。

試しに4を指定してみるとリストに含まれていない判定になることが確認できます。

from jinja2 import Template

txt: str = (
    "{% if 4 in [1, 2, 3] %}"
    "4という値はリストに含まれています。"
    "{% else %}"
    "4という値はリストに含まれていません。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
4という値はリストに含まれていません。

辞書の場合はキーがチェック対象になります。

from jinja2 import Template

txt: str = (
    "{% if 'a' in {'a': 1, 'b': 2} %}"
    "aというキーは辞書に含まれています。"
    "{% else %}"
    "aというキーは辞書に含まれていません。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
aというキーは辞書に含まれています。

辞書の値でチェックしたい場合にはvalues()メソッドを辞書の部分に追加します。

from jinja2 import Template

txt: str = (
    "{% if 1 in {'a': 1, 'b': 2}.values() %}"
    "1という値は辞書に含まれています。"
    "{% else %}"
    "1という値は辞書に含まれていません。"
    "{% endif %}"
)
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
1という値は辞書に含まれています。

テンプレートの記法で余白を取り除く

Djangoとかもそうですが、Jinjaの記法を使っているとfor文やif文、変数展開などで発生する余分な改行やスペースなどが気になることがあります。特に対象がPythonであれば3連続したクォーテーションでの文字列で記述した場合やフォームなどでのユーザーの入力値(SQLなど)、HTMLなどで扱う場合空白行などが顕著です(SQLやHTMLであれば余分な改行などが入っても大半は実害が無いことが多いですが・・・)。

例えばif文で条件を満たさない箇所に関しては結果に残らなくなるものの、if文の領域の前後の改行はそのまま残るので結果の文字列にたくさんの改行が含まれてしまいます。

from jinja2 import Template

txt: str = """
{% set value = 50 %}
{% if value >= 200 %}
値は200以上になっています。
対象値: {{ value }}
{% elif value >= 100 %}
値は100以上になっています。
対象値: {{ value }}
{% else %}
値は100未満の値です。
対象値: {{ value }}
{% endif %}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)



値は100未満の値です。
対象値: 50

そういった場合は各記述に-の記述を追加すると前後の空白行や空白スペースなどを取り除いてくれます。Pythonで言うところの文字列のstripメソッドみたいな挙動をします。確かDjangoだとフィルター処理などしないといけなかった?気がするのでDjangoには無くJinja特有の書き方?かと思われます。

if文などで%記号を使う箇所であれば{%- ... -%}といったように%の記号の隣に-の記号を配置します。変数などであれば{{- 変数名 -}}といったように内側の括弧の隣に-の記号を配置します。

from jinja2 import Template

txt: str = """
{%- set value = 50 -%}
{%- if value >= 200 -%}
値は200以上になっています。
対象値: {{- value -}}
{%- elif value >= 100 -%}
値は100以上になっています。
対象値: {{- value -}}
{%- else -%}
値は100未満の値です。
対象値: {{- value -}}
{%- endif -%}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
値は100未満の値です。
対象値:50

結果の文字列から余分な改行などが削除されてすっきりしました。また、対象値:50といったようにコロンと数字の間のスペースも消えている点も注目してください。これは{{- value -}}といったように変数にも-の記号を付与しているため前後の余分なスペースが削除されていることによる挙動となります。

-の記号は必ずしも先頭と最後の方の両方に付けないといけないというものではなく、片方だけ設定した場合はそちらの方向のみ空白行などの削除が実行されます。

例えば{%- if value >= 200 %}といったように左側のみ-の記号があればif文の左側に対して削除処理が実行され、逆に右側にのみ-の記号が付いていれば右側のみ空白行などが削除されます。

コメント

コメントとして何らかの説明を記述しつつ、結果のテキストには残さないようにしたい・・・といった場合には{# コメント内容 #}という書き方を使うことができます。この書き方はDjangoとも一緒です。

以下の例では{# 各日付ごとの売り上げの集計値を取得するSQLです。 #}という説明のコメントを追加しています。このテキストは結果のテキストには含まれなくなります。

from jinja2 import Template

txt: str = """
{# 各日付ごとの売り上げの集計値を取得するSQLです。 #}
{%- set limit_num = 100 -%}
SELECT date, sales FROM any_db.any_table LIMIT {{ limit_num }};
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)

SELECT date, sales FROM any_db.any_table LIMIT 100;

コメントも前節で触れたものと同様に-の記号を付与することで余分な前後の改行などを取り除くことができます(例 : {#- 各日付ごとの売り上げの集計値を取得するSQLです。 -#})。

エスケープ

場合によっては{{{%などの記述をそのまま表示したい時があるかもしれません。そういった場合のエスケープ処理にはいくつか方法があります。

1つ目は{{などの記述自体を文字列の変数として扱ってしまう方法です。

{{ }}の記述は変数参照というのは前節までで触れた通りですが、その中で文字列の固定値であればそのまま文字列の値として展開されます。つまり{{ }}の記述の中に'{{'といったようにクォーテーション付きで文字列として記述すればそのまま括弧が残ります。

from jinja2 import Template

txt: str = """
{{ '{{' }}エスケープしているので括弧はそのまま表示されます{{ '}}' }}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
{{エスケープしているので括弧はそのまま表示されます}}

もしくは括弧などが多く特定の範囲でまとめてエスケープしたい・・・といった場合には{% raw %}のブロックを記述することでも対応ができます。開始位置を{% raw %}、終了位置を{% endraw %}で指定します。このブロック内の記述はそのまま表示されます。

from jinja2 import Template

txt: str = """
{% raw %}
{{エスケープしているので括弧がそのまま表示されます}}
{% for %}for文などの括弧もそのまま表示されます。{% endfor %}
{% endraw %}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
{{エスケープしているので括弧がそのまま表示されます}}
{% for %}for文などの括弧もそのまま表示されます。{% endfor %}

Pythonビルトインのメソッドを使う

Djangoのテンプレートだとフィルターなどの記述となって基本的には文字列などのビルトインのメソッドは直接使えない・・・?と(少しうろ覚えですが)記憶していますが、Jinjaでも文字列のPythonビルトインのメソッドなどはそのまま使用することができます。

以下の例ではビルトインのupperメソッドを使って文字列の変数の値を大文字にして結果に出力しています。

from jinja2 import Template

txt: str = """
{% set text = 'hello!' %}
{{ text.upper() }}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
HELLO!

フィルター

フィルターの基本的な使い方

Djangoと同様にフィルターの機能もあります。フィルター機能は変数に対する関数のような動作をします。DjangoとJinja双方でフィルターを使う際には|のパイプの記号を使うのは同様です。

フィルターは対象の変数の後に|の記号を書き、その後に対象のフィルター名を記述します。例えば変数の絶対値を取るフィルターのabsであれば以下のようにvalue|absといった具合に書きます。

from jinja2 import Template

txt: str = """
{% set value = -50 %}
{{ value|abs }}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
50

引数を取るフィルターも存在します。引数の指定は()の括弧を使います。Djangoだとコロンの記号を使ったりするのでこの点はDjangoと異なるようです。例えばjoinのフィルターでは連結で使用する文字列の引数が必要となります。以下の例では' - 'の文字列で配列をjoinのフィルターで連結しています。

from jinja2 import Template

txt: str = """
{% set value = [100, 200, 300] %}
{{ value|join(' - ') }}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
100 - 200 - 300

ビルトインフィルター

Jinjaには前節で触れたabsやjoinなどの他にも様々なビルトインフィルターが用意されています。数が多いので本記事では1つ1つは触れません。必要に応じて公式ドキュメントなどをご確認ください。

image.png

※スクショは以下の公式ドキュメントの表から引用しています。

独自のフィルターを追加する

独自のフィルターとしてなんらか関数を登録したい場合には現状Templateクラスでストレートには対応はできないようです。テンプレートの環境設定などを扱うEnvironmentクラスを挟んでEnvironmentクラスからテンプレートのインスタンスを作成する必要があります。

以下のような手順が必要になります。

  • 【1】jinja2パッケージからEnvironmentクラスをimportします。
  • 【2】登録したい関数を用意します。
  • 【3】Environmentクラスのインスタンスを作成します。
  • 【4】Environmentクラスのインスタンスのfilters属性に登録したい関数を指定します。
  • 【5】Environmentクラスのインスタンスのfrom_stringメソッドでテキストを指定してテンプレートのインスタンスを作成します。

コードにしてみると以下のようになります。登録した関数は値を10倍するだけのシンプルなものになっています。インラインコメントの【】部分の数字は前述のリストに該当します。

from jinja2 import Template
from jinja2 import Environment  # 【1】


def multiply_10(value: int) -> int:  # 【2】
    return value * 10


txt: str = """
{% set value = 1000 %}
{{ value|multiply_10 }}
"""
env: Environment = Environment()  # 【3】
env.filters['multiply_10'] = multiply_10  # 【4】
template: Template = env.from_string(source=txt)  # 【5】
rendered_txt: str = template.render()
print(rendered_txt)

結果として登録した関数による値の10倍の処理を通した結果を得られます。

10000

登録した関数では第一引数はフィルターを反映する変数となります。フィルターで引数を使う場合には第二引数以降を関数にて定義します。以下の例では10倍するのではなく引数でn倍する形にしています(関数に第二引数のnを設定しています)。

from jinja2 import Template
from jinja2 import Environment


def multiply_n(value: int, n: int) -> int:
    return value * n


txt: str = """
{% set value = 1000 %}
{{ value|multiply_n(5) }}
"""
env: Environment = Environment()
env.filters['multiply_n'] = multiply_n
template: Template = env.from_string(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
5000

ブロック単位でフィルターを設定する

ブロック単位でまとめてフィルターを設定するということもできます。{% filter 設定するフィルター名 %}といった書き方をします。フィルターの終了箇所には{% endfilter %}の記述が必要です。

以下のコード例ではupperのビルトインフィルターをブロック内のテキスト全体に反映して全て大文字にしています。

from jinja2 import Template

txt: str = """
{% filter upper %}
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
{% endfilter %}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT,
SED DO EIUSMOD TEMPOR INCIDIDUNT UT LABORE ET DOLORE MAGNA ALIQUA.

検証用のTests機能

JinjaにはTestsと呼ばれる、if文などで便利な機能が用意されています。例えば「特定の値で割り切れるかどうか」「奇数かどうか」「全て大文字かどうか」といった判定を行うことができます。他の機能を使ったりビルトインメソッドを使ったりすることで同じようなことはできますが、記述をシンプルにしたり等を行うことができます。

書き方としては<変数など> is 対象のTestsといったように書きます。例えば3で割り切れるかどうかといった判定であればdivisibleby を使ってvalue is divisibleby 3といったような書き方ができます。括弧を使ってdivisibleby(3)といったように書くこともできます。複数の引数を必要とするTestsの場合には括弧を使う後者の書き方が必要です。

from jinja2 import Template

txt: str = """
{% set value = 30 %}
{% if value is divisibleby(3) %}
{{ value }}は3で割り切れます。
{% else %}
{{ value }}は3で割り切れません。
{% endif %}
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
30は3で割り切れます。

他にも様々なビルトインのTestsのインターフェイスがあります。本記事では1つ1つ説明はしないため必要な方は公式ドキュメントをご確認ください。

image.png

※スクショは以下の公式ドキュメントの表部分から引用しています。

マクロ

マクロ機能は自分で定義する関数のように動作します。書き方は{% macro 関数定義 %}という書き方が必要になります。マクロ定義の最後には{% endmacro %}という記述が必要です。

関数定義部分は関数名(引数)といったように書きます(例 : any_func(x, y, z))。

{% macro ... %} ~ {% macro %}の間のブロック部分の記述が返却値的な値になります。このブロック内では引数に指定された値を参照することができます。

マクロの呼び出しは関数呼び出しのように{{ マクロ名(引数) }}といったように書きます(例 : any_func(10, 20, 30))。

例えばSQLで何らかの句を生成するマクロを考えてみます。サンプルとしてシンプルなLIMIT句を作成するcreate_limit_clauseという関数のマクロを作ってみると以下のようになります。引数にはLIMIT句の数値としてnという引数を受け付けるようにしています。

from jinja2 import Template

txt: str = """
{%- macro create_limit_clause(n) -%}
LIMIT {{ n }}
{%- endmacro -%}
SELECT * FROM any_db.any_table
{{ create_limit_clause(10) }};
"""
template: Template = Template(source=txt)
rendered_txt: str = template.render()
print(rendered_txt)
SELECT * FROM any_db.any_table
LIMIT 10;

今回のコード例だとあまりメリットが伝わりにくいかもしれませんが、一部のパラメーターのみ変わるものの同じような記述が連続する場合などに記述を統一したりすることができます。

参考サイト・参考文献・参考イベントまとめ

51
56
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
56