formato de cadena parcial


128

¿Es posible hacer un formateo parcial de cadenas con los métodos avanzados de formateo de cadenas, similar a la plantilla de cadena safe_substitute() función de ?

Por ejemplo:

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

Respuestas:


58

Puede engañarlo en un formato parcial sobrescribiendo la asignación:

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

impresión

FOO {bar}

Por supuesto, esta implementación básica solo funciona correctamente para casos básicos.


77
Esto no funciona para formatos más avanzados como{bar:1.2f}
MaxNoe

Entiendo que "la implementación más básica solo funciona correctamente para los casos básicos", pero ¿hay alguna manera de expandir esto para simplemente no eliminar la especificación de formato?
Tadhg McDonald-Jensen

55
@ TadhgMcDonald-Jensen: Sí, hay una manera. En lugar de devolver una cadena __missing__(), devuelva una instancia de una clase personalizada que sobrescriba __format__()de manera que devuelva el marcador de posición original, incluida la especificación de formato. Prueba de concepto: ideone.com/xykV7R
Sven Marnach

@SvenMarnach ¿por qué su prueba de concepto no está en el cuerpo de su respuesta? Eso es un poco difícil de alcanzar. ¿Hay alguna advertencia conocida que te impida promocionarla?
norok2

1
@ norok2 Es una respuesta a una pregunta formulada en un comentario, así que puse la respuesta en un comentario. La pregunta original realmente no incluía ese requisito, y en general todavía pienso que es un poco extraño intentar formatear parcialmente una cadena.
Sven Marnach

128

Si sabe en qué orden está formateando las cosas:

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

Úselo así:

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

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

No puede especificar fooy, baral mismo tiempo, debe hacerlo secuencialmente.


¿Cuál es el punto de esto? Si especifico tanto foo como bar: s.format(foo='FOO',bar='BAR')entonces todavía tengo 'FOO {bar}', no importa qué. ¿Podrías aclararlo?
n611x007

10
Que no puedas completar ambos a la vez es molesto. Esto es útil cuando, por cualquier razón, tiene que formatear su cadena en etapas y conoce el orden de esas etapas.
aaren

1
Probablemente deberías diseñar tu manera de no tener que hacer esto, pero tal vez es que estás obligado a hacerlo.
aaren

2
No sabía sobre esto. He tenido varios casos de uso en los que quería "cebar" una cadena como una mini plantilla
ejrb

Esto es súper útil al completar parte de una cadena en una parte de su código, pero dejando un marcador de posición que se completará más adelante en otra parte de su código.
Alex Petralia

98

Podrías usar la partialfunción desde la functoolscual es corta, más legible y también describe la intención del codificador:

from functools import partial

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

2
No solo es la solución más corta y fácil de leer, sino que también describe la intención del codificador. Versión de 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 es cierto, la respuesta necesita algo de amor;)
ypercubeᵀᴹ

8
@ ypercubeᵀᴹ Bueno, no estoy seguro de que esto sea exactamente lo que la mayoría de la gente está buscando. partial()no me va a ayudar si necesito hacer un procesamiento con la cadena parcialmente formateada (es decir "FOO {bar}").
Delgan

1
Esto es mejor para el caso cuando está operando en una entrada que no controla al 100%. Imagínese: "{foo} {{bar}}".format(foo="{bar}").format(bar="123")de los otros ejemplos. Yo esperaría "{bar} 123"pero salen "123 123".
Benjamin Manns

50

Esta limitación de .format() la incapacidad de hacer sustituciones parciales me ha estado molestando.

Después de evaluar escribir una Formatterclase personalizada como se describe en muchas respuestas aquí e incluso considerar el uso de paquetes de terceros como lazy_format , descubrí una solución incorporada mucho más simple: cadenas de plantillas

Proporciona una funcionalidad similar pero también proporciona un safe_substitute()método completo de sustitución parcial . Las cadenas de plantilla deben tener un $prefijo (que se siente un poco extraño, pero la solución general creo que es mejor).

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ó una envoltura de conveniencia basada en esto:

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

Del mismo modo, un contenedor basado en la respuesta de Sven que utiliza el formato de cadena predeterminado:

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

No estoy seguro si esto está bien como una solución rápida, pero ¿qué tal

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

? :)


Hice totalmente lo mismo, ojalá supiera si hubiera advertencias al hacerlo.
ramgo


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

Probar esto.


Wow, exactamente lo que necesito! ¿Lo explicas?
Sergey Chizhik

1
{{y }}es una forma de escapar de las marcas de formato, por lo format()que no realiza sustitución y reemplaza {{y }}con {y }, respectivamente.
7yl4r

El problema de esta solución es que el doble {{ }}solo funciona para un formato, si necesita aplicar más, necesitará agregar más {}. ex. 'fd:{uid}:{{topic_id}}'.format(uid=123).format(a=1)devolverá un error ya que el segundo formato no proporciona el topic_idvalor.
Franzi

7

Gracias al comentario de Amber , se me ocurrió esto:

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 una función python> = 2.6.
n611x007

Definitivamente estoy usando esta solución :) ¡Gracias!
astrojuanlu

2
Tenga en cuenta que esto perderá la conversión y las especificaciones de formato si existen (y en realidad aplica la especificación de formato al valor devuelto. Es decir, se {field!s: >4}convierte en{field}
Brendan Abel el

3

Para mí esto fue lo suficientemente bueno:

>>> 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 las soluciones que he encontrado parecen tener problemas con especificaciones más avanzadas u opciones de conversión. @ SvenMarnach de FormatPlaceholder es maravillosamente inteligente, pero no funciona correctamente con la coacción (por ejemplo {a!s:>2s}), ya que llama al __str__método (en este ejemplo) en lugar de__format__ y se pierde cualquier formato adicional.

Esto es lo que terminé y algunas de sus características clave:

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}'
  • proporciona una interfaz similar a str.format(no solo un mapeo)
  • admite opciones de formato más complejas:
    • coerción {k!s} {!r}
    • anidamiento {k:>{size}}
    • getattr {k.foo}
    • obtiene el objeto {k[0]}
    • coerción + formateo {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)

Descubrí los problemas con las diversas implementaciones después de escribir algunas pruebas sobre cómo quería que se comportara este método. Están debajo si alguien los encuentra 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)

Agregué una respuesta que es similar al código @SvenMarnach pero que maneja la coerción correctamente para sus pruebas.
Tohiko

1

Mi sugerencia sería la siguiente (probado 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'

Actualización: aquí se muestra una forma aún más elegante (subclasificación dicty sobrecarga __missing__(self, key)): https://stackoverflow.com/a/17215533/333403


0

Suponiendo que no usará la cadena hasta que esté completamente llena, podría hacer algo como esta clase:

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)

Ejemplo:

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

Hay una forma más de lograr esto, es decir, usando formaty %reemplazando variables. Por ejemplo:

>>> 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 solución muy fea pero la más simple para mí es simplemente hacer:

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

De esta manera, todavía puede usar tmplcomo plantilla regular y realizar un formateo parcial solo cuando sea necesario. Este problema me parece demasiado trivial para usar una solución exagerada como la de Mohan Raj.


0

Después de probar las soluciones más prometedoras de aquí y de allá , me di cuenta de que ninguna de ellas realmente cumplía con los siguientes requisitos:

  1. adherirse estrictamente a la sintaxis reconocida por str.format_map()para la plantilla;
  2. ser capaz de retener un formato complejo, es decir, totalmente compatible con el formato Mini-Language

Entonces, escribí mi propia solución, que satisface los requisitos anteriores. ( EDITAR : ahora la versión de @SvenMarnach, como se informa en esta respuesta, parece manejar los casos de esquina que necesitaba).

Básicamente, terminé analizando la cadena de plantilla, encontrando {.*?}grupos anidados coincidentes (usando una find_all()función auxiliar) y construyendo la cadena formateada de manera progresiva y directa usando str.format_map()mientras detectaba cualquier 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 también está disponible en FlyingCircus - DESCARGO DE RESPONSABILIDAD: soy el autor principal de la misma).


El uso de este código sería:

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

Comparemos esto con mi solución favorita (por @SvenMarnach que amablemente compartió su código aquí y allá ):

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

Aquí hay un par de pruebas:

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

y el código para hacerlo 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 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}

Como puede ver, la versión actualizada ahora parece manejar bien los casos de esquina donde la versión anterior solía fallar.


En el tiempo, están dentro de aprox. 50% el uno del otro, dependiendo del textformato real (y probablemente el real source), pero safe_format_map()parece tener una ventaja en la mayoría de las pruebas que realicé (lo que sea que signifiquen, por supuesto):

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)

Tenga en cuenta que, {d[x]}por lo que sé, no es una cadena de formato válida.
Sven Marnach

@SvenMarnach Los documentos oficiales cuentan explícitamente field_name ::= arg_name ("." attribute_name | "[" element_index "]")*y ambos str.format()y lo str.format_map()entienden. Yo diría que hay suficiente evidencia de que esta es una cadena de formato válida.
norok2

¿Puedes dar un ejemplo de uso str.format()con un índice no entero entre corchetes? Solo puedo hacer que funcionen los índices enteros.
Sven Marnach

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

1
Ah, ya veo. Supuse que esto se interpreta como a[b]en el código Python, pero en realidad es a["b"]¡Gracias!
Sven Marnach

0

Si desea descomprimir un diccionario para pasar argumentos format, como en esta pregunta relacionada , puede usar el siguiente método.

Primero suponga que la cadena ses la misma que en esta pregunta:

s = '{foo} {bar}'

y los valores están dados por el siguiente diccionario:

replacements = {'foo': 'FOO'}

Claramente esto no funcionará:

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

Sin embargo, primero puede obtener uno setde todos los argumentos nombradoss y crear un diccionario que asigne el argumento a sí mismo envuelto en llaves:

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

Ahora use el argsdiccionario para completar las claves que faltan replacements. Para python 3.5+, puede hacer esto en una sola expresión :

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

Para versiones anteriores de python, puede llamar a update:

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

0

Me gusta la respuesta de @ sven-marnach. Mi respuesta es simplemente una versión extendida de la misma. Permite el formato sin palabras clave e ignora las claves adicionales. Aquí hay ejemplos de uso (el nombre de una función es una referencia al formato de cadena f de 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}'

Y aquí está mi 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

Si está haciendo muchas plantillas y encuentra que la funcionalidad de plantilla integrada de Python es insuficiente o torpe, mire Jinja2 .

De los documentos:

Jinja es un lenguaje de plantillas moderno y amigable para Python, diseñado a partir de las plantillas de Django.


0

Leyendo el comentario de @Sam Bourne, modifiqué el código de @SvenMarnach para que funcione correctamente con la coerción (me gusta {a!s:>2s}) sin escribir un analizador personalizado. La idea básica no es convertir a cadenas, sino concatenar las claves que faltan con etiquetas de coerción.

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 ejemplo) como este

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

Las siguientes pruebas (inspiradas en las pruebas de @ norok2) verifican el resultado para el tradicional format_mapy el safe_format_mapbasado en la clase anterior en dos casos: proporcionando palabras clave correctas o sin ellas.

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

Que salidas

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

Podría envolverlo en una función que tome argumentos predeterminados:

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

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

Estás reemplazando {foo} con una cadena vacía. La pregunta es sobre el formateo parcial para un formateo final adicional, sin ignorar los campos faltantes.
egvo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.