formatação parcial de string


128

É possível fazer formatação parcial de sequência com os métodos avançados de formatação de sequência, semelhante à safe_substitute()função do modelo de sequência ?

Por exemplo:

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

Respostas:


58

Você pode enganá-lo para formatação parcial, substituindo o mapeamento:

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

impressão

FOO {bar}

Obviamente, essa implementação básica só funciona corretamente para casos básicos.


7
Isso não funciona para formatações mais avançadas como{bar:1.2f}
MaxNoe

Entendo dizer que "a implementação mais básica só funciona corretamente para os casos básicos", mas existe uma maneira de expandir isso, mesmo para não excluir as especificações de formato?
Tadhg McDonald-Jensen

5
@ TadhgMcDonald-Jensen: Sim, existe um caminho. Em vez de retornar uma string __missing__(), retorne uma instância de uma classe personalizada substituindo de __format__()forma a retornar o espaço reservado original, incluindo a especificação de formato. Prova de conceito: ideone.com/xykV7R
Sven Marnach 26/16

@SvenMarnach Por que sua prova de conceito não está no corpo de sua resposta? Isso é um pouco esquivo. Existem advertências conhecidas que o impedem de promovê-lo?
Norok2 26/07/19

1
@ norok2 É uma resposta a uma pergunta feita em um comentário, então coloquei a resposta em um comentário. A pergunta original realmente não incluía esse requisito, e geralmente ainda acho um pouco estranho tentar formatar parcialmente uma string.
Sven Marnach 26/07/19

128

Se você sabe em que ordem está formatando as coisas:

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

Use-o assim:

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

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

Você não pode especificar fooe barao mesmo tempo - você deve fazer isso sequencialmente.


Qual o sentido disso? Se eu especificar foo e bar: s.format(foo='FOO',bar='BAR')então ainda recebi 'FOO {bar}', não importa o quê. Você poderia esclarecer isso?
N611x007 11/11

10
Que você não pode preencher os dois ao mesmo tempo é irritante. Isso é útil quando, por qualquer motivo, você precisar formatar sua sequência em estágios e conhecer a ordem desses estágios.
Aaren

1
Provavelmente, você deve evitar isso, mas talvez seja obrigado a fazê-lo.
Aaren

2
Não sabia disso. Eu tive vários casos de uso onde eu queria "prime" uma string como um modelo de mini
ejrb

Isso é super útil ao preencher parte de uma seqüência de caracteres em uma parte do seu código, mas deixando um espaço reservado para ser preenchido posteriormente em outra parte do seu código.
Alex Petralia

98

Você pode usar a partialfunção da functoolsqual é curta, mais legível e também descreve a intenção do codificador:

from functools import partial

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

2
Não apenas a solução mais curta e legível, mas também descreve a intenção do codificador. Versão do 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 verdade, a resposta precisa de um pouco de amor;)
ypercubeᵀᴹ

8
@ ypercubeᵀᴹ Bem, não tenho certeza se é exatamente isso que a maioria das pessoas está procurando. partial()não vai me ajudar se eu precisar fazer algum processamento com a string parcialmente formatada (ou seja "FOO {bar}").
Delgan

1
Isso é melhor para o caso em que você está operando com entradas que não controla 100%. Imagine: "{foo} {{bar}}".format(foo="{bar}").format(bar="123")dos outros exemplos. Eu esperaria, "{bar} 123"mas eles produzem "123 123".
Benjamin Manns

50

Essa limitação .format()- a incapacidade de fazer substituições parciais - está me incomodando.

Depois de avaliar a criação de uma Formatterclasse personalizada, conforme descrito em muitas respostas aqui, e mesmo considerando o uso de pacotes de terceiros, como lazy_format , descobri uma solução embutida muito mais simples: Cadeias de modelo

Ele fornece funcionalidade semelhante, mas também fornece um safe_substitute()método completo de substituição parcial . As strings do modelo precisam ter um $prefixo (o que é um pouco estranho - mas a solução geral é melhor).

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'

Formou um invólucro de conveniência com base nisso:

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

Da mesma forma, um invólucro baseado na resposta de Sven, que usa a formatação padrão de string:

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

Não tenho certeza se isso está ok como uma solução rápida, mas que tal

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

? :)


Eu fiz totalmente o mesmo, gostaria de saber se havia advertências ao fazê-lo.
ramgo


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

Experimente isso.


Uau, exatamente o que eu preciso! Você explicaria isso?
Sergey Chizhik

1
{{e }}é uma maneira de escapar das marcas de formatação, portanto format(), não realiza substituição e substitui {{e }}por {e }, respectivamente.
precisa saber é

O problema desta solução é que o duplo {{ }}funciona apenas para um formato; se você precisar aplicar mais, precisará adicionar mais {}. ex. 'fd:{uid}:{{topic_id}}'.format(uid=123).format(a=1)retornará erro, pois o segundo formato não está fornecendo o topic_idvalor.
Franzi

7

Graças ao comentário de Amber , eu vim com isso:

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

Parece um recurso python> = 2.6.
N611x007 11/11

Definitivamente, estou usando esta solução :) Obrigado!
Astrojuanlu 9/06

2
Esteja ciente de que este vai perder a especificação de conversão e formato se eles existem (e que realmente aplica o formato de especificação para o valor retornado ie (. {field!s: >4}Torna-se{field}
Brendan Abel

3

Para mim, isso foi bom o suficiente:

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

Todas as soluções que encontrei pareciam ter problemas com opções de especificação ou conversão mais avançadas. @ Do SvenMarnach FormatPlaceholder é maravilhosamente inteligente, mas ele não funciona corretamente com a coerção (por exemplo {a!s:>2s}), porque ele chama o __str__método (neste exemplo) em vez de __format__e você perder qualquer formatação adicional.

Aqui está o que eu acabei e algumas de suas principais características:

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}'
  • fornece interface semelhante a str.format(não apenas um mapeamento)
  • suporta opções de formatação mais complexas:
    • coerção {k!s} {!r}
    • aninhamento {k:>{size}}
    • getattr {k.foo}
    • getitem {k[0]}
    • coerção + formatação {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)

Descobri os problemas com as várias implementações depois de escrever alguns testes sobre como eu queria que esse método se comportasse. Eles estão abaixo se alguém os achar perspicazes.

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)

Adicionei uma resposta semelhante ao código @SvenMarnach, mas que lida com coerção corretamente para seus testes.
Tohiko 03/07

1

Minha sugestão seria a seguinte (testada com 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'

Atualização: Uma maneira ainda mais elegante (subclassificação dicte sobrecarga __missing__(self, key)) é mostrada aqui: https://stackoverflow.com/a/17215533/333403


0

Supondo que você não use a string até que ela esteja completamente preenchida, você pode fazer algo parecido com esta 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)

Exemplo:

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

Há mais uma maneira de conseguir isso, ou seja, usando formate %substituindo variáveis. Por exemplo:

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

0

Uma solução muito feia, mas a mais simples, para mim, é:

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

Dessa forma, você ainda pode usar tmplcomo modelo regular e executar a formatação parcial somente quando necessário. Acho esse problema trivial demais para usar uma solução de exagero como a de Mohan Raj.


0

Depois de testar as soluções mais promissoras daqui e dali , percebi que nenhuma delas realmente atendia aos seguintes requisitos:

  1. siga rigorosamente a sintaxe reconhecida por str.format_map()para o modelo;
  2. ser capaz de reter formatação complexa, ou seja, oferecer suporte total à Mini-linguagem Format

Então, eu escrevi minha própria solução, que atende aos requisitos acima. ( EDIT : agora a versão do @SvenMarnach - conforme relatado nesta resposta - parece lidar com os casos de canto que eu precisava).

Basicamente, acabei analisando a sequência do modelo, encontrando {.*?}grupos aninhados correspondentes (usando uma find_all()função auxiliar) e construindo a sequência formatada de forma progressiva e direta usando str.format_map()enquanto captava qualquer potencial 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

(Este código também está disponível no FlyingCircus - AVISO LEGAL: Eu sou o principal autor dele.)


O uso para esse código seria:

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

Vamos comparar isso com a minha solução favorita (por @SvenMarnach, que gentilmente compartilhou seu código aqui e ali ):

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

Aqui estão alguns testes:

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 o código para fazê-lo funcionar:

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

resultando em:

    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}

como você pode ver, a versão atualizada agora parece lidar bem com os casos de canto em que a versão anterior costumava falhar.


Timewise, eles estão dentro de aprox. 50% um do outro, dependendo do textformato real (e provavelmente do formato real source), mas safe_format_map()parece ter uma vantagem na maioria dos testes que realizei (o que eles significam, é claro):

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)

Observe que {d[x]}não é uma string de formato válida, pelo que sei.
Sven Marnach 26/07/19

@SvenMarnach Os documentos oficiais explicitamente explicam os field_name ::= arg_name ("." attribute_name | "[" element_index "]")*dois str.format()e o str.format_map()entendem. Eu diria que há evidências suficientes para que essa seja uma sequência de formato válida.
Norok2 26/07/19

Você pode dar um exemplo de uso str.format()com um índice não inteiro entre colchetes? Só posso fazer com que índices inteiros funcionem.
Sven Marnach 26/07/19

@SvenMarnach a = dict(b='YAY!'); '{a[b]}'.format_map(dict(a=a))leva você `` YAY! ''
Norok2 26/07/19

1
Ah entendo. Eu estava assumindo que isso é interpretado como a[b]no código Python, mas na verdade é a["b"]obrigado!
Sven Marnach 26/07/19

0

Se você deseja descompactar um dicionário para transmitir argumentos format, como nesta pergunta relacionada , use o seguinte método.

Primeiro, assuma que a string sé a mesma que nesta pergunta:

s = '{foo} {bar}'

e os valores são fornecidos pelo seguinte dicionário:

replacements = {'foo': 'FOO'}

Claramente, isso não vai funcionar:

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

No entanto, você pode primeiro obter um setde todos os argumentos nomeadoss e criar um dicionário que mapeie o argumento para si mesmo envolto em chaves:

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

Agora use o argsdicionário para preencher as chaves ausentes replacements. Para python 3.5+, você pode fazer isso em uma única expressão :

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

Para versões mais antigas do python, você pode chamar update:

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

0

Eu gosto da resposta do @ sven-marnach. Minha resposta é simplesmente uma versão estendida. Permite formatação sem palavra-chave e ignora chaves extras. Aqui estão exemplos de uso (o nome de uma função é uma referência à formatação da string f do 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}'

E aqui está o meu código:

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 você está fazendo um monte de modelagem e descobrindo que a funcionalidade de modelagem de string incorporada do Python é insuficiente ou desajeitada, veja o Jinja2 .

Dos documentos:

O Jinja é uma linguagem de modelagem moderna e amigável para o Python, modelada com base nos modelos do Django.


0

Lendo o comentário de @Sam Bourne, modifiquei o código de @ SvenMarnach para funcionar corretamente com coerção (como {a!s:>2s}) sem escrever um analisador personalizado. A idéia básica não é converter em seqüências de caracteres, mas concatenar chaves ausentes com tags de coerção.

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)

Use (por exemplo) assim

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

Os testes a seguir (inspirados nos testes do @ norok2) verificam a saída do tradicional format_mape um safe_format_mapbaseados na classe acima em dois casos: fornecendo palavras-chave corretas ou sem elas.

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

Quais saídas

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

Você pode envolvê-lo em uma função que aceita argumentos padrão:

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

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

Você está substituindo {foo} por uma string vazia. A questão é sobre formatação parcial para formatação final adicional, sem ignorar os campos ausentes.
egvo 14/02
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.