teilweise Zeichenfolgenformatierung

128

Ist es möglich, eine teilweise Zeichenfolgenformatierung mit den erweiterten Zeichenfolgenformatierungsmethoden durchzuführen, ähnlich wie bei der Zeichenfolgenvorlagenfunktion safe_substitute()?

Beispielsweise:

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

Antworten:

58

Sie können es zur Teilformatierung verleiten, indem Sie das Mapping überschreiben:

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

Drucken

FOO {bar}

Natürlich funktioniert diese grundlegende Implementierung nur in grundlegenden Fällen korrekt.

Sven Marnach
quelle
7
Dies funktioniert nicht für fortgeschrittenere Formatierungen wie{bar:1.2f}
MaxNoe
Ich verstehe, dass "die grundlegendste Implementierung nur für die grundlegenden Fälle korrekt funktioniert", aber gibt es eine Möglichkeit, dies zu erweitern, um die Formatspezifikation auch nicht zu löschen?
Tadhg McDonald-Jensen
5
@ TadhgMcDonald-Jensen: Ja, es gibt einen Weg. Anstatt eine Zeichenfolge zurückzugeben __missing__(), geben Sie eine Instanz einer benutzerdefinierten Klasse zurück, die so überschrieben __format__()wird, dass der ursprüngliche Platzhalter einschließlich der Formatspezifikation zurückgegeben wird. Proof of Concept: ideone.com/xykV7R
Sven Marnach
@SvenMarnach Warum ist Ihr Proof of Concept nicht im Hauptteil Ihrer Antwort enthalten? Das ist ein bisschen schwer fassbar. Gibt es bekannte Vorbehalte, die Sie daran hindern, diese zu fördern?
Norok2
1
@ norok2 Es ist eine Antwort auf eine Frage, die in einem Kommentar gestellt wird, also habe ich die Antwort in einen Kommentar eingefügt. Die ursprüngliche Frage enthielt diese Anforderung nicht wirklich, und ich denke im Allgemeinen immer noch, dass es etwas seltsam ist, zu versuchen, eine Zeichenfolge teilweise zu formatieren.
Sven Marnach
128

Wenn Sie wissen, in welcher Reihenfolge Sie die Dinge formatieren:

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

Verwenden Sie es so:

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

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

Sie können nicht angeben foound bargleichzeitig - Sie müssen es nacheinander tun.

aaren
quelle
Was ist der Sinn davon? Wenn ich sowohl foo als auch bar spezifiziere: s.format(foo='FOO',bar='BAR')dann habe ich immer noch 'FOO {bar}', egal was passiert. Könnten Sie es klarstellen?
n611x007
10
Dass Sie nicht beide gleichzeitig ausfüllen können, ist ärgerlich. Dies ist nützlich, wenn Sie aus irgendeinem Grund Ihre Zeichenfolge schrittweise formatieren müssen und die Reihenfolge dieser Stufen kennen.
Aaren
1
Sie sollten Ihren Weg wahrscheinlich so gestalten, dass Sie dies nicht tun müssen, aber vielleicht sind Sie dazu gezwungen.
Aaren
2
Wusste nichts davon. Ich hatte mehrere Anwendungsfälle, in denen ich eine Zeichenfolge als Mini-Vorlage "
grundieren
Dies ist sehr nützlich, wenn Sie einen Teil einer Zeichenfolge in einen Teil Ihres Codes einfügen, einen Platzhalter jedoch später in einem anderen Teil Ihres Codes ausfüllen möchten.
Alex Petralia
98

Sie können die partialFunktion verwenden, functoolsdie kurz und am besten lesbar ist und auch die Absicht des Codierers beschreibt:

from functools import partial

s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR
Saikiran Yerram
quelle
2
Nicht nur die kürzeste und am besten lesbare Lösung, sondern auch die Absicht des Codierers. Python3-Version: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 wahr, die Antwort braucht etwas Liebe;)
ypercubeᵀᴹ
8
@ ypercubeᵀᴹ Nun, ich bin mir nicht sicher, ob dies genau das ist, wonach die meisten Leute suchen. partial()wird mir nicht helfen, wenn ich etwas mit der teilweise formatierten Zeichenfolge (das heißt "FOO {bar}") verarbeiten muss.
Delgan
1
Dies ist besser für den Fall, dass Sie mit Eingaben arbeiten, die Sie nicht zu 100% kontrollieren. Stellen Sie sich vor: "{foo} {{bar}}".format(foo="{bar}").format(bar="123")aus den anderen Beispielen. Ich würde erwarten, "{bar} 123"aber sie geben aus "123 123".
Benjamin Manns
50

Diese Einschränkung von .format() - die Unfähigkeit, teilweise Substitutionen vorzunehmen - hat mich nervt.

Nachdem ich das Schreiben einer benutzerdefinierten FormatterKlasse wie in vielen Antworten beschrieben beschrieben und sogar die Verwendung von Paketen von Drittanbietern wie lazy_format in Betracht gezogen hatte , entdeckte ich eine viel einfachere integrierte Lösung: Vorlagenzeichenfolgen

Es bietet ähnliche Funktionen, bietet aber auch eine gründliche safe_substitute()Methode zur teilweisen Substitution . Die Vorlagenzeichenfolgen müssen ein $Präfix haben (was sich etwas seltsam anfühlt - aber die Gesamtlösung ist meiner Meinung nach besser).

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'

Auf dieser Grundlage wurde ein Convenience-Wrapper gebildet:

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

Ebenso ein Wrapper basierend auf Svens Antwort, der die Standard-String-Formatierung verwendet:

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)
Mohan Raj
quelle
29

Ich bin mir nicht sicher, ob dies als schnelle Problemumgehung in Ordnung ist, aber wie wäre es damit?

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

? :) :)

Memphis
quelle
Ich habe das Gleiche getan und wünschte, ich wüsste, ob es dabei Vorbehalte gibt.
Ramgo
11

Wenn Sie eine eigene definieren, Formatterdie die get_valueMethode überschreibt , können Sie damit undefinierte Feldnamen beliebig zuordnen:

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

Zum Beispiel könnten Sie zuordnen bar, "{bar}"wenn barnicht in den kwargs.

Dies erfordert jedoch die Verwendung der format()Methode Ihres Formatter-Objekts und nicht der format()Methode der Zeichenfolge .

Bernstein
quelle
Scheint wie eine Python> = 2.6-Funktion.
n611x007
11
>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'

Probieren Sie es aus.

Pengfei.X
quelle
Wow, genau das was ich brauche! Würden Sie es erklären?
Sergey Chizhik
1
{{und }}ist ein Weg, um den Formatierungsmarkierungen zu entkommen, führt also format()keine Ersetzung durch und ersetzt {{bzw. }}durch {und }.
7yl4r
Das Problem dieser Lösung ist, dass das Double {{ }}nur für ein Format funktioniert. Wenn Sie mehr anwenden müssen, müssen Sie mehr hinzufügen {}. Ex. 'fd:{uid}:{{topic_id}}'.format(uid=123).format(a=1)gibt einen Fehler zurück, da das zweite Format den topic_idWert nicht bereitstellt .
Franzi
7

Dank Bernsteins Kommentar habe ich mir Folgendes ausgedacht:

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
gatto
quelle
Scheint wie eine Python> = 2.6-Funktion.
n611x007
Ich benutze definitiv diese Lösung :) Danke!
Astrojuanlu
2
Beachten Sie, dass dadurch die Konvertierungs- und Formatspezifikation verloren geht, wenn sie vorhanden ist (und die Formatspezifikation tatsächlich auf den zurückgegebenen Wert angewendet {field!s: >4}wird, dh ( wird{field}
Brendan Abel
3

Für mich war das gut genug:

>>> 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'
David Veza
quelle
3

Alle Lösungen, die ich gefunden habe, schienen Probleme mit erweiterten Spezifikationen oder Konvertierungsoptionen zu haben. Der FormatPlaceholder von @ SvenMarnach ist wunderbar clever, funktioniert aber mit Zwang (z. B. {a!s:>2s}) nicht richtig, da er die __str__Methode (in diesem Beispiel) anstelle von aufruft __format__und Sie keine zusätzliche Formatierung verlieren.

Hier ist, was ich am Ende hatte und einige seiner Hauptmerkmale:

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}'
  • bietet eine ähnliche Schnittstelle wie str.format(nicht nur ein Mapping)
  • unterstützt komplexere Formatierungsoptionen:
    • Zwang {k!s} {!r}
    • Verschachtelung {k:>{size}}
    • getattr {k.foo}
    • getitem {k[0]}
    • Zwang + Formatierung {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)

Ich entdeckte die Probleme mit den verschiedenen Implementierungen, nachdem ich einige Tests geschrieben hatte, wie sich diese Methode verhalten sollte. Sie sind unten, wenn jemand sie aufschlussreich findet.

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)
Sam Bourne
quelle
Ich habe eine Antwort hinzugefügt, die dem @ SavMarnach-Code ähnelt, die jedoch den Zwang für Ihre Tests korrekt behandelt.
Tohiko
1

Mein Vorschlag wäre der folgende (getestet mit 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'

Update: Eine noch elegantere Methode (Unterklassen dictund Überladen __missing__(self, key)) wird hier gezeigt: https://stackoverflow.com/a/17215533/333403

cknoll
quelle
0

Angenommen, Sie verwenden die Zeichenfolge erst, wenn sie vollständig ausgefüllt ist, können Sie Folgendes tun:

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)

Beispiel:

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'
Brett Beatty
quelle
0

Es gibt noch eine weitere Möglichkeit, dies zu erreichen, indem Variablen verwendet formatund %ersetzt werden. Beispielsweise:

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

Eine sehr hässliche, aber einfachste Lösung für mich ist, einfach zu tun:

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

Auf diese Weise können Sie weiterhin tmplals reguläre Vorlage verwenden und nur bei Bedarf eine Teilformatierung durchführen. Ich finde dieses Problem zu trivial, um eine Overkilling-Lösung wie die von Mohan Raj zu verwenden.

michcio1234
quelle
0

Nachdem ich hier und da die vielversprechendsten Lösungen getestet hatte , stellte ich fest, dass keine von ihnen wirklich die folgenden Anforderungen erfüllte:

  1. Halten Sie sich strikt an die Syntax, die von str.format_map()der Vorlage erkannt wird .
  2. in der Lage zu sein, komplexe Formatierungen beizubehalten, dh die Format Mini-Sprache vollständig zu unterstützen

Also habe ich meine eigene Lösung geschrieben, die die oben genannten Anforderungen erfüllt. ( BEARBEITEN : Jetzt scheint die Version von @SvenMarnach - wie in dieser Antwort angegeben - die Eckfälle zu behandeln, die ich brauchte).

Grundsätzlich habe ich die Vorlagenzeichenfolge analysiert, passende verschachtelte {.*?}Gruppen gefunden (mithilfe einer find_all()Hilfsfunktion) und die formatierte Zeichenfolge schrittweise und direkt mithilfe erstellt, str.format_map()während ich potenzielle Potenziale abfing 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

(Dieser Code ist auch in FlyingCircus verfügbar - HAFTUNGSAUSSCHLUSS: Ich bin der Hauptautor davon.)


Die Verwendung für diesen Code wäre:

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

Vergleichen wir dies mit meiner Lieblingslösung (von @SvenMarnach, der freundlicherweise hier und da seinen Code geteilt hat ):

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

Hier sind einige Tests:

test_texts = (
    '{b} {f}',  # simple nothing useful in source
    '{a} {b}',  # simple
    '{a} {b} {c:5d}',  # formatting
    '{a} {b} {c!s}',  # coercion
    '{a} {b} {c!s:>{a}s}',  # formatting and coercion
    '{a} {b} {c:0{a}d}',  # nesting
    '{a} {b} {d[x]}',  # dicts (existing in source)
    '{a} {b} {e.index}',  # class (existing in source)
    '{a} {b} {f[g]}',  # dict (not existing in source)
    '{a} {b} {f.values}',  # class (not existing in source)

)
source = dict(a=4, c=101, d=dict(x='FOO'), e=[])

und den Code, um es zum Laufen zu bringen:

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

ergebend:

    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}

Wie Sie sehen können, scheint die aktualisierte Version jetzt die Eckfälle gut zu handhaben, in denen die frühere Version früher fehlgeschlagen ist.


Zeitlich sind sie innerhalb von rd. 50% voneinander, abhängig vom tatsächlichen textFormat (und wahrscheinlich vom tatsächlichen Format source), safe_format_map()scheint aber bei den meisten von mir durchgeführten Tests einen Vorteil zu haben (was auch immer sie bedeuten):

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)
norok2
quelle
Beachten Sie, dass {d[x]}dies meines Wissens keine gültige Formatzeichenfolge ist.
Sven Marnach
@SvenMarnach Die offiziellen Dokumente erzählen field_name ::= arg_name ("." attribute_name | "[" element_index "]")*und beides explizit str.format()und str.format_map()verstehen es. Ich würde sagen, es gibt genügend Beweise dafür, dass dies eine gültige Formatzeichenfolge ist.
Norok2
Können Sie ein Beispiel für die Verwendung str.format()mit einem nicht ganzzahligen Index in eckigen Klammern geben? Ich kann nur ganzzahlige Indizes zum Laufen bringen.
Sven Marnach
@SvenMarnach a = dict(b='YAY!'); '{a[b]}'.format_map(dict(a=a))bringt dir "YAY!"
Norok2
1
Ah ich sehe. Ich habe angenommen, dass dies wie a[b]in Python-Code interpretiert wird , aber es ist tatsächlich a["b"]Danke!
Sven Marnach
0

Wenn Sie ein Wörterbuch entpacken möchten, an das Argumente übergeben werden sollen format, wie in dieser verwandten Frage , können Sie die folgende Methode verwenden.

Nehmen Sie zunächst an, dass die Zeichenfolge sdieselbe ist wie in dieser Frage:

s = '{foo} {bar}'

und die Werte werden durch das folgende Wörterbuch angegeben:

replacements = {'foo': 'FOO'}

Das wird natürlich nicht funktionieren:

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

Sie können jedoch zuerst eines setder genannten Argumente abrufens und ein Wörterbuch erstellen, das das Argument in geschweiften Klammern auf sich selbst abbildet:

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

Verwenden Sie nun das argsWörterbuch, um die fehlenden Schlüssel einzugeben replacements. Für Python 3.5+ können Sie dies in einem einzigen Ausdruck tun :

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

Für ältere Versionen von Python können Sie Folgendes aufrufen update:

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

Ich mag @ sven-marnach Antwort. Meine Antwort ist einfach eine erweiterte Version davon. Es ermöglicht die Formatierung ohne Schlüsselwörter und ignoriert zusätzliche Schlüssel. Hier einige Anwendungsbeispiele (der Name einer Funktion verweist auf die Formatierung von Python 3.6-F-Strings):

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

Und hier ist mein Code:

from string import Formatter


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


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


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

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

Wenn Sie viel Templating betreiben und feststellen, dass Pythons integrierte String-Template-Funktionalität unzureichend oder klobig ist, schauen Sie sich Jinja2 an .

Aus den Dokumenten:

Jinja ist eine moderne und designerfreundliche Vorlagensprache für Python, die Djangos Vorlagen nachempfunden ist.

Vito
quelle
0

Beim Lesen des Kommentars von @Sam Bourne habe ich den Code von @ SvenMarnach so geändert , dass er mit Zwang (wie {a!s:>2s}) ordnungsgemäß funktioniert, ohne einen benutzerdefinierten Parser zu schreiben. Die Grundidee besteht nicht darin, in Zeichenfolgen zu konvertieren, sondern fehlende Schlüssel mit Zwangs-Tags zu verketten.

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)

Verwenden Sie (zum Beispiel) so

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

Die folgenden Tests (inspiriert von Tests von @ norok2) überprüfen die Ausgabe für die traditionellen format_mapund a safe_format_mapbasierend auf der obigen Klasse in zwei Fällen: Bereitstellung korrekter Schlüsselwörter oder ohne diese.

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

Welche Ausgänge

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>
Tohiko
quelle
-2

Sie können es in eine Funktion einschließen, die Standardargumente akzeptiert:

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

print_foo_bar(bar='BAR') # ' BAR'
Trevor
quelle
Sie ersetzen {foo} durch eine leere Zeichenfolge. Die Frage betrifft die teilweise Formatierung für die weitere endgültige Formatierung, wobei fehlende Felder nicht ignoriert werden.
Egvo