【Python2/3】書式文字列をパースする

Cのsprintfに相当する機能が、Pythonには今のところ3種類もあります。(`f`-stringは書式文字列を使わないので除外)

|名称?|使い方|
|:--|:--|
| `%`-formatting | `fmt % values` |
| `str.format()` | `fmt.format(*values)` / `fmt.format(**values)` |
| Template strings | `string.Template(fmt).substitute(**values)` |

この記事では、上の表の `values`部分に要求されるタプルの要素数、または辞書のキーを調べる方法をまとめます。
正直、かなり使用頻度の低い知識だと思いますが、`logging.Formatter`を継承して自作のFormatterを作るときなどではもしかしたら役に立つかもしれません。

# 概要
- `%`-formatting → formatを失敗させて例外を見る。
- `str.format` → `string.Formatter().parser(fmt)`
- Template string → `string.Template.pattern.finditer(fmt)`

# `%`-formatting (printf style formatting)
他の2つとは違ってパースするための専用機能が標準パッケージに見当たらないので自分で頑張ります。
今回は投げ返される例外をもとに解読する関数を作ってみました。`fmt % values` の `values` の部分に要求されている型が、タプルならその長さを、辞書ならそのキーを返します。

```py3
def parse_printf_style_format(fmt):
    if not isinstance(fmt, (bytes, str)):
        raise TypeError('got ' + type(fmt).__name__)
        
    try:
        fmt % ()
    except TypeError as e:
        if e.args[0] == 'not enough arguments for format string':
            values = ()
        elif e.args[0] == 'format requires a mapping':
            values = {}
        else:
            raise
    else:
        return None
    
    if isinstance(values, tuple):
        while True:
            try:
                fmt % values
            except TypeError as e:
                if e.args[0] == 'not enough arguments for format string':
                    values += (0,)
                else:
                    raise ValueError('invalid format: ' + repr(fmt))
            else:
                return len(values)
    elif isinstance(values, dict):
        while True:
            try:
                fmt % values
            except TypeError as e:
                if e.args[0] == 'not enough arguments for format string':
                    raise ValueError('invalid format: ' + repr(fmt))
                else:
                    raise
            except KeyError as e:
                values[e.args[0]] = 0
            else:
                return tuple(values.keys())
    else:
        assert False
```

使用例

```py3
>>> parse_printf_style_format('%d %s %x')
3
>>> parse_printf_style_format('%(foo)s, %(bar)d')
('foo', 'bar')
```

# `str.format()`

`string.Formatter().parse(fmt)` で一撃です。詳しくは[公式ドキュメント](https://docs.python.jp/3/library/string.html#string.Formatter)を参照してください。

```py3
>>> import string
>>> fmt = '{foo:s}, {{bar:d}}, and {:f}'
>>> list(string.Formatter().parse(fmt))
[('', 'foo', 's', None),
 (', {', None, None, None),
 ('bar:d}', None, None, None),
 (', and ', '', 'f', None)]
```

# Template strings

`string.Template.pattern.finditer(fmt)` で一撃です。
プレースホルダにマッチする正規表現が`pattern`属性に格納されています。

```py3
>>> import string
>>> fmt = '${this_is_braced} $$this_is_escaped $@this_is_invalid $this_is_named'
>>> print(string.Template.pattern.pattern)
```

```
    \$(?:
      (?P<escaped>\$) |   # Escape sequence of two delimiters
      (?P<named>[_a-z][_a-z0-9]*)      |   # delimiter and a Python identifier
      {(?P<braced>[_a-z][_a-z0-9]*)}   |   # delimiter and a braced identifier
      (?P<invalid>)              # Other ill-formed delimiter exprs
    )
```

```py3
>>> [match.groupdict() for match in string.Template.pattern.finditer(fmt)]
```

```
[{'braced': 'this_is_braced', 'escaped': None, 'invalid': None, 'named': None},
 {'braced': None, 'escaped': '$', 'invalid': None, 'named': None},
 {'braced': None, 'escaped': None, 'invalid': '', 'named': None},
 {'braced': None, 'escaped': None, 'invalid': None, 'named': 'this_is_named'}]
```

# 所感
`%`-fomattingの冷遇され方がひどい。