naritoブログ

このブログはPython/DjangoとBootstrap4で作成されました
Githubのソースコード

Pythonで、string.Templateを使う

約153日前 2017年12月25日16:28
プログラミング関連
Python
Pythonの文字列メソッドにformatがあります。
これは非常に便利ですが、用途によってはもう少し他のものが欲しい、ということもあります。

例えばhtmlコードを作成したいとしましょう。以下のコードは動きません。
HTML = """\
<html>
  <head>
    <style>
      h1 {
        color:{0};
      }
    </style>
  </head>
  <body>
    <h1>ハロー</h1>
  </body>
</html>
"""

html = HTML.format('red')
print(html)



h1のあとの{}波括弧が、プレースホルダの{}として認識されるためですね。
以下のように波括弧部分を二重にすれば、上手く動作はします...
HTML = """\
<html>
  <head>
    <style>
      h1 {{
        color:{0};
      }}
    </style>
  </head>
  <body>
    <h1>ハロー</h1>
  </body>
</html>
"""



埋め込みたい部分が多くなると、全ての引数を渡すのに苦労してきます。
位置引数の指定ではなく、辞書を使って管理したくなるでしょう。
波括弧内に名前を入れて、その名前をキーとして持つ辞書を作成し、
format_map(context)を使うか、format(**context)とすることで可能です。
HTML = """\
<html>
  <head>
    <style>
      h1 {{
        color:{h1_css};
      }}
    </style>
  </head>
  <body>
    <h1>{message}</h1>
  </body>
</html>
"""

context = {
    'h1_css': 'red',
    'message': 'ハロー',
}
html = HTML.format_map(context)
print(html)



キーが足りなかったりすると、エラーになります。
context = {
    'h1_css': 'red',
}
html = HTML.format_map(context)
print(html)



辞書で管理しても、引数が多くなると管理が面倒です。
DjangoのテンプレートやJinja2のように、そもそもキーを渡さなかった場合は何も表示されなくした方が楽なこともあります。
そうなると、文字列のformatメソッドでは対応できません。他の方法として、string.Templateが使えます。


基本的な使い方は、以下のようになります。波括弧部分が$name のようになりました。
string.Template(HTML)としてベースの文字列を渡し、substituteメソッドを呼び出します。
import string

HTML = """\
<html>
  <head>
    <style>
      h1 {
        color:$h1_css;
      }
    </style>
  </head>
  <body>
    <h1>$message</h1>
  </body>
</html>
"""

context = {
    'h1_css': 'red',
    'message': 'ハロー',
}
template = string.Template(HTML)
html = template.substitute(context)
print(html)




$message worldはうまく動作しますが、$messageworld のように空白なしでworldという文字列をつなげたい場合はどうでしょうか。
$のあとに波括弧で囲むことができるので、それを使います。そうすると${message}world のように書けますね。
color:${h1_css};
<h1>${message}</h1>


substituteだとキーがなければエラーになるのですが、safe_substituteだと基のプレースホルダが入ります。
html = template.safe_substitute(context)



基本的な使い方は紹介したので本題に戻り、キーがなかった場合に空欄にするように変更しましょう。
string.Templateを継承したTemplateクラスを作り、renderというメソッドを作りました。よく使われる名前ですね。
safe_substituteのソースコードを基に、KeyError出たら空文字を返すようにしただけです。
from collections import ChainMap as _ChainMap
import string

HTML = """\
<html>
  <head>
    <style>
      h1 {
        color:${h1_css};
      }
    </style>
  </head>
  <body>
    <h1>${message}</h1>
  </body>
</html>
"""


class Template(string.Template):

    def render(*args, **kws):
        if not args:
            raise TypeError("descriptor 'safe_substitute' of 'Template' object "
                            "needs an argument")
        self, *args = args  # allow the "self" keyword be passed
        if len(args) > 1:
            raise TypeError('Too many positional arguments')
        if not args:
            mapping = kws
        elif kws:
            mapping = _ChainMap(kws, args[0])
        else:
            mapping = args[0]
        # Helper function for .sub()
        def convert(mo):
            named = mo.group('named') or mo.group('braced')
            if named is not None:
                try:
                    return str(mapping[named])
                except KeyError:
                    return ''  # ここを変えただけ
            if mo.group('escaped') is not None:
                return self.delimiter
            if mo.group('invalid') is not None:
                return mo.group()
            raise ValueError('Unrecognized named group in pattern',
                             self.pattern)
        return self.pattern.sub(convert, self.template)


context = {
    'h1_css': 'red',  # messageキーをなくしてテスト
}
template = Template(HTML)  # 新しいクラス
html = template.render(context)  # 新しく作ったrenderメソッド
print(html)



messageを渡さなかった場合、ちゃんと空欄になっていますね。
<html>
  <head>
    <style>
      h1 {
        color:red;
      }
    </style>
  </head>
  <body>
    <h1></h1>
  </body>
</html>





${name}ではなく、DjangoやJinja2のように{{ name }} としたい場合は、pattern属性を上書きして以下のようにします。
class Template(string.Template):

    # {{ }} で区切り、中身の空白は気にしない
    pattern = r'''
    \{\{[ ]*(?:
    (?P<escaped>\{\{[ ]*)|
    (?P<named>[_a-z][_a-z0-9]*)[ ]*\}\}|
    (?P<braced>[_a-z][_a-z0-9]*)[ ]*\}\}|
    (?P<invalid>)
    )
    '''