safe_substitute()
文字列テンプレート機能と同様に、高度な文字列フォーマット メソッドを使用して部分的な文字列フォーマットを行うことは可能ですか?
例えば:
s = '{foo} {bar}'
s.format(foo='FOO') #Problem: raises KeyError 'bar'
safe_substitute()
文字列テンプレート機能と同様に、高度な文字列フォーマット メソッドを使用して部分的な文字列フォーマットを行うことは可能ですか?
例えば:
s = '{foo} {bar}'
s.format(foo='FOO') #Problem: raises KeyError 'bar'
書式設定の順序がわかっている場合:
s = '{foo} {{bar}}'
次のように使用します。
ss = s.format(foo='FOO')
print ss
>>> 'FOO {bar}'
print ss.format(bar='BAR')
>>> 'FOO BAR'
同時に指定することはできません-順番に行う必要がfoo
あります。bar
短く、最も読みやすく、コーダーの意図を説明するpartial
関数を使用できます。functools
from functools import partial
s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR
マッピングを上書きすることで、部分的なフォーマットにだますことができます。
import string
class FormatDict(dict):
def __missing__(self, key):
return "{" + key + "}"
s = '{foo} {bar}'
formatter = string.Formatter()
mapping = FormatDict(foo='FOO')
print(formatter.vformat(s, (), mapping))
印刷
FOO {bar}
もちろん、この基本的な実装は基本的なケースでのみ正しく機能します。
この制限.format()
- 部分的な置換を行うことができない - は、私を悩ませてきました。
ここの多くの回答で説明されているようにカスタムFormatter
クラスの作成を評価し、 lazy_formatなどのサードパーティパッケージの使用を検討した後、はるかに単純な組み込みのソリューションを発見しました:テンプレート文字列
同様の機能を提供しますが、部分置換によるsafe_substitute()
方法も提供します。テンプレート文字列には$
プレフィックスが必要です (これは少し奇妙に感じますが、全体的な解決策はより良いと思います)。
import string
template = string.Template('${x} ${y}')
try:
template.substitute({'x':1}) # raises KeyError
except KeyError:
pass
# but the following raises no error
partial_str = template.safe_substitute({'x':1}) # no error
# partial_str now contains a string with partial substitution
partial_template = string.Template(partial_str)
substituted_str = partial_template.safe_substitute({'y':2}) # no error
print substituted_str # prints '12'
これに基づいて便利なラッパーを形成しました:
class StringTemplate(object):
def __init__(self, template):
self.template = string.Template(template)
self.partial_substituted_str = None
def __repr__(self):
return self.template.safe_substitute()
def format(self, *args, **kws):
self.partial_substituted_str = self.template.safe_substitute(*args, **kws)
self.template = string.Template(self.partial_substituted_str)
return self.__repr__()
>>> s = StringTemplate('${x}${y}')
>>> s
'${x}${y}'
>>> s.format(x=1)
'1${y}'
>>> s.format({'y':2})
'12'
>>> print s
12
同様に、デフォルトの文字列フォーマットを使用する Sven の回答に基づくラッパー:
class StringTemplate(object):
class FormatDict(dict):
def __missing__(self, key):
return "{" + key + "}"
def __init__(self, template):
self.substituted_str = template
self.formatter = string.Formatter()
def __repr__(self):
return self.substituted_str
def format(self, *args, **kwargs):
mapping = StringTemplate.FormatDict(*args, **kwargs)
self.substituted_str = self.formatter.vformat(self.substituted_str, (), mapping)
これが簡単な回避策として問題ないかどうかはわかりませんが、どうですか
s = '{foo} {bar}'
s.format(foo='FOO', bar='{bar}')
? :)
Formatter
メソッドをオーバーライドする独自のものを定義するget_value
と、それを使用して、未定義のフィールド名を必要なものにマップできます。
http://docs.python.org/library/string.html#string.Formatter.get_value
たとえば、kwargs にないbar
場合"{bar}"
にマップできます。bar
ただし、format()
文字列のメソッドではなく、Formatter オブジェクトのメソッドを使用する必要がありますformat()
。
>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'
これを試してみてください。
Amberのコメントのおかげで、私はこれを思いつきました:
import string
try:
# Python 3
from _string import formatter_field_name_split
except ImportError:
formatter_field_name_split = str._formatter_field_name_split
class PartialFormatter(string.Formatter):
def get_field(self, field_name, args, kwargs):
try:
val = super(PartialFormatter, self).get_field(field_name, args, kwargs)
except (IndexError, KeyError, AttributeError):
first, _ = formatter_field_name_split(field_name)
val = '{' + field_name + '}', first
return val
私にとってはこれで十分でした:
>>> ss = 'dfassf {} dfasfae efaef {} fds'
>>> nn = ss.format('f1', '{}')
>>> nn
'dfassf f1 dfasfae efaef {} fds'
>>> n2 = nn.format('whoa')
>>> n2
'dfassf f1 dfasfae efaef whoa fds'
この関連する質問のようにformat
、引数を渡すためにディクショナリをアンパックしたい場合は、次の方法を使用できます。
最初に、文字列s
がこの質問と同じであると仮定します。
s = '{foo} {bar}'
値は次の辞書によって与えられます。
replacements = {'foo': 'FOO'}
明らかにこれはうまくいきません:
s.format(**replacements)
#---------------------------------------------------------------------------
#KeyError Traceback (most recent call last)
#<ipython-input-29-ef5e51de79bf> in <module>()
#----> 1 s.format(**replacements)
#
#KeyError: 'bar'
ただし、最初にからすべての名前付き引数を取得しset
s
、引数を中括弧で囲まれたそれ自体にマップする辞書を作成できます。
from string import Formatter
args = {x[1]:'{'+x[1]+'}' for x in Formatter().parse(s)}
print(args)
#{'foo': '{foo}', 'bar': '{bar}'}
次に、辞書を使用args
して の不足しているキーを埋めますreplacements
。Python 3.5+ では、単一の式でこれを行うことができます:
new_s = s.format(**{**args, **replacements}}
print(new_s)
#'FOO {bar}'
古いバージョンの Python では、次のように呼び出すことができますupdate
。
args.update(replacements)
print(s.format(**args))
#'FOO {bar}'
これを実現する方法がもう 1 つあります。つまり、変数を使用format
し%
て置き換えます。例えば:
>>> s = '{foo} %(bar)s'
>>> s = s.format(foo='my_foo')
>>> s
'my_foo %(bar)s'
>>> s % {'bar': 'my_bar'}
'my_foo my_bar'
文字列が完全に入力されるまで文字列を使用しないと仮定すると、次のクラスのようにすることができます。
class IncrementalFormatting:
def __init__(self, string):
self._args = []
self._kwargs = {}
self._string = string
def add(self, *args, **kwargs):
self._args.extend(args)
self._kwargs.update(kwargs)
def get(self):
return self._string.format(*self._args, **self._kwargs)
例:
template = '#{a}:{}/{}?{c}'
message = IncrementalFormatting(template)
message.add('abc')
message.add('xyz', a=24)
message.add(c='lmno')
assert message.get() == '#24:abc/xyz?lmno'
あちこちから最も有望なソリューションをテストした後、どれも実際に次の要件を満たしていないことに気付きました。
str.format_map()
によって認識されるテンプレートの構文に厳密に従います。そこで、上記の要件を満たす独自のソリューションを作成しました。(編集: @SvenMarnach によるバージョン (この回答で報告されているように) は、必要なコーナーケースを処理しているようです)。
基本的に、テンプレート文字列を解析し、(ヘルパー関数を使用して) 一致するネストされたグループを見つけ{.*?}
、潜在的な.find_all()
str.format_map()
KeyError
def find_all(
text,
pattern,
overlap=False):
"""
Find all occurrencies of the pattern in the text.
Args:
text (str|bytes|bytearray): The input text.
pattern (str|bytes|bytearray): The pattern to find.
overlap (bool): Detect overlapping patterns.
Yields:
position (int): The position of the next finding.
"""
len_text = len(text)
offset = 1 if overlap else (len(pattern) or 1)
i = 0
while i < len_text:
i = text.find(pattern, i)
if i >= 0:
yield i
i += offset
else:
break
def matching_delimiters(
text,
l_delim,
r_delim,
including=True):
"""
Find matching delimiters in a sequence.
The delimiters are matched according to nesting level.
Args:
text (str|bytes|bytearray): The input text.
l_delim (str|bytes|bytearray): The left delimiter.
r_delim (str|bytes|bytearray): The right delimiter.
including (bool): Include delimeters.
yields:
result (tuple[int]): The matching delimiters.
"""
l_offset = len(l_delim) if including else 0
r_offset = len(r_delim) if including else 0
stack = []
l_tokens = set(find_all(text, l_delim))
r_tokens = set(find_all(text, r_delim))
positions = l_tokens.union(r_tokens)
for pos in sorted(positions):
if pos in l_tokens:
stack.append(pos + 1)
elif pos in r_tokens:
if len(stack) > 0:
prev = stack.pop()
yield (prev - l_offset, pos + r_offset, len(stack))
else:
raise ValueError(
'Found `{}` unmatched right token(s) `{}` (position: {}).'
.format(len(r_tokens) - len(l_tokens), r_delim, pos))
if len(stack) > 0:
raise ValueError(
'Found `{}` unmatched left token(s) `{}` (position: {}).'
.format(
len(l_tokens) - len(r_tokens), l_delim, stack.pop() - 1))
def safe_format_map(
text,
source):
"""
Perform safe string formatting from a mapping source.
If a value is missing from source, this is simply ignored, and no
`KeyError` is raised.
Args:
text (str): Text to format.
source (Mapping|None): The mapping to use as source.
If None, uses caller's `vars()`.
Returns:
result (str): The formatted text.
"""
stack = []
for i, j, depth in matching_delimiters(text, '{', '}'):
if depth == 0:
try:
replacing = text[i:j].format_map(source)
except KeyError:
pass
else:
stack.append((i, j, replacing))
result = ''
i, j = len(text), 0
while len(stack) > 0:
last_i = i
i, j, replacing = stack.pop()
result = replacing + text[j:last_i] + result
if i > 0:
result = text[0:i] + result
return result
(このコードはFlyingCircusでも利用できます-- 免責事項: 私はその主な作成者です。)
このコードの使用法は次のとおりです。
print(safe_format_map('{a} {b} {c}', dict(a=-A-)))
# -A- {b} {c}
これを私のお気に入りのソリューションと比較してみましょう (@SvenMarnach がコードをあちこちで親切に共有してくれました):
import string
class FormatPlaceholder:
def __init__(self, key):
self.key = key
def __format__(self, spec):
result = self.key
if spec:
result += ":" + spec
return "{" + result + "}"
def __getitem__(self, index):
self.key = "{}[{}]".format(self.key, index)
return self
def __getattr__(self, attr):
self.key = "{}.{}".format(self.key, attr)
return self
class FormatDict(dict):
def __missing__(self, key):
return FormatPlaceholder(key)
def safe_format_alt(text, source):
formatter = string.Formatter()
return formatter.vformat(text, (), FormatDict(source))
いくつかのテストを次に示します。
test_texts = (
'{b} {f}', # simple nothing useful in source
'{a} {b}', # simple
'{a} {b} {c:5d}', # formatting
'{a} {b} {c!s}', # coercion
'{a} {b} {c!s:>{a}s}', # formatting and coercion
'{a} {b} {c:0{a}d}', # nesting
'{a} {b} {d[x]}', # dicts (existing in source)
'{a} {b} {e.index}', # class (existing in source)
'{a} {b} {f[g]}', # dict (not existing in source)
'{a} {b} {f.values}', # class (not existing in source)
)
source = dict(a=4, c=101, d=dict(x='FOO'), e=[])
そしてそれを実行するためのコード:
funcs = safe_format_map, safe_format_alt
n = 18
for text in test_texts:
full_source = {**dict(b='---', f=dict(g='Oh yes!')), **source}
print('{:>{n}s} : OK : '.format('str.format_map', n=n) + text.format_map(full_source))
for func in funcs:
try:
print(f'{func.__name__:>{n}s} : OK : ' + func(text, source))
except:
print(f'{func.__name__:>{n}s} : FAILED : {text}')
その結果:
str.format_map : OK : --- {'g': 'Oh yes!'}
safe_format_map : OK : {b} {f}
safe_format_alt : OK : {b} {f}
str.format_map : OK : 4 ---
safe_format_map : OK : 4 {b}
safe_format_alt : OK : 4 {b}
str.format_map : OK : 4 --- 101
safe_format_map : OK : 4 {b} 101
safe_format_alt : OK : 4 {b} 101
str.format_map : OK : 4 --- 101
safe_format_map : OK : 4 {b} 101
safe_format_alt : OK : 4 {b} 101
str.format_map : OK : 4 --- 101
safe_format_map : OK : 4 {b} 101
safe_format_alt : OK : 4 {b} 101
str.format_map : OK : 4 --- 0101
safe_format_map : OK : 4 {b} 0101
safe_format_alt : OK : 4 {b} 0101
str.format_map : OK : 4 --- FOO
safe_format_map : OK : 4 {b} FOO
safe_format_alt : OK : 4 {b} FOO
str.format_map : OK : 4 --- <built-in method index of list object at 0x7f7a485666c8>
safe_format_map : OK : 4 {b} <built-in method index of list object at 0x7f7a485666c8>
safe_format_alt : OK : 4 {b} <built-in method index of list object at 0x7f7a485666c8>
str.format_map : OK : 4 --- Oh yes!
safe_format_map : OK : 4 {b} {f[g]}
safe_format_alt : OK : 4 {b} {f[g]}
str.format_map : OK : 4 --- <built-in method values of dict object at 0x7f7a485da090>
safe_format_map : OK : 4 {b} {f.values}
safe_format_alt : OK : 4 {b} {f.values}
ご覧のとおり、更新されたバージョンは、以前のバージョンでは失敗していたコーナー ケースを適切に処理できるようになりました。
時間的に、それらはおよその範囲内にあります。text
実際のフォーマット(およびおそらく実際のsource
)に応じて、お互いの50%ですがsafe_format_map()
、私が実行したほとんどのテストで優位性があるようです(もちろん、それらが意味するものは何でも):
for text in test_texts:
print(f' {text}')
%timeit safe_format(text * 1000, source)
%timeit safe_format_alt(text * 1000, source)
{b} {f}
3.93 ms ± 153 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
6.35 ms ± 51.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b}
4.37 ms ± 57.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
5.2 ms ± 159 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {c:5d}
7.15 ms ± 91.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
7.76 ms ± 69.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {c!s}
7.04 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
7.56 ms ± 161 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {c!s:>{a}s}
8.91 ms ± 113 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10.5 ms ± 181 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {c:0{a}d}
8.84 ms ± 147 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10.2 ms ± 202 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {d[x]}
7.01 ms ± 197 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
7.35 ms ± 106 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {e.index}
11 ms ± 68.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
8.78 ms ± 405 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {f[g]}
6.55 ms ± 88.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
9.12 ms ± 159 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
{a} {b} {f.values}
6.61 ms ± 55.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
9.92 ms ± 98.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
デフォルトの引数を取る関数でラップできます。
def print_foo_bar(foo='', bar=''):
s = '{foo} {bar}'
return s.format(foo=foo, bar=bar)
print_foo_bar(bar='BAR') # ' BAR'