formatage de chaîne partiel


128

Est-il possible d'effectuer un formatage de chaîne partiel avec les méthodes de formatage de chaîne avancées, similaires à la safe_substitute()fonction de modèle de chaîne ?

Par exemple:

s = '{foo} {bar}'
s.format(foo='FOO') #Problem: raises KeyError 'bar'

Réponses:


58

Vous pouvez le tromper 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 sûr, cette implémentation de base ne fonctionne correctement que pour les cas de base.


7
Cela ne fonctionne pas pour les {bar:1.2f}
formats

Je comprends que "l'implémentation la plus basique ne fonctionne correctement que pour les cas de base" mais y a-t-il un moyen d'étendre cela pour ne même pas supprimer les spécifications de format?
Tadhg McDonald-Jensen

5
@ TadhgMcDonald-Jensen: Oui, il y a un moyen. Au lieu de renvoyer une chaîne dans __missing__(), retournez une instance d'une classe personnalisée remplaçant __format__()de manière à renvoyer l'espace réservé d'origine, y compris la spécification de format. Preuve de concept: ideone.com/xykV7R
Sven Marnach

@SvenMarnach pourquoi votre preuve de concept n'est-elle pas dans le corps de votre réponse? C'est un peu insaisissable. Y a-t-il des mises en garde connues qui vous empêchent d'en faire la promotion?
norok2

1
@ norok2 C'est une réponse à une question posée dans un commentaire, donc je mets la réponse dans un commentaire. La question originale n'incluait pas vraiment cette exigence, et je pense généralement toujours que c'est un peu bizarre d'essayer de formater partiellement une chaîne.
Sven Marnach

128

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 fooet baren même temps - vous devez le faire de manière séquentielle.


À quoi ça sert? Si je spécifie à la fois foo et bar: s.format(foo='FOO',bar='BAR')alors j'ai toujours 'FOO {bar}', quoi qu'il arrive . Pouvez-vous clarifier cela?
n611x007

10
Que vous ne puissiez pas remplir les deux à la fois est ennuyeux. Ceci est utile lorsque, pour une raison quelconque, vous devez formater votre chaîne par étapes et que vous connaissez l'ordre de ces étapes.
aaren

1
Vous devriez probablement concevoir votre moyen de ne pas avoir à faire cela, mais c'est peut-être que vous y êtes obligé.
aaren

2
Je ne savais pas à ce sujet. J'ai eu plusieurs cas d'utilisation où je voulais "amorcer" une chaîne comme mini modèle
ejrb

C'est très utile lorsque vous remplissez une partie d'une chaîne dans une partie de votre code, mais en laissant un espace réservé à remplir ultérieurement dans une autre partie de votre code.
Alex Petralia

99

Vous pouvez utiliser la partialfonction à partir de functoolslaquelle est courte, la plus lisible et décrit également l'intention du codeur:

from functools import partial

s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR

2
Non seulement la solution la plus courte et la plus lisible, mais décrit également l'intention du codeur. Version Python3:python from functool import partial s = "{foo} {bar}".format s_foo = partial(s, foo="FOO") print(s_foo(bar="BAR")) # FOO BAR print(s(foo="FOO", bar="BAR")) # FOO BAR
Paul Brown

@PaulBrown vrai, la réponse a besoin d'un peu d'amour;)
ypercubeᵀᴹ

8
@ ypercubeᵀᴹ Eh bien, je ne suis pas sûr que ce soit exactement ce que la plupart des gens recherchent. partial()ne va pas m'aider si j'ai besoin de faire un traitement avec la chaîne partiellement formatée (c'est-à-dire "FOO {bar}").
Delgan

1
C'est mieux dans le cas où vous utilisez une entrée que vous ne contrôlez pas à 100%. Imaginez: à "{foo} {{bar}}".format(foo="{bar}").format(bar="123")partir des autres exemples. Je m'attendrais "{bar} 123"mais ils sortent "123 123".
Benjamin Manns

50

Cette limitation de .format()- l'incapacité de faire des substitutions partielles - m'a dérangé.

Après avoir évalué l'écriture d'une Formatterclasse personnalisée comme décrit dans de nombreuses réponses ici et même envisagé d'utiliser des packages tiers tels que lazy_format , j'ai découvert une solution intégrée beaucoup plus simple: les chaînes de modèle

Il fournit des fonctionnalités similaires mais fournit également une safe_substitute()méthode approfondie de substitution partielle . Les chaînes de modèle doivent avoir un $préfixe (ce qui semble un peu étrange - mais la solution globale, je pense, est meilleure).

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 emballage pratique 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 même, 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)

29

Je ne sais pas si cela est correct comme solution de contournement rapide, mais que diriez-vous

s = '{foo} {bar}'
s.format(foo='FOO', bar='{bar}')

? :)


J'ai totalement fait la même chose, j'aurais aimé savoir s'il y avait des mises en garde à le faire.
ramgo

11

Si vous définissez le vôtre Formatterqui remplace la get_valueméthode, vous pouvez l'utiliser pour mapper des noms de champs non définis à ce que vous voulez:

http://docs.python.org/library/string.html#string.Formatter.get_value

Par exemple, vous pouvez mapper barà "{bar}"si barn'est pas dans les kwargs.

Cependant, cela nécessite d'utiliser la format()méthode de votre objet Formatter, pas la format()méthode de la chaîne .


On dirait une fonctionnalité python> = 2.6.
n611x007

11
>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'

Essayez ceci.


Wow, exactement ce dont j'ai besoin! Voulez-vous l'expliquer?
Sergey Chizhik

1
{{et }}est un moyen d'échapper aux marques de mise en forme, donc format()n'effectue pas de substitution et remplace {{et }}avec {et }, respectivement.
7yl4r

Le problème de cette solution est que le double {{ }}ne fonctionne que pour un format, si vous devez en appliquer plus, vous devrez en ajouter plus {}. ex. 'fd:{uid}:{{topic_id}}'.format(uid=123).format(a=1)renverra une erreur car le second format ne fournit pas la topic_idvaleur.
Franzi

7

Grâce au commentaire d' 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

On dirait une fonctionnalité python> = 2.6.
n611x007

J'utilise définitivement cette solution :) Merci!
astrojuanlu

2
Sachez que cela perdra la conversion et la spécification de format si elles existent (et elle applique réellement la spécification de format à la valeur renvoyée. Ie ( {field!s: >4}devient{field}
Brendan Abel

3

Pour moi, c'était assez bien:

>>> 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'

3

Toutes les solutions que j'ai trouvées semblaient avoir des problèmes avec des spécifications ou des options de conversion plus avancées. @ SvenMarnach de FormatPlaceholder est merveilleusement intelligent , mais il ne fonctionne pas correctement avec la contrainte (par exemple {a!s:>2s}) , car il appelle la __str__méthode (dans cet exemple) au lieu de __format__et vous perdez toute mise en forme supplémentaire.

Voici ce avec quoi je me suis retrouvé et certaines de ses fonctionnalités clés:

sformat('The {} is {}', 'answer')
'The answer is {}'

sformat('The answer to {question!r} is {answer:0.2f}', answer=42)
'The answer to {question!r} is 42.00'

sformat('The {} to {} is {:0.{p}f}', 'answer', 'everything', p=4)
'The answer to everything is {:0.4f}'
  • fournit une interface similaire str.format(pas seulement un mappage)
  • prend en charge des options de formatage plus complexes:
    • coercition {k!s} {!r}
    • nidification {k:>{size}}
    • getattr {k.foo}
    • obtenir l'article {k[0]}
    • coercition + formatage {k!s:>{size}}
import string


class SparseFormatter(string.Formatter):
    """
    A modified string formatter that handles a sparse set of format
    args/kwargs.
    """

    # re-implemented this method for python2/3 compatibility
    def vformat(self, format_string, args, kwargs):
        used_args = set()
        result, _ = self._vformat(format_string, args, kwargs, used_args, 2)
        self.check_unused_args(used_args, args, kwargs)
        return result

    def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
                 auto_arg_index=0):
        if recursion_depth < 0:
            raise ValueError('Max string recursion exceeded')
        result = []
        for literal_text, field_name, format_spec, conversion in \
                self.parse(format_string):

            orig_field_name = field_name

            # output the literal text
            if literal_text:
                result.append(literal_text)

            # if there's a field, output it
            if field_name is not None:
                # this is some markup, find the object and do
                #  the formatting

                # handle arg indexing when empty field_names are given.
                if field_name == '':
                    if auto_arg_index is False:
                        raise ValueError('cannot switch from manual field '
                                         'specification to automatic field '
                                         'numbering')
                    field_name = str(auto_arg_index)
                    auto_arg_index += 1
                elif field_name.isdigit():
                    if auto_arg_index:
                        raise ValueError('cannot switch from manual field '
                                         'specification to automatic field '
                                         'numbering')
                    # disable auto arg incrementing, if it gets
                    # used later on, then an exception will be raised
                    auto_arg_index = False

                # given the field_name, find the object it references
                #  and the argument it came from
                try:
                    obj, arg_used = self.get_field(field_name, args, kwargs)
                except (IndexError, KeyError):
                    # catch issues with both arg indexing and kwarg key errors
                    obj = orig_field_name
                    if conversion:
                        obj += '!{}'.format(conversion)
                    if format_spec:
                        format_spec, auto_arg_index = self._vformat(
                            format_spec, args, kwargs, used_args,
                            recursion_depth, auto_arg_index=auto_arg_index)
                        obj += ':{}'.format(format_spec)
                    result.append('{' + obj + '}')
                else:
                    used_args.add(arg_used)

                    # do any conversion on the resulting object
                    obj = self.convert_field(obj, conversion)

                    # expand the format spec, if needed
                    format_spec, auto_arg_index = self._vformat(
                        format_spec, args, kwargs,
                        used_args, recursion_depth-1,
                        auto_arg_index=auto_arg_index)

                    # format the object and append to the result
                    result.append(self.format_field(obj, format_spec))

        return ''.join(result), auto_arg_index


def sformat(s, *args, **kwargs):
    # type: (str, *Any, **Any) -> str
    """
    Sparse format a string.

    Parameters
    ----------
    s : str
    args : *Any
    kwargs : **Any

    Examples
    --------
    >>> sformat('The {} is {}', 'answer')
    'The answer is {}'

    >>> sformat('The answer to {question!r} is {answer:0.2f}', answer=42)
    'The answer to {question!r} is 42.00'

    >>> sformat('The {} to {} is {:0.{p}f}', 'answer', 'everything', p=4)
    'The answer to everything is {:0.4f}'

    Returns
    -------
    str
    """
    return SparseFormatter().format(s, *args, **kwargs)

J'ai découvert les problèmes avec les différentes implémentations après avoir écrit quelques tests sur la façon dont je voulais que cette méthode se comporte. Ils sont ci-dessous si quelqu'un les trouve perspicaces.

import pytest


def test_auto_indexing():
    # test basic arg auto-indexing
    assert sformat('{}{}', 4, 2) == '42'
    assert sformat('{}{} {}', 4, 2) == '42 {}'


def test_manual_indexing():
    # test basic arg indexing
    assert sformat('{0}{1} is not {1} or {0}', 4, 2) == '42 is not 2 or 4'
    assert sformat('{0}{1} is {3} {1} or {0}', 4, 2) == '42 is {3} 2 or 4'


def test_mixing_manualauto_fails():
    # test mixing manual and auto args raises
    with pytest.raises(ValueError):
        assert sformat('{!r} is {0}{1}', 4, 2)


def test_kwargs():
    # test basic kwarg
    assert sformat('{base}{n}', base=4, n=2) == '42'
    assert sformat('{base}{n}', base=4, n=2, extra='foo') == '42'
    assert sformat('{base}{n} {key}', base=4, n=2) == '42 {key}'


def test_args_and_kwargs():
    # test mixing args/kwargs with leftovers
    assert sformat('{}{k} {v}', 4, k=2) == '42 {v}'

    # test mixing with leftovers
    r = sformat('{}{} is the {k} to {!r}', 4, 2, k='answer')
    assert r == '42 is the answer to {!r}'


def test_coercion():
    # test coercion is preserved for skipped elements
    assert sformat('{!r} {k!r}', '42') == "'42' {k!r}"


def test_nesting():
    # test nesting works with or with out parent keys
    assert sformat('{k:>{size}}', k=42, size=3) == ' 42'
    assert sformat('{k:>{size}}', size=3) == '{k:>3}'


@pytest.mark.parametrize(
    ('s', 'expected'),
    [
        ('{a} {b}', '1 2.0'),
        ('{z} {y}', '{z} {y}'),
        ('{a} {a:2d} {a:04d} {y:2d} {z:04d}', '1  1 0001 {y:2d} {z:04d}'),
        ('{a!s} {z!s} {d!r}', '1 {z!s} {\'k\': \'v\'}'),
        ('{a!s:>2s} {z!s:>2s}', ' 1 {z!s:>2s}'),
        ('{a!s:>{a}s} {z!s:>{z}s}', '1 {z!s:>{z}s}'),
        ('{a.imag} {z.y}', '0 {z.y}'),
        ('{e[0]:03d} {z[0]:03d}', '042 {z[0]:03d}'),
    ],
    ids=[
        'normal',
        'none',
        'formatting',
        'coercion',
        'formatting+coercion',
        'nesting',
        'getattr',
        'getitem',
    ]
)
def test_sformat(s, expected):
    # test a bunch of random stuff
    data = dict(
        a=1,
        b=2.0,
        c='3',
        d={'k': 'v'},
        e=[42],
    )
    assert expected == sformat(s, **data)

J'ai ajouté une réponse similaire au code @SvenMarnach mais qui gère correctement la coercition pour vos tests.
Tohiko le

1

Ma suggestion serait la suivante (testée 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 dict-classement et surcharge __missing__(self, key)) est présentée ici: https://stackoverflow.com/a/17215533/333403


0

En supposant que vous n'utiliserez pas la chaîne tant qu'elle n'est pas complètement remplie, vous pouvez 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'

0

Il existe une autre façon d'y parvenir, c'est-à-dire en utilisant formatet %en remplaçant des 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'

0

Une solution très moche mais la plus simple pour moi est de simplement faire:

tmpl = '{foo}, {bar}'
tmpl.replace('{bar}', 'BAR')
Out[3]: '{foo}, BAR'

De cette façon, vous pouvez toujours utiliser tmplcomme modèle régulier et effectuer un formatage partiel uniquement lorsque cela est nécessaire. Je trouve ce problème trop trivial pour utiliser une solution exagérée comme celle de Mohan Raj.


0

Après avoir testé les solutions les plus prometteuses ici et , je me suis rendu compte qu'aucune d'entre elles ne répondait vraiment aux exigences suivantes:

  1. adhérer strictement à la syntaxe reconnue par str.format_map()pour le modèle;
  2. être capable de conserver un formatage complexe, c'est-à-dire prendre pleinement en charge le format mini-langage

J'ai donc écrit ma propre solution, qui satisfait aux exigences ci-dessus. ( EDIT : maintenant la version de @SvenMarnach - comme indiqué dans cette réponse - semble gérer les cas de coin dont j'avais besoin).

Fondamentalement, j'ai fini par analyser la chaîne de modèle, trouver des {.*?}groupes imbriqués correspondants (en utilisant une find_all()fonction d'assistance) et construire la chaîne formatée progressivement et directement en utilisant str.format_map()tout en captant tout 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 - AVIS DE NON -RESPONSABILITÉ: 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 à ma solution préférée (par @SvenMarnach qui a gentiment partagé son code ici et ):

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 voir, la version mise à jour semble maintenant bien gérer les cas de coin où la version antérieure échouait.


Dans le temps, ils sont à env. 50% les uns des autres, selon le textformat réel (et probablement le réel source), mais safe_format_map()semble avoir un avantage dans la plupart des tests que j'ai effectués (quoi qu'ils signifient, 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)

Notez que ce {d[x]}n'est pas une chaîne de format valide pour autant que je sache.
Sven Marnach le

@SvenMarnach Les documents officiels le disent explicitement field_name ::= arg_name ("." attribute_name | "[" element_index "]")*, les deux str.format()et le str.format_map()comprennent. Je dirais qu'il y a suffisamment de preuves pour qu'il s'agisse d'une chaîne de format valide.
norok2

Pouvez-vous donner un exemple d'utilisation str.format()avec un index non entier entre crochets? Je ne peux faire fonctionner que les index entiers.
Sven Marnach du

@SvenMarnach a = dict(b='YAY!'); '{a[b]}'.format_map(dict(a=a))vous obtient `` YAY! ''
norok2

1
Ah, je vois. Je supposais que cela était interprété comme a[b]dans le code Python, mais c'est en fait a["b"]Merci!
Sven Marnach

0

Si vous souhaitez décompresser un dictionnaire auquel passer des arguments format, comme dans cette question connexe , vous pouvez utiliser la méthode suivante.

Supposons d'abord que la chaîne sest la même que dans cette question:

s = '{foo} {bar}'

et les valeurs sont données par le dictionnaire suivant:

replacements = {'foo': 'FOO'}

Clairement, cela ne fonctionnera pas:

s.format(**replacements)
#---------------------------------------------------------------------------
#KeyError                                  Traceback (most recent call last)
#<ipython-input-29-ef5e51de79bf> in <module>()
#----> 1 s.format(**replacements)
#
#KeyError: 'bar'

Cependant, vous pouvez d'abord obtenir un setde tous les arguments nomméss et créer un dictionnaire qui mappe l'argument à lui-même entouré d'accolades:

from string import Formatter
args = {x[1]:'{'+x[1]+'}' for x in Formatter().parse(s)}
print(args)
#{'foo': '{foo}', 'bar': '{bar}'}

Utilisez maintenant le argsdictionnaire pour renseigner les clés manquantes replacements. Pour python 3.5+, vous pouvez le faire dans une seule expression :

new_s = s.format(**{**args, **replacements}}
print(new_s)
#'FOO {bar}'

Pour les anciennes versions de python, vous pouvez appeler update:

args.update(replacements)
print(s.format(**args))
#'FOO {bar}'

0

J'aime la réponse @ sven-marnach. Ma réponse en est simplement une version étendue. Il permet le formatage sans mot-clé et ignore les clés supplémentaires. Voici des exemples d'utilisation (le nom d'une fonction est une référence au formatage de chaîne f python 3.6):

# partial string substitution by keyword
>>> f('{foo} {bar}', foo="FOO")
'FOO {bar}'

# partial string substitution by argument
>>> f('{} {bar}', 1)
'1 {bar}'

>>> f('{foo} {}', 1)
'{foo} 1'

# partial string substitution with arguments and keyword mixed
>>> f('{foo} {} {bar} {}', '|', bar='BAR')
'{foo} | BAR {}'

# partial string substitution with extra keyword
>>> f('{foo} {bar}', foo="FOO", bro="BRO")
'FOO {bar}'

# you can simply 'pour out' your dictionary to format function
>>> kwargs = {'foo': 'FOO', 'bro': 'BRO'}
>>> f('{foo} {bar}', **kwargs)
'FOO {bar}'

Et voici mon code:

from string import Formatter


class FormatTuple(tuple):
    def __getitem__(self, key):
        if key + 1 > len(self):
            return "{}"
        return tuple.__getitem__(self, key)


class FormatDict(dict):
    def __missing__(self, key):
        return "{" + key + "}"


def f(string, *args, **kwargs):
    """
    String safe substitute format method.
    If you pass extra keys they will be ignored.
    If you pass incomplete substitute map, missing keys will be left unchanged.
    :param string:
    :param kwargs:
    :return:

    >>> f('{foo} {bar}', foo="FOO")
    'FOO {bar}'
    >>> f('{} {bar}', 1)
    '1 {bar}'
    >>> f('{foo} {}', 1)
    '{foo} 1'
    >>> f('{foo} {} {bar} {}', '|', bar='BAR')
    '{foo} | BAR {}'
    >>> f('{foo} {bar}', foo="FOO", bro="BRO")
    'FOO {bar}'
    """
    formatter = Formatter()
    args_mapping = FormatTuple(args)
    mapping = FormatDict(kwargs)
    return formatter.vformat(string, args_mapping, mapping)

0

Si vous faites beaucoup de modèles et que vous trouvez que la fonctionnalité de modèles de chaînes intégrée de Python est insuffisante ou maladroite, regardez Jinja2 .

À partir de la documentation:

Jinja est un langage de création de modèles moderne et convivial pour Python, inspiré des modèles de Django.


0

En lisant le commentaire de @Sam Bourne, j'ai modifié le code de @ SvenMarnach pour qu'il fonctionne correctement avec la coercition (comme {a!s:>2s}) sans écrire un analyseur personnalisé. L'idée de base n'est pas de convertir en chaînes mais de concaténer les clés manquantes avec des balises de coercition.

import string
class MissingKey(object):
    def __init__(self, key):
        self.key = key

    def __str__(self):  # Supports {key!s}
        return MissingKeyStr("".join([self.key, "!s"]))

    def __repr__(self):  # Supports {key!r}
        return MissingKeyStr("".join([self.key, "!r"]))

    def __format__(self, spec): # Supports {key:spec}
        if spec:
            return "".join(["{", self.key, ":", spec, "}"])
        return "".join(["{", self.key, "}"])

    def __getitem__(self, i): # Supports {key[i]}
        return MissingKey("".join([self.key, "[", str(i), "]"]))

    def __getattr__(self, name): # Supports {key.name}
        return MissingKey("".join([self.key, ".", name]))


class MissingKeyStr(MissingKey, str):
    def __init__(self, key):
        if isinstance(key, MissingKey):
            self.key = "".join([key.key, "!s"])
        else:
            self.key = key

class SafeFormatter(string.Formatter):
    def __init__(self, default=lambda k: MissingKey(k)):
        self.default=default

    def get_value(self, key, args, kwds):
        if isinstance(key, str):
            return kwds.get(key, self.default(key))
        else:
            return super().get_value(key, args, kwds)

Utilisez (par exemple) comme ceci

SafeFormatter().format("{a:<5} {b:<10}", a=10)

Les tests suivants (inspirés des tests de @ norok2) vérifient la sortie pour le traditionnel format_mapet un safe_format_mapbasé sur la classe ci-dessus dans deux cas: fournir des mots-clés corrects ou sans eux.

def safe_format_map(text, source):
    return SafeFormatter().format(text, **source)

test_texts = (
    '{a} ',             # simple nothing useful in source
    '{a:5d}',       # formatting
    '{a!s}',        # coercion
    '{a!s:>{a}s}',  # formatting and coercion
    '{a:0{a}d}',    # nesting
    '{d[x]}',       # indexing
    '{d.values}',   # member
)

source = dict(a=10,d=dict(x='FOO'))
funcs = [safe_format_map,
         str.format_map
         #safe_format_alt  # Version based on parsing (See @norok2)
         ]
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))
    print("Testing:", text)
    for func in funcs:
        try:
            print(f'{func.__name__:>{n}s} : OK\t\t\t: ' + func(text, dict()))
        except:
            print(f'{func.__name__:>{n}s} : FAILED')

        try:
            print(f'{func.__name__:>{n}s} : OK\t\t\t: ' + func(text, source))
        except:
            print(f'{func.__name__:>{n}s} : FAILED')

Quelles sorties

Testing: {a} 
   safe_format_map : OK         : {a} 
   safe_format_map : OK         : 10 
        format_map : FAILED
        format_map : OK         : 10 
Testing: {a:5d}
   safe_format_map : OK         : {a:5d}
   safe_format_map : OK         :    10
        format_map : FAILED
        format_map : OK         :    10
Testing: {a!s}
   safe_format_map : OK         : {a!s}
   safe_format_map : OK         : 10
        format_map : FAILED
        format_map : OK         : 10
Testing: {a!s:>{a}s}
   safe_format_map : OK         : {a!s:>{a}s}
   safe_format_map : OK         :         10
        format_map : FAILED
        format_map : OK         :         10
Testing: {a:0{a}d}
   safe_format_map : OK         : {a:0{a}d}
   safe_format_map : OK         : 0000000010
        format_map : FAILED
        format_map : OK         : 0000000010
Testing: {d[x]}
   safe_format_map : OK         : {d[x]}
   safe_format_map : OK         : FOO
        format_map : FAILED
        format_map : OK         : FOO
Testing: {d.values}
   safe_format_map : OK         : {d.values}
   safe_format_map : OK         : <built-in method values of dict object at 0x7fe61e230af8>
        format_map : FAILED
        format_map : OK         : <built-in method values of dict object at 0x7fe61e230af8>

-2

Vous pouvez l'envelopper dans une fonction qui prend les arguments par défaut:

def print_foo_bar(foo='', bar=''):
    s = '{foo} {bar}'
    return s.format(foo=foo, bar=bar)

print_foo_bar(bar='BAR') # ' BAR'

Vous remplacez {foo} par une chaîne vide. La question concerne le formatage partiel pour une mise en forme finale supplémentaire, sans ignorer les champs manquants.
egvo le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.