Jinja2のWhitespace Control

  • Python 3.6.5
  • Jinja 2.10

Jinja2でforifを使ってテンプレートを書くと、出力される文字列に空行が多く入ってしまう。これはforやifのみが書いてある行にある改行も出力されてしまうから。

Jinja2にはWhitespace Controlという機能があり、この出力を制御できる。

Whitespace Controlでは+-記号を使うのだが、空行削除の目的では-をforやifにつける。e.g. {%- if ... %}

-をつける場所は4か所あり、どこにつけるかによって挙動が変わる。具体的に出力を見るほうが理解しやすいため、4か所それぞれに-をつけて出力を見ていく。

for文でのWhitespace Control挙動確認

用意したのは以下の通り何も設定しない場合と4パターン。

#!/usr/bin/env python

from jinja2 import Template

print('======================')
print('何も設定していない場合')
print(Template('''before for line.
{% for i in [1, 2, 3] %}
    {{ i }}
{% endfor %}
after for line.''').render())

print('======================')
print('forの前に設定')
print(Template('''before for line.
{%- for i in [1, 2, 3] %}
    {{ i }}
{% endfor %}
after for line.''').render())

print('======================')
print('forの後に設定')
print(Template('''before for line.
{% for i in [1, 2, 3] -%}
    {{ i }}
{% endfor %}
after for line.''').render())

print('======================')
print('endforの前に設定')
print(Template('''before for line.
{% for i in [1, 2, 3] %}
    {{ i }}
{%- endfor %}
after for line.''').render())

print('======================')
print('endforの後に設定')
print(Template('''before for line.
{% for i in [1, 2, 3] %}
    {{ i }}
{% endfor -%}
after for line.''').render())

このPythonスクリプトを実行すると、出力は以下の通りになる。

======================
何も設定していない場合
before for line.

    1

    2

    3

after for line.
======================
forの前に設定
before for line.
    1

    2

    3

after for line.
======================
forの後に設定
before for line.
1
2
3

after for line.
======================
endforの前に設定
before for line.

    1
    2
    3
after for line.
======================
endforの後に設定
before for line.

    1

    2

    3
after for line.

for, endforともに何も設定していない場合

{% for ... %}の行の改行がそのまま出力に反映されているため、各出力行の前に空行が出力されている。

ループの中にある構造は{{ i }}だけでなく、for文と同じ行にある改行も含まれている。

for文と同じ行にある改行
    {{ i }}改行

さらにendfor文の行末にも改行があるので、各出力行の前後に空行が出力されることになる。

before for line.
for行の改行
    1
for行の改行
    2
for行の改行
    3
endfor行の改行
after for line.

forの前に設定されている場合

{%- for ... %}の前の改行とスペースが削られている。つまりbefore for line.の改行がなくなり、for文の行の改行が次にきている。

forの後に設定されている場合

{% for ... -%}の後の改行とスペースが削られている。つまりfor文の行の改行と{{ i }}自体のスペースがなくなっている。

endforの前に設定されている場合

{%- endfor %}の前の改行とスペースが削られている。つまりループで毎回出力されているのは、for文の行の改行と{%- endfor %}によって改行が削られた{{ i }}となる。

ループごとの出力を[]でくくってみる。

before for line.
[for行の改行
    1][for行の改行
    2][for行の改行
    3]endfor行の改行
after for line.

endforの後に設定されている場合

{% endfor -%}の後の改行とスペースが削られている。つまりendfor文の行の改行がなくなり、after for line.が直後にきている。

パターンから見る原理原則

-がついた箇所が制御文の前なのか後なのかで挙動が変わり、前であればその制御文の前の改行とスペースが、後であればその制御文の後の改行とスペースが消えてなくなるという仕組みになっていることが、4パターンの出力結果を見比べると把握しやすい。

for文でよく使うWhitespace Controlの設定場所

forとendforを消す

先ほどの出力結果パターンでいうと、多くの場合期待されるものは以下になるだろう。

before for line.
    1
    2
    3
after for line.

「endforの前に設定」パターンが一番近いが、ループがfor行の改行から始まるために、二行目だけ空行ができてしまう。

before for line.
[for行の改行
    1][for行の改行
    2][for行の改行
    3]endfor行の改行
after for line.

before for line.行の改行が消せれば、二行目の改行が一行目に繰り上がり、期待する出力となる。そのため「forの前に設定」を組み合わせればいい。

print('======================')
print('forの前とendforの前に設定')
print(Template('''before for line.
{%- for i in [1, 2, 3] %}
    {{ i }}
{%- endfor %}
after for line.''').render())

期待通りの出力結果が得られた。

======================
forの前とendforの前に設定
before for line.
    1
    2
    3
after for line.

改行なしで連続で出力する

もうひとつの期待される出力は連続で1行で値を出力するもの

before for line.
123
after for line.

「forの後に設定」で{{ i }}の前のスペースを、「endforの前に設定」で{{ i }}の後の改行を、それぞれ削除できる。

print('======================')
print('forの後とendforの前に設定')
print(Template('''before for line.
{% for i in [1, 2, 3] -%}
    {{ i }}
{%- endfor %}
after for line.''').render())

if文でのWhitespace Control挙動確認

if文の原理原則もfor文と同じだが、繰り返し構文ではないため、Whitespace Controlをendifの前につけたときと後につけたときで出力差がなくなっている。

print('======================')
print('何も設定していない場合')
print(Template('''before if line.
{% if True %}
    True
{% endif %}
after if line.''').render())

print('======================')
print('ifの前に設定')
print(Template('''before if line.
{%- if True %}
    True
{% endif %}
after if line.''').render())

print('======================')
print('ifの後に設定')
print(Template('''before if line.
{% if True -%}
    True
{% endif %}
after if line.''').render())

print('======================')
print('endifの前に設定')
print(Template('''before if line.
{% if True %}
    True
{%- endif %}
after if line.''').render())

print('======================')
print('endifの後に設定')
print(Template('''before if line.
{% if True %}
    True
{% endif -%}
after if line.''').render())

出力は次の通り。

======================
何も設定していない場合
before if line.

    True

after if line.
======================
ifの前に設定
before if line.
    True

after if line.
======================
ifの後に設定
before if line.
True

after if line.
======================
endifの前に設定
before if line.

    True
after if line.
======================
endifの後に設定
before if line.

    True
after if line.

if文でよく使うWhitespace Controlの設定場所

こちらもfor文と同じ考えで問題ない。次の二つがよくある期待出力と設定位置となる。

======================
ifの前とendifの前に設定
before if line.
    True
after if line.
======================
ifの後とendifの前に設定
before if line.
True
after if line.

trim_blocksとlstrip_blocks

毎回-で挙動を制御してもいいのだが、trim_blockslstrip_blocksでテンプレート出力のデフォルト動作を変えることで、-を制御文に設定するのと似た動作をさせられる。

#!/usr/bin/env python

from jinja2 import Environment

template_str = '''before for line.
  {% for i in [1, 2, 3] %}
    {{ i }}
  {% endfor %}
after for line.'''

env = Environment()
tb_env = Environment(trim_blocks=True)
lb_env = Environment(lstrip_blocks=True)
tb_lb_env = Environment(trim_blocks=True, lstrip_blocks=True)

print('======================')
print('default settings')
print(env.from_string(template_str).render())

print('======================')
print('trim_blocks=True')
print(tb_env.from_string(template_str).render())

print('======================')
print('lstrip_blocks=True')
print(lb_env.from_string(template_str).render())

print('======================')
print('trim_blocks=True, lstrip_blocks=True')
print(tb_lb_env.from_string(template_str).render())

出力は以下の通り。

======================
default settings
before for line.
  
    1
  
    2
  
    3
  
after for line.
======================
trim_blocks=True
before for line.
      1
      2
      3
  after for line.
======================
lstrip_blocks=True
before for line.

    1

    2

    3

after for line.
======================
trim_blocks=True, lstrip_blocks=True
before for line.
    1
    2
    3
after for line.

trim_blocks{% for ... %}改行{% endfor %}改行の出力を消してくれる。lstrip_blocks{% for ... %}{% endfor %}と同じ行の文頭にスペースがあれば、それを消してくれる。(スペースなので視覚的に見れないが、上記出力をカーソルで選択して反転させると、lstrip_blocksTrueにしたときにforとendforの前にあるスペースが消えていることがわかる。)

両方のオプションを有効化すると、多くの場合期待される出力結果を得ることができる。

新規テンプレートを作成する場合にtrim_blockslstrip_blocksを有効にした方がいいのかどうかについて、個人的な結論はまだ出せていないが、以下の点を考慮して、-を使う場合の挙動をよく把握しておくことが大切だと思う。

  • -を使うほうが出力制御の幅が広い
  • trim_blocks, lstrip_blocksが有効化されていない既存のテンプレートを修正するときに、他に影響を与えず局所的に出力制御できる