Est-il possible d'effectuer un formatage de chaîne partiel avec les méthodes de formatage de chaîne avancées, similaire à la fonction safe_substitute()
du modèle de chaîne?
Par exemple:
s = '{foo} {bar}'
s.format(foo='FOO') #Problem: raises KeyError 'bar'
Vous pouvez le piéger en un formatage partiel en écrasant le mappage:
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))
impression
FOO {bar}
Bien entendu, cette implémentation de base ne fonctionne correctement que dans les cas élémentaires.
Si vous savez dans quel ordre vous formatez les choses:
s = '{foo} {{bar}}'
Utilisez-le comme ceci:
ss = s.format(foo='FOO')
print ss
>>> 'FOO {bar}'
print ss.format(bar='BAR')
>>> 'FOO BAR'
Vous ne pouvez pas spécifier foo
et bar
en même temps - vous devez le faire de manière séquentielle.
Cette limitation de .format()
- l'impossibilité d'effectuer des substitutions partielles - m'a dérangé.
Après avoir évalué l'écriture d'une classe Formatter
personnalisée comme décrit dans de nombreuses réponses ici et même si vous envisagiez d'utiliser des packages tiers tels que lazy_format , j'ai découvert une solution intégrée bien plus simple: Template string
Il fournit une fonctionnalité similaire mais fournit également une substitution partielle via la méthode safe_substitute()
. Les chaînes de modèle doivent avoir un préfixe $
(ce qui semble un peu étrange - mais la solution globale est meilleure, à mon avis).
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'
Formé un wrapper de commodité basé sur ceci:
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
De la même manière, un wrapper basé sur la réponse de Sven qui utilise le formatage de chaîne par défaut:
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)
Vous pouvez utiliser la fonction partial
de functools
qui est courte, plus lisible et décrit également le mieux l'intention du codeur:
from functools import partial
s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR
Pas sûr que ce soit une solution rapide, mais que diriez-vous de
s = '{foo} {bar}'
s.format(foo='FOO', bar='{bar}')
? :)
Si vous définissez votre propre variable Formatter
qui remplace la méthode get_value
, vous pouvez l'utiliser pour mapper des noms de champs non définis à ceux que vous souhaitiez:
http://docs.python.org/library/string.html#string.Formatter.get_value
Par exemple, vous pouvez mapper bar
à "{bar}"
si bar
n'est pas dans les kwargs.
Toutefois, cela nécessite l'utilisation de la méthode format()
de votre objet Formatter, et non de la méthode format()
de la chaîne.
>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'
Essayez ceci.
Grâce au commentaire de Amber , j'ai trouvé ceci:
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
Pour moi c'était assez bon:
>>> 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'
Ma suggestion serait la suivante (testé avec Python3.6):
class Lazymap(object):
def __init__(self, **kwargs):
self.dict = kwargs
def __getitem__(self, key):
return self.dict.get(key, "".join(["{", key, "}"]))
s = '{foo} {bar}'
s.format_map(Lazymap(bar="FOO"))
# >>> '{foo} FOO'
s.format_map(Lazymap(bar="BAR"))
# >>> '{foo} BAR'
s.format_map(Lazymap(bar="BAR", foo="FOO", baz="BAZ"))
# >>> 'FOO BAR'
Mise à jour: Une manière encore plus élégante (sous-classe dict
et surcharge __missing__(self, key)
) est montrée ici: https: //stackoverflow.com/a/17215533/3334
Il existe un autre moyen d’atteindre cet objectif, c’est-à-dire utiliser format
et %
pour remplacer les variables. Par exemple:
>>> s = '{foo} %(bar)s'
>>> s = s.format(foo='my_foo')
>>> s
'my_foo %(bar)s'
>>> s % {'bar': 'my_bar'}
'my_foo my_bar'
Une solution très laide mais la plus simple pour moi est de faire:
tmpl = '{foo}, {bar}'
tmpl.replace('{bar}', 'BAR')
Out[3]: '{foo}, BAR'
De cette façon, vous pouvez toujours utiliser tmpl
comme modèle standard et effectuer un formatage partiel uniquement en cas de besoin. Je trouve ce problème trop trivial pour utiliser une solution trop lourde comme celle de Mohan Raj.
Après avoir testé les solutions les plus prometteuses de ici et là , je me suis rendu compte qu'aucune d'entre elles ne répondait réellement aux exigences suivantes:
str.format_map()
pour le modèle;J'ai donc écrit ma propre solution, qui répond aux exigences ci-dessus. (EDIT: maintenant, la version de @SvenMarnach - comme indiqué dans cette réponse - semble traiter les cas critiques dont j'avais besoin).
Fondamentalement, j’ai fini par analyser la chaîne de modèle, trouver les groupes imbriqués {.*?}
(à l’aide d’une fonction d’aide find_all()
) et construire la chaîne formatée progressivement et directement en utilisant str.format_map()
tout en capturant un potentiel 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
(Ce code est également disponible dans FlyingCircus - AVERTISSEMENT: j'en suis l'auteur principal.)
L'utilisation de ce code serait:
print(safe_format_map('{a} {b} {c}', dict(a=-A-)))
# -A- {b} {c}
Comparons cela à la solution que je préfère (par @SvenMarnach qui a gentiment partagé son code ici et là ):
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))
Voici quelques tests:
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=[])
et le code pour le faire fonctionner:
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}')
résultant en:
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}
comme vous pouvez le constater, la version mise à jour semble désormais bien gérer les cas où la version précédente échouait.
Dans le temps, ils sont dans env. 50% les uns des autres, en fonction de la text
réelle à formater (et probablement de la source
réelle), mais safe_format_map()
semble avoir un Edge dans la plupart des tests que j'ai effectués (quels qu'ils soient moyen, bien sûr):
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)
En supposant que vous n'utilisiez pas la chaîne tant qu'elle n'est pas complètement remplie, vous pourriez faire quelque chose comme cette classe:
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)
Exemple:
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'