式評価機能付 Template

式評価機能があると便利かもしれません。

tags:python, tips, experiment, template, string, eval
created:2007-08-05T20:24:24

式評価機能があると便利かもしれません。 標準テンプレートクラスを拡張してみた。

標準テンプレートクラスについて

Python2.4から標準モジュール「string」にある「Template」クラスは、

  • 「$」プレフィクスで辞書にアクセスできる。
  • 辞書の値を文字列に変換してくれる。

などの特徴をもち、「モジュラ(%)演算子」による書式化よりも簡潔に記述できるメリットがありました。

しかし!、「 Cheetah 」に触れたことがあればこの機能でも不十分に感じるでしょう。

そう。式や関数呼び出しもテンプレートソースに記述したいのです。

かといって、「 Cheetah 」の導入までは必要がないとか、 「 Pure Python 」をキープしたいと思うなら、以下の内容を試して見てください!

ソースコード

template.py :

# -*- encode: utf-8 -*-

import re as _re
import string

class Template(string.Template):
  pattern = _re.compile(r"""\\$(?:
    (?P<escaped>\\$)|
    (?P<named>[a-zA-Z_]+[a-zA-Z0-9_().]*)|
    {(?P<braced>[^}]+)}|
    (?P<invalid>)
    )""", _re.VERBOSE)

  def __init__(self, template):
    string.Template.__init__(self, template)

  def _miss_point(self, mo):
    i = mo.start()
    whole = self.template.splitlines(True)
    lines = self.template[:i].splitlines(True)
    if not lines:
      colno = 1
      lineno = 1
    else:
      colno = i - len(''.join(lines[:-1]))
      lineno = len(lines)
    return (whole[lineno-1].strip(), lineno, colno)

  def eval_substitute(self, *args, **keys):
    if len(args) > 1:
        raise TypeError('Too many positional arguments')
    if args[0:]:
      keys.update(args[0])
    def convert(mo):
      named = mo.group('named') or mo.group('braced')
      if named is not None:
        #print named
        try:
          value = eval(named, keys)
        except Exception, ex:
          line, ln, col = self._miss_point(mo)
          message = str(ex)
          message += ' in line, %(ln)d, col %(col)d\n%(ln)d:"%(line)s"' % locals()
          raise ValueError(message)
        return '%s' % value
      if mo.group('escaped') is not None:
        return self.delimiter
      if mo.group('invalid') is not None:
        self._invalid(mo)
      raise ValueError('Unrecognized named group in pattern', self.pattern)
    return self.pattern.sub(convert, self.template)

利用例

従来のTemplateを継承しているので、従来の使い方も出来ます。

付け加えたのは、「eval_substitute」 メソッドです。

引数の仕様は「string.Template.substitute」とおなじです。

>>> from template import Template
>>> def mm(a):
...   return a*2
...
>>> a1 = 12000
>>> print Template('hoge : mm(a1)[m] = ${'$'}{mm(a1)/1000.}[km]').eval_substitute(vars())
hoge : 24000[m] = 24.0[km]

このように式や算術式がそのまま使えます。インスタンスやメソッドも使えるはず。 一応記述ミスも場所がわかるような例外を投げるようにしてあります。

例えば関数「mm」が未定儀の場合。

Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "template.py", line 51, in eval_substitute
    return self.pattern.sub(convert, self.template)
  File "template.py", line 44, in convert
    raise ValueError(message)
ValueError: name 'mm' is not defined in line, 1, col 9
1:"hoge : mm(a1)[m] = ${'$'}{mm(a1)/1000.}[km]"

こんなかんじです。