formattazione parziale della stringa


128

È possibile eseguire la formattazione parziale della stringa con i metodi avanzati di formattazione della stringa, in modo simile alla safe_substitute()funzione del modello di stringa ?

Per esempio:

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

Risposte:


58

Puoi ingannare la formattazione parziale sovrascrivendo la mappatura:

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

stampa

FOO {bar}

Naturalmente questa implementazione di base funziona correttamente solo per i casi di base.


7
Questo non funziona per formattazioni più avanzate come{bar:1.2f}
MaxNoe,

Comprendo che "l'implementazione più semplice funziona correttamente solo per i casi di base", ma c'è un modo per espanderlo anche solo per non eliminare le specifiche del formato?
Tadhg McDonald-Jensen,

5
@ TadhgMcDonald-Jensen: Sì, c'è un modo. Invece di restituire una stringa __missing__(), restituisce un'istanza di una classe personalizzata che sostituisce __format__()in modo da restituire il segnaposto originale, comprese le specifiche del formato. Prova del concetto: ideone.com/xykV7R
Sven Marnach,

@SvenMarnach perché la prova del concetto non è nel corpo della tua risposta? È un po 'sfuggente. Ci sono avvertimenti noti che ti impediscono di promuoverlo?
norok2

1
@ norok2 È una risposta a una domanda posta in un commento, quindi ho inserito la risposta in un commento. La domanda originale non includeva davvero quel requisito, e generalmente penso ancora che sia un po 'strano provare a formattare parzialmente una stringa.
Sven Marnach,

128

Se sai in quale ordine stai formattando le cose:

s = '{foo} {{bar}}'

Usalo in questo modo:

ss = s.format(foo='FOO') 
print ss 
>>> 'FOO {bar}'

print ss.format(bar='BAR')
>>> 'FOO BAR'

Non è possibile specificare fooe barallo stesso tempo, è necessario farlo in sequenza.


Che senso ha questo? Se specifico sia foo che bar: s.format(foo='FOO',bar='BAR')allora ho ancora 'FOO {bar}', qualunque cosa accada. Potresti chiarirlo?
n611x007,

10
Che non sia possibile compilare entrambi contemporaneamente è fastidioso. Ciò è utile quando, per qualsiasi motivo, devi formattare la stringa in più fasi e conosci l'ordine di tali fasi.
aaren,

1
Probabilmente dovresti progettare la tua via d'uscita dal doverlo fare, ma forse sei costretto a farlo.
aaren,

2
Non lo sapevo. Ho avuto diversi casi d'uso in cui volevo "adescare" una stringa come mini template
ejrb

Ciò è molto utile quando si popola una parte di una stringa in una parte del codice, ma si lascia che un segnaposto venga popolato successivamente in un'altra parte del codice.
Alex Petralia,

98

È possibile utilizzare la partialfunzione dalla functoolsquale è breve, più leggibile e descrive anche l'intenzione del programmatore:

from functools import partial

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

2
Non solo la soluzione più breve e più leggibile, ma descrive anche l'intenzione del programmatore. Versione 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 vero, la risposta ha bisogno di un po 'd'amore;)
ypercubeᵀᴹ

8
@ ypercubeᵀᴹ Beh, non sono sicuro che sia esattamente quello che la maggior parte delle persone sta cercando. partial()non mi aiuterà se devo fare un po 'di elaborazione con la stringa parzialmente formattata (cioè "FOO {bar}").
Delgan,

1
Questo è meglio nel caso in cui si operi su input che non si controlla al 100%. Immagina: "{foo} {{bar}}".format(foo="{bar}").format(bar="123")dagli altri esempi. Mi sarei aspettato, "{bar} 123"ma hanno prodotto "123 123".
Benjamin Manns,

50

Questa limitazione .format()- l'incapacità di effettuare sostituzioni parziali - mi ha infastidito.

Dopo aver valutato la scrittura di una Formatterclasse personalizzata come descritto in molte risposte qui e anche considerando l'utilizzo di pacchetti di terze parti come lazy_format , ho scoperto una soluzione integrata molto più semplice: stringhe di modelli

Fornisce funzionalità simili ma fornisce anche un safe_substitute()metodo completo di sostituzione parziale . Le stringhe del modello devono avere un $prefisso (che sembra un po 'strano - ma la soluzione complessiva penso sia migliore).

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'

Costituito da un comodo wrapper basato su questo:

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

Allo stesso modo un wrapper basato sulla risposta di Sven che utilizza la formattazione di stringa predefinita:

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

Non sono sicuro se questo va bene come soluzione rapida, ma che ne dici

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

? :)


Ho fatto lo stesso, vorrei sapere se ci fossero avvertimenti nel farlo.
Ramgo,

11

Se definisci il tuo Formatterche sovrascrive il get_valuemetodo, puoi usarlo per mappare i nomi dei campi indefiniti a quello che volevi:

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

Per esempio, si potrebbe mappare barper "{bar}"se barnon è nelle kwargs.

Tuttavia, ciò richiede l'utilizzo del format()metodo dell'oggetto Formatter, non del format()metodo della stringa .


Sembra una funzione python> = 2.6.
n611x007,

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

Prova questo.


Caspita, esattamente quello di cui ho bisogno! Lo spiegheresti?
Sergey Chizhik,

1
{{ed }}è un modo per sfuggire ai segni di formattazione, quindi format()non esegue la sostituzione e sostituisce {{e }}con {e }, rispettivamente.
7yl4r

Il problema di questa soluzione è che il doppio {{ }}funziona solo per un formato, se è necessario applicare di più è necessario aggiungerne di più {}. ex. 'fd:{uid}:{{topic_id}}'.format(uid=123).format(a=1)restituirà un errore poiché il secondo formato non fornisce il topic_idvalore.
Franzi,

7

Grazie al commento di Amber , ho pensato a questo:

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

Sembra una funzione python> = 2.6.
n611x007,

Sto sicuramente usando questa soluzione :) Grazie!
astrojuanlu,

2
Essere consapevoli del fatto che questo perderà la conversione e le specifiche del formato se esistono (e in realtà applica le specifiche del formato al valore restituito. Cioè ( {field!s: >4}diventa{field}
Brendan Abel

3

Per me questo è stato abbastanza buono:

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

Tutte le soluzioni che ho trovato sembravano avere problemi con specifiche più avanzate o opzioni di conversione. @ Di SvenMarnach FormatPlaceholder è meravigliosamente intelligente, ma non funziona correttamente con la coercizione (ad esempio {a!s:>2s}), perché chiama il __str__metodo (in questo esempio), invece di __format__e si perde qualsiasi formattazione aggiuntiva.

Ecco cosa ho finito e alcune delle sue caratteristiche principali:

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}'
  • fornisce un'interfaccia simile a str.format(non solo una mappatura)
  • supporta opzioni di formattazione più complesse:
    • coercizione {k!s} {!r}
    • Nidificazione {k:>{size}}
    • getattr {k.foo}
    • GetItem {k[0]}
    • coercizione + formattazione {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)

Ho scoperto i problemi con le varie implementazioni dopo aver scritto alcuni test su come volevo che questo metodo si comportasse. Sono sotto se qualcuno li trova perspicaci.

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)

Ho aggiunto una risposta simile al codice @SvenMarnach ma che gestisce correttamente la coercizione per i tuoi test.
Tohiko,

1

Il mio suggerimento sarebbe il seguente (testato con 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'

Aggiornamento: un modo ancora più elegante (sottoclasse dicte sovraccarico __missing__(self, key)) è mostrato qui: https://stackoverflow.com/a/17215533/333403


0

Supponendo che non userai la stringa fino a quando non sarà completamente compilata, potresti fare qualcosa come questa 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)

Esempio:

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

Esiste un altro modo per raggiungere questo obiettivo, ovvero utilizzando formate %sostituendo le variabili. Per esempio:

>>> s = '{foo} %(bar)s'
>>> s = s.format(foo='my_foo')
>>> s
'my_foo %(bar)s'
>>> s % {'bar': 'my_bar'}
'my_foo my_bar'

0

Una soluzione molto brutta ma la più semplice per me è semplicemente fare:

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

In questo modo è ancora possibile utilizzare tmplcome modello normale ed eseguire la formattazione parziale solo quando necessario. Trovo questo problema troppo banale per usare una soluzione eccessiva come quella di Mohan Raj.


0

Dopo aver testato le soluzioni più promettenti di qua e di , mi sono reso conto che nessuno di loro soddisfaceva davvero i seguenti requisiti:

  1. aderire rigorosamente alla sintassi riconosciuta str.format_map()per il modello;
  2. essere in grado di mantenere una formattazione complessa, ovvero supportare pienamente il formato mini-lingua

Quindi, ho scritto la mia soluzione, che soddisfa i requisiti di cui sopra. ( EDIT : ora la versione di @SvenMarnach - come riportato in questa risposta - sembra gestire i casi angolari di cui avevo bisogno).

Fondamentalmente, ho finito per analizzare la stringa del modello, trovando i {.*?}gruppi nidificati corrispondenti (usando una find_all()funzione di supporto) e costruendo la stringa formattata progressivamente e usando direttamentestr.format_map() mentre coglievo qualsiasi potenziale 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

(Questo codice è disponibile anche in FlyingCircus - DISCLAIMER: ne sono l'autore principale.)


L'utilizzo per questo codice sarebbe:

print(safe_format_map('{a} {b} {c}', dict(a=-A-)))
# -A- {b} {c}

Confrontiamolo con la mia soluzione preferita (di @SvenMarnach che ha gentilmente condiviso il suo codice qua e ):

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

Ecco un paio di test:

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=[])

e il codice per farlo funzionare:

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

con il risultato di:

    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}

come puoi vedere, ora la versione aggiornata sembra gestire bene i casi angolari in cui la versione precedente non funzionava.


Nel tempo, sono entro ca. Il 50% l'uno dall'altro, a seconda dell'effettivo textda formattare (e probabilmente dell'attuale source), ma safe_format_map()sembra avere un vantaggio nella maggior parte dei test che ho eseguito (qualunque cosa significino, ovviamente):

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)

Si noti che {d[x]}non è una stringa di formato valida per quanto ne so.
Sven Marnach,

@SvenMarnach I documenti ufficiali lo dicono esplicitamente field_name ::= arg_name ("." attribute_name | "[" element_index "]")*e entrambi str.format()e lo str.format_map()comprendono. Direi che ci sono prove sufficienti per questo essere una stringa di formato valida.
norok2,

Puoi fare un esempio di utilizzo str.format()con un indice non intero tra parentesi quadre? Posso solo far funzionare gli indici interi.
Sven Marnach,

@SvenMarnach a = dict(b='YAY!'); '{a[b]}'.format_map(dict(a=a))ti dà `` YAY! '
norok2,

1
Ah, capisco. Supponevo che questo venisse interpretato come a[b]nel codice Python, ma in realtà è a["b"]Grazie!
Sven Marnach,

0

Se desideri decomprimere un dizionario per passare argomenti format, come in questa domanda correlata , puoi utilizzare il seguente metodo.

Supponiamo innanzitutto che la stringa ssia la stessa di questa domanda:

s = '{foo} {bar}'

e i valori sono dati dal seguente dizionario:

replacements = {'foo': 'FOO'}

Chiaramente questo non funzionerà:

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

Tuttavia, potresti prima ottenere uno setdi tutti gli argomenti nominatis e creare un dizionario che associ l'argomento a se stesso racchiuso tra parentesi graffe:

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

Ora usa il argsdizionario per inserire le chiavi mancanti replacements. Per python 3.5+, puoi farlo in una singola espressione :

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

Per le versioni precedenti di Python, è possibile chiamare update:

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

0

Mi piace la risposta di @ sven-marnach. La mia risposta è semplicemente una versione estesa di esso. Consente la formattazione senza parole chiave e ignora le chiavi extra. Ecco alcuni esempi di utilizzo (il nome di una funzione è un riferimento alla formattazione f-string di 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}'

Ed ecco il mio codice:

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

Se stai facendo un sacco di modelli e trovi che la funzionalità di templating delle stringhe integrata in Python è insufficiente o goffa, guarda Jinja2 .

Dai documenti:

Jinja è un linguaggio di template moderno e di design per Python, modellato sui modelli di Django.


0

Leggendo il commento di @Sam Bourne, ho modificato il codice di @SvenMarnach per funzionare correttamente con la coercizione (come {a!s:>2s}) senza scrivere un parser personalizzato. L'idea di base non è convertire in stringhe ma concatenare chiavi mancanti con tag di coercizione.

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)

Utilizzare (ad esempio) in questo modo

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

I seguenti test (ispirati ai test di @ norok2) controllano l'output per il tradizionale format_mape uno safe_format_mapbasato sulla classe sopra in due casi: fornire parole chiave corrette o senza di esse.

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

Quali uscite

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

Potresti avvolgerlo in una funzione che accetta argomenti predefiniti:

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

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

Stai sostituendo {pippo} con una stringa vuota. La domanda riguarda la formattazione parziale per un'ulteriore formattazione finale, non ignorare i campi mancanti.
egvo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.