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 の部分に要求されている型が、タプルならその長さを、辞書ならそのキーを返します。
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
使用例
>>> parse_printf_style_format('%d %s %x')
3
>>> parse_printf_style_format('%(foo)s, %(bar)d')
('foo', 'bar')
str.format()
string.Formatter().parse(fmt) で一撃です。詳しくは公式ドキュメントを参照してください。
>>> 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属性に格納されています。
>>> 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
)
>>> [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の冷遇され方がひどい。