Python Templateクラスによる文字列の書式設定

Python Templates はデータを文字列に代入するために使われます。

Templatesを使うことで、文字列置換(または文字列補間)のためのカスタマイズ可能なインターフェイスを手に入れることができます。

Pythonは最近導入されたf-Stringを含め、すでに多くの文字列代入の方法を提供しています。

Templatesで文字列を置換することはあまり一般的ではありませんが、その威力は文字列の書式規則をカスタマイズできることにあります。

この記事では、Pythonの Template クラスを使って文字列をフォーマットします。

そして、テンプレートが文字列にデータを代入する方法を変更する方法を見ていきます。

これらのトピックをよりよく理解するためには、クラスと正規表現の扱い方に関する基本的な知識が必要です。

Python Template クラスの理解

Python の Template クラスは Python 2.4 から string モジュールに追加されました。

このクラスは、組み込みの置換オプション (主に %) の代わりとして、複雑な文字列ベースのテンプレートを作成したり、ユーザーフレンドリーな方法でテンプレートを扱ったりするために使用することを意図しています。

このクラスの実装では、正規表現を用いて、有効なテンプレート文字列の一般的なパターンにマッチさせます。

有効なテンプレート文字列、またはプレースホルダは2つの部分から構成されています。

  • シンボル
  • 有効な Python 識別子。識別子とは、AからZの大文字と小文字、アンダースコア(_)、0から9の数字の並びのことです。識別子は数字で始めることはできませんし、Pythonのキーワードにすることもできません。

テンプレート文字列では、 $name$age が有効なプレースホルダーとみなされます。

Pythonの Template クラスをコードで使用するには、次のことが必要です。

    1. stringモジュールから Template をインポートする。
  1. 有効なテンプレート文字列を作成する
  2. テンプレート文字列を引数として、 Template のインスタンスを作成します。
  3. 置換メソッドを使用して置換を実行する

Python の Template クラスをコードでどのように使用するかの基本的な例を示します。

>>> from string import Template
>>> temp_str = 'Hi $name, welcome to $site'
>>> temp_obj = Template(temp_str)
>>> temp_obj.substitute(name='John Doe', site='StackAbuse.com')
'Hi John Doe, welcome to StackAbuse.com'


テンプレート文字列 temp_str を作成するときに、 $name$site という2つのプレースホルダーを使用していることに注意してください。

記号は実際の置換を行い、識別子 (namesite) はプレースホルダーをテンプレート文字列に挿入する必要のある具象オブジェクトにマップするために使用されます。

substitute()メソッドを使って置換を行い、目的の文字列を構築すると、マジックは完了します。

Substitute()は、Pythonに「この文字列を調べて、もし$nameが見つかったら、それをJohn Doeに置き換えてください」と伝えているようなものだと考えてください。

さらに文字列を検索して、$siteという識別子を見つけたら、それをStackAbuse.com` に置き換えてください。

.substitute()` に渡す引数の名前は、テンプレート文字列のプレースホルダーで使用した識別子と一致させる必要があります。

Templateと他のPythonの文字列置換ツールの最も重要な違いは、引数の型が考慮されないということです。

Python の有効な文字列に変換できるものであれば、どのような型のオブジェクトでも渡すことができます。

Template クラスは自動的にこれらのオブジェクトを文字列に変換し、最終的な文字列に挿入します。

さて、Pythonの Template クラスの使い方の基本がわかったところで、このクラスが内部でどのように動作しているかをよりよく理解するために、その実装の詳細に飛び込んでみましょう。

この知識があれば、私たちのコードでこのクラスを効果的に使うことができるようになるでしょう。

テンプレート文字列

テンプレート文字列は通常のPython文字列で、特別なプレースホルダーを含んでいます。

前に見たように、これらのプレースホルダーは $ 記号と有効な Python 識別子を使って作成されます。

いったん有効なテンプレート文字列ができれば、プレースホルダーを独自の値で置き換えて、より精巧な文字列を作成することができます。

PEP 292 — Simpler String Substitutions によると、プレースホルダーで $ 記号を使用する際のルールは以下のとおりです。

1. $$ はエスケープであり、単一の $ に置き換えられます。

2. $identifierは “identifier “のマッピングキーにマッチする置換プレースホルダーを指定します。

デフォルトでは、”identifier “は http://docs.python.org/reference/lexical_analysis.html#identifiers-and-keywords で定義されているPythonの識別子のスペルである必要があります。

の後の最初の非識別子文字がこのプレースホルダーの指定を終了させます。

3. ${identifier}$identifier と同じ意味です。

これは、有効な識別子文字がプレースホルダーの後に続くが、プレースホルダーの一部ではない場合、例えば "${noun}ification" のように必要とされます。

これらのルールがどのように機能するかをよりよく理解するために、いくつかの例をコーディングしてみましょう。

まず、$ 記号をどのようにエスケープするかという例から始めます。

例えば、通貨を扱っていて、結果の文字列にドル記号が必要な場合を考えてみましょう。

以下のように、テンプレートの文字列の中で $ 記号を二重にして自分自身をエスケープすることができます。

>>> budget = Template('The $time budget for investment is $$$amount')
>>> budget.substitute(time='monthly', amount='1,000.00')
'The monthly budget for investment is $1,000.00'


$$$amountで行ったように、エスケープされた記号と次のプレースホルダーの間に余分なスペースを入れる必要はないことに注意してください。

テンプレートは賢いので、$$` 記号を正しくエスケープすることができます。

2つ目のルールは、テンプレートの文字列の中で有効なプレースホルダーを作成するための基本的な事項を述べています。

すべてのプレースホルダーは $ 文字の後に有効なPythonの識別子を付けて作成する必要があります。

次の例を見てください。

>>> template = Template('$what, $who!')
>>> template.substitute(what='Hello', who='World')
'Hello, World!'


ここでは、両方のプレースホルダーが有効なPython識別子(whatwho)を使用して形成されています。

また、2番目のルールで述べたように、最初の非識別子文字がプレースホルダーを終了させることに注意してください。

例えば、 $who! では、文字 ! がプレースホルダーの一部ではなく、最後の文字列の一部になっています。

文字列の中の単語を部分的に置換する必要がある場合もあるでしょう。

これが、プレースホルダーを作るための2つ目のオプションがある理由です。

3つ目のルールは、${identifier}$identifier と同等であり、有効な識別子文字がプレースホルダーの後に続くが、プレースホルダー自体の一部ではない場合に使用されるべきというものです。

例えば、私たちの会社の製品に関する商業情報を含むファイルの作成を自動化する必要があるとします。

ファイル名は、製品コード、名前、生産バッチを含み、アンダースコア(_)文字で区切られるパターンに従って命名されます。

次のような例で考えてみましょう。

>>> filename_temp = Template('$code_$product_$batch.xlsx')
>>> filename_temp.substitute(code='001', product='Apple_Juice', batch='zx.001.2020')
Traceback (most recent call last):
  ...
KeyError: 'code_'


はPythonの識別子として有効な文字なので、このテンプレート文字列は期待通りに動作せず、 TemplateKeyError を発生させます。

この問題を解決するには、以下のように中括弧付きの記法 (${identifier}) を使用してプレースホルダーを作成します。

>>> filename_temp = Template('${code}_${product}_$batch.xlsx')
>>> filename_temp.substitute(code='001', product='Apple_Juice', batch='zx.001.2020')
'001_Apple_Juice_zx.001.2020.xlsx'


これで、テンプレートは正しく動作するようになりました。

これは、中括弧が識別子と _ 文字を適切に分離しているためです。

注目すべきは、中括弧を使うのは codeproduct だけで、 batch には使わないということです。

なぜなら、 batch の後に続く . 文字は Python では有効な識別子の文字ではないからです。

最後に、テンプレートの文字列はインスタンスの template プロパティに格納されます。

それでは、 Hello, World! の例をもう一度見てみましょう。

今回は template を少し変更してみましょう。

>>> template = Template('$what, $who!')  # Original template
>>> template.template = 'My $what, $who template'  # Modified template
>>> template.template
'My $what, $who template'
>>> template.substitute(what='Hello', who='World')
'My Hello, World template'


Pythonはインスタンスの属性へのアクセスを制限しないので、必要なときにいつでもテンプレート文字列を変更することができます。

しかし、Pythonの Template クラスを使用する場合、これは一般的な方法ではありません。

コード内で使用するテンプレート文字列が変わるたびに、新しい Template のインスタンスを作成するのがベストです。

こうすることで、不確かなテンプレート文字列の使用に関連した、微妙で見つけにくいバグを避けることができます。

substitute() メソッド

これまでのところ、文字列の置換を行うために Template インスタンスの substitute() メソッド を使用してきました。

このメソッドは、キーワード引数や識別子と値のペアを含むマッピングを使用して、テンプレートの文字列のプレースホルダーを置き換えます。

キーワード引数またはマッピングの識別子は、テンプレートの文字列のプレースホルダーを定義するために使用された識別子と一致しなければなりません。

値は文字列にうまく変換される任意のPython型にすることができます。

これまでの例でキーワード引数の使い方を説明しましたので、今度は辞書の使い方に集中しましょう。

以下はその例です。

>>> template = Template('Hi $name, welcome to $site')
>>> mapping = {'name': 'John Doe', 'site': 'StackAbuse.com'}
>>> template.substitute(**mapping)
'Hi John Doe, welcome to StackAbuse.com'


Substitute()で辞書を引数として使用する場合、辞書の展開演算子である**` を使用する必要があります。

この演算子は、キーと値のペアをキーワード引数に展開し、テンプレート文字列のマッチするプレースホルダーを置換するために使用されます。

よくあるテンプレートエラー

Pythonの Template クラスを使用する際に、不注意で発生する可能性のある一般的なエラーがいくつか存在します。

例えば、 substitute() に不完全な引数のセットを与えると、 KeyError が発生します。

不完全な引数のセットを使用する次のコードを考えてみましょう。

>>> template = Template('Hi $name, welcome to $site')
>>> template.substitute(name='Jane Doe')
Traceback (most recent call last):
  ...
KeyError: 'site'


もし、テンプレート文字列のすべてのプレースホルダにマッチしない引数のセットで substitute() を呼び出すと、 KeyError を受け取ります。

もし、いくつかのプレースホルダーで無効なPythonの識別子を使用すると、プレースホルダーが正しくないことを伝える ValueError が発生します。

この例では、$nameの代わりに $0name という無効な識別子をプレースホルダーとして使用しています。

>>> template = Template('Hi $0name, welcome to $site')
>>> template.substitute(name='Jane Doe', site='StackAbuse.com')
Traceback (most recent call last):
  ...
ValueError: Invalid placeholder in string: line 1, col 4


Templateオブジェクトが置換を行うためにテンプレートの文字列を読み込んだとき、初めて無効な識別子を発見します。

これはすぐにValueErrorを発生させます。

0name は数字で始まっているため、有効な Python 識別子や名前ではないことに注意してください。

safe_substitute()メソッド

Python の Template クラスは、文字列の置換を行うために使用できる 2 つ目のメソッドを持っています。

このメソッドは safe_substitute() と呼ばれます。

このメソッドは substitute() と同様に動作しますが、不完全な引数やマッチしない引数を使用した場合には KeyError を発生させません。

この場合、欠落している、あるいはマッチしないプレースホルダは、最終的な文字列に変更されずに表示されます。

以下は、不完全な引数のセット (site がない) を使用して safe_substitute() がどのように動作するかを示しています。

>>> template = Template('Hi $name, welcome to $site')
>>> template.safe_substitute(name='John Doe')
'Hi John Doe, welcome to $site'


ここでは、不完全な引数のセットを使って safe_substitute() を拳で呼び出しています。

結果として得られる文字列には、元のプレースホルダー $site が含まれますが、 KeyError は発生しません。

Pythonのテンプレートクラスをカスタマイズする

Python の Template クラスはサブクラス化、カスタマイズができるように設計されています。

これにより、正規表現のパターンやクラスの他の属性を、私たちの特定のニーズに合わせて変更することができます。

このセクションでは、クラスの最も重要な属性をカスタマイズする方法と、それが Template オブジェクトの一般的な動作にどのような影響を与えるかについて説明します。

まずは、クラスの属性である .delimiter から始めましょう。

別のデリミターを使用する

クラス属性 delimiter は、プレースホルダーの開始文字として使用する文字を保持します。

これまで見てきたように、そのデフォルト値は $ です。

Pythonの Template クラスは継承できるように設計されているので、 Template をサブクラス化して、 delimiter のデフォルト値をオーバーライドして変更することができます。

次の例では、デリミタが $ の代わりに # を使用するようにオーバーライドしています。

from string import Template
class MyTemplate(Template):
    delimiter = '#'


template = MyTemplate('Hi #name, welcome to #site')
print(template.substitute(name='Jane Doe', site='StackAbuse.com'))


# Output:
# 'Hi Jane Doe, welcome to StackAbuse.com'


# Escape operations also work
tag = MyTemplate('This is a Twitter hashtag: ###hashtag')
print(tag.substitute(hashtag='Python'))


# Output:
# 'This is a Twitter hashtag: #Python'


MyTemplateクラスは、通常のPythonのTemplateクラスと同じように使うことができます。

しかし、プレースホルダーを作成する際には$の代わりに#` を使用しなければなりません。

これは、通貨を扱うときなど、ドル記号をたくさん扱う文字列を扱うときに便利です。

注意: delimiter を正規表現に置き換えないでください。

テンプレートクラスは自動的にデリミタをエスケープします。

そのため、もし delimiter に正規表現を使用すると、カスタム Template が正しく動作しない可能性が高くなります。

Identifierとして認定されるものの変更

idpatternクラス属性は、テンプレート文字列のプレースホルダの後半を検証するために使用される正規表現を保持します。

言い換えると、idpatternはプレースホルダーで使用する識別子が有効なPythonの識別子であることを検証します。

idpattern のデフォルト値は r'(?-i:[_a-zA-Z][_a-zA-Z0-9]*)' である。

Templateのサブクラスを作成して、idpatternに独自の正規表現パターンを使用することができる。

例えば、識別子をアンダースコア (_) や数字 ([0-9]) を含まない名前に制限する必要があるとします。

そのためには、以下のようにidpattern` をオーバーライドして、パターンからこれらの文字を削除することができます。

from string import Template
class MyTemplate(Template):
    idpattern = r'(?-i:[a-zA-Z][a-zA-Z]*)'


# Underscores are not allowed
template = MyTemplate('$name_underscore not allowed')
print(template.substitute(name_underscore='Jane Doe'))


このコードを実行すると、次のようなエラーが発生します。

Traceback (most recent call last):
    ...
KeyError: 'name'


数字も同様に許されないことが確認できます。

template = MyTemplate('$python3 digits not allowed')
print(template.substitute(python3='Python version 3.x'))


エラーは次のようになります。

Traceback (most recent call last):
    ...
KeyError: 'python'


アンダースコアと数字はカスタム idpattern に含まれないので、 Template オブジェクトは2番目のルールを適用して、 $ の後にある最初の非識別用文字でプレースホルダーを壊します。

そのため、それぞれのケースで KeyError が発生します。

高度なテンプレート・サブクラスの構築

Pythonの Template クラスの振る舞いを変更する必要がある場合、 delimiteridpattern 、あるいはその両方をオーバーライドするだけでは十分でないことがあります。

このような場合には、 pattern クラス属性をオーバーライドして、独自の Template サブクラスに対して全く新しい正規表現を定義することができます。

もし、 pattern に全く新しい正規表現を使用することにした場合には、4つの名前の付いたグループを持つ正規表現を提供する必要があります。

    1. escaped$$ のような区切り文字に対応するエスケープシーケンスにマッチします。
  1. namedはデリミタと$identifier` のような有効なPythonの識別子にマッチします。
  2. braced${identifier}` のように、デリミタと中括弧を使用した有効なPythonの識別子にマッチします。
  3. invalid$0site` のように、他の不正な形式のデリミタにマッチします。

patternプロパティはコンパイルされた正規表現オブジェクトを保持します。

しかし、patternプロパティのpattern` 属性にアクセスすることで、元の正規表現の文字列を検査することができます。

以下のコードを見てください。

>>> template = Template('$name')
>>> print(template.pattern.pattern)
$(?:
    (?P<escaped$) |   # Escape sequence of two delimiters
    (?P<named(?-i:[_a-zA-Z][_a-zA-Z0-9]*))      |   # delimiter and a Python identifier
    {(?P<braced(?-i:[_a-zA-Z][_a-zA-Z0-9]*))}   |   # delimiter and a braced identifier
    (?P<invalid)              # Other ill-formed delimiter exprs
  )


このコードは pattern クラス属性をコンパイルするために使用されたデフォルトの文字列を出力します。

この場合、デフォルトの正規表現に適合する4つの名前付きグループをはっきりと見ることができます。

前述したように、もし Template の動作を深くカスタマイズする必要があるなら、これら4つの名前付きグループと、それぞれのグループに対する特定の正規表現を提供する必要があります。

eval()とexec()を使ったコードの実行

Note: 組み込み関数の eval()exec() は、悪意のある入力で使用された場合、セキュリティ上重要な影響を与える可能性があります。

この最後のセクションでは、Pythonの Template クラスを eval()exec() といったPythonの組み込み関数と一緒に使えば、どれだけ強力なものになるかを紹介します。

eval()関数は Python の式を 1 つ実行し、その結果を返します。

exec() 関数も Python の式を実行しますが、その値を返すことはありません。

通常 exec() を使うのは、例えば変数の値を変更するような、式の副作用にしか興味がないときでしょう。

これから取り上げる例はやや型破りに見えるかもしれませんが、この強力なPythonツールの組み合わせの興味深い使用例を見つけられると確信しています。

最初の例では、リスト内包によって動的にリストを作成するために、 eval() と共にTemplateを使用することにします。

&gt;&gt;&gt; template = Template('[$exp for item in $coll]')
&gt;&gt;&gt; eval(template.substitute(exp='item ** 2', coll='[1, 2, 3, 4]'))
[1, 4, 9, 16]
&gt;&gt;&gt; eval(template.substitute(exp='2 ** item', coll='[3, 4, 5, 6, 7, 8]'))
[8, 16, 32, 64, 128, 256]
&gt;&gt;&gt; import math
&gt;&gt;&gt; eval(template.substitute(expression='math.sqrt(item)', collection='[9, 16, 25]'))
[3.0, 4.0, 5.0]


この例で使うテンプレートオブジェクトはリスト内包の基本的な構文を持っています。

このテンプレートから始めて、プレースホルダーを有効な式 (exp) とコレクション (coll) に置き換えて、動的にリストを作成することができます。

最後のステップとして、 eval() を使用して内包を実行します。

テンプレート文字列の複雑さに制限はないので、どんなPythonコードでも保持するテンプレート文字列を作成することができます。

次の例では、クラス全体を作成するために Template オブジェクトを使用する方法を考えてみましょう。

from string import Template


_class_template = """
class ${klass}:
    def __init__(self, name):
        self.name = name


def ${method}(self):
        print('Hi', self.name + ',', 'welcome to', '$site')
"""


template = Template(_class_template)
exec(template.substitute(klass='MyClass',
                         method='greet',
                         site='StackAbuse.com'))


obj = MyClass("John Doe")
obj.greet()


ここでは、完全に機能する Python クラスを保持するテンプレート文字列を作成します。

このテンプレートを使って、必要に応じて異なる名前のクラスを作成することができます。

この場合、exec() が実際のクラスを作成し、現在の名前空間に取り込みます。

この時点から、通常の Python クラスと同じように、このクラスを自由に使うことができるようになります。

これらの例はかなり基本的なものですが、Pythonの Template クラスがいかに強力で、Pythonで複雑なプログラミングの問題を解決するためにどのように活用できるかを示しています。

結論

Python の Template クラスは、文字列の置換や文字列の補間に使用することを意図しています。

このクラスは正規表現を使用して動作し、ユーザーフレンドリーで強力なインターフェイスを提供します。

複雑な文字列ベースのテンプレートを作成する際に、組み込みの文字列置換オプションに代わる実行可能な選択肢となります。

この記事では、Pythonの Template クラスがどのように動作するのかを学びました。

また、 Template を使用する際に発生する可能性のある、より一般的なエラーと、その回避方法についても学びました。

最後に、サブクラス化によってクラスをカスタマイズする方法と、それを使ってPythonのコードを実行する方法について学びました。

このような知識があれば、Pythonの Template クラスを効果的に使って、コードの中で文字列の補間や置換を実行することができるようになります。

.

タイトルとURLをコピーしました