pemformatan string parsial

128

Apakah mungkin untuk melakukan pemformatan string parsial dengan metode pemformatan string lanjutan, mirip dengan safe_substitute()fungsi template string ?

Sebagai contoh:

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

Jawaban:

58

Anda dapat mengelabunya menjadi sebagian format dengan menimpa pemetaan:

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

pencetakan

FOO {bar}

Tentu saja implementasi dasar ini hanya berfungsi dengan benar untuk kasus dasar.

Sven Marnach
sumber
7
Ini tidak berfungsi untuk pemformatan yang lebih canggih seperti{bar:1.2f}
MaxNoe
Saya mengerti mengatakan bahwa "implementasi paling dasar hanya bekerja dengan benar untuk kasus dasar" tetapi apakah ada cara untuk memperluas ini bahkan tidak menghapus format format?
Tadhg McDonald-Jensen
5
@ TadhgMcDonald-Jensen: Ya, ada jalan. Alih-alih mengembalikan string __missing__(), kembalikan instance kelas kustom yang menimpa __format__()dengan cara mengembalikan placeholder asli termasuk spesifikasi format. Bukti konsep: ideone.com/xykV7R
Sven Marnach
@ SvenMarnach mengapa bukti konsep Anda tidak ada di tubuh jawaban Anda? Itu agak sulit dipahami. Adakah peringatan yang dikenal yang mencegah Anda mempromosikannya?
norok2
1
@ norok2 Ini adalah jawaban untuk pertanyaan yang diajukan dalam komentar, jadi saya memberikan balasan dalam komentar. Pertanyaan aslinya tidak benar-benar memasukkan persyaratan itu, dan saya umumnya masih berpikir bahwa itu agak aneh untuk mencoba memformat sebagian string.
Sven Marnach
128

Jika Anda tahu dalam urutan apa Anda memformat sesuatu:

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

Gunakan seperti ini:

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

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

Anda tidak dapat menentukan foodan barpada saat yang sama - Anda harus melakukannya secara berurutan.

aaren
sumber
Apa gunanya ini? Jika saya menentukan foo dan bar: s.format(foo='FOO',bar='BAR')maka saya masih dapat 'FOO {bar}', apa pun yang terjadi. Bisakah Anda menjelaskannya?
n611x007
10
Bahwa Anda tidak dapat mengisi keduanya sekaligus itu menjengkelkan. Ini berguna ketika, untuk alasan apa pun, Anda harus memformat string Anda secara bertahap dan Anda tahu urutan tahapan tersebut.
aaren
1
Anda mungkin harus merancang jalan keluar karena harus melakukan ini, tetapi mungkin Anda terpaksa melakukannya.
aaren
2
Tidak tahu tentang ini. Saya sudah memiliki beberapa kasus penggunaan di mana saya ingin "prima" string sebagai template mini
ejrb
Ini sangat berguna ketika mengisi bagian dari string di satu bagian dari kode Anda, tetapi meninggalkan placeholder untuk diisi kemudian di bagian lain dari kode Anda.
Alex Petralia
98

Anda bisa menggunakan partialfungsi functoolsyang pendek, paling mudah dibaca dan juga menjelaskan maksud pembuat kode:

from functools import partial

s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR
Saikiran Yerram
sumber
2
Tidak hanya solusi terpendek dan paling mudah dibaca, tetapi juga menggambarkan niat pembuat kode. Versi 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 benar, jawabannya membutuhkan beberapa cinta;)
ypercubeᵀᴹ
8
@ ypercubeᵀᴹ Ya, saya tidak yakin inilah yang dicari oleh kebanyakan orang. partial()tidak akan membantu saya jika saya perlu melakukan beberapa pemrosesan dengan string yang sebagian diformat (yaitu "FOO {bar}").
Delgan
1
Ini lebih baik untuk kasus ketika Anda beroperasi pada input yang tidak Anda kontrol 100%. Bayangkan: "{foo} {{bar}}".format(foo="{bar}").format(bar="123")dari contoh lainnya. Saya harapkan "{bar} 123"tetapi mereka output "123 123".
Benjamin Manns
50

Keterbatasan ini .format()- ketidakmampuan untuk melakukan pergantian sebagian - telah mengganggu saya.

Setelah mengevaluasi penulisan Formatterkelas khusus seperti yang dijelaskan dalam banyak jawaban di sini dan bahkan mempertimbangkan menggunakan paket pihak ketiga seperti lazy_format , saya menemukan solusi inbuilt yang lebih sederhana: Templat string

Ini memberikan fungsionalitas yang serupa tetapi juga menyediakan safe_substitute()metode menyeluruh penggantian sebagian . String template harus memiliki $awalan (yang terasa agak aneh - tetapi solusi keseluruhan saya pikir lebih baik).

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'

Membentuk pembungkus yang nyaman berdasarkan ini:

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

Demikian pula pembungkus berdasarkan jawaban Sven yang menggunakan pemformatan string default:

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

Tidak yakin apakah ini ok sebagai solusi cepat, tapi bagaimana

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

? :)

Memphis
sumber
Saya benar-benar melakukan hal yang sama, berharap saya tahu jika ada peringatan dalam melakukannya.
ramgo
11

Jika Anda menentukan sendiri Formatteryang menimpa get_valuemetode, Anda bisa menggunakannya untuk memetakan nama bidang yang tidak ditentukan untuk apa pun yang Anda inginkan:

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

Misalnya, Anda bisa memetakan baruntuk "{bar}"jika bartidak di kwargs.

Namun, itu mengharuskan menggunakan format()metode objek Formatter Anda, bukan metode string format().

Amber
sumber
Tampak seperti fitur python> = 2.6.
n611x007
11
>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'

Coba ini.

Pengfei.X
sumber
Wow, persis apa yang saya butuhkan! Apakah Anda akan menjelaskannya?
Sergey Chizhik
1
{{dan }}merupakan cara keluar dari tanda pemformatan, sehingga format()tidak melakukan subtitusi dan menggantikan {{dan }}dengan {dan }, masing-masing.
7yl4r
Masalah dari solusi ini adalah bahwa ganda {{ }}hanya berfungsi untuk satu format, jika Anda perlu menerapkan lebih banyak, Anda perlu menambahkan lebih banyak {}. ex. 'fd:{uid}:{{topic_id}}'.format(uid=123).format(a=1)akan mengembalikan kesalahan karena format kedua tidak memberikan topic_idnilai.
Franzi
7

Berkat komentar Amber , saya datang dengan ini:

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
sumber
Tampak seperti fitur python> = 2.6.
n611x007
Saya pasti menggunakan solusi ini :) Terima kasih!
astrojuanlu
2
Ketahuilah bahwa ini akan kehilangan konversi dan spesifikasi format jika ada (dan itu benar-benar menerapkan spesifikasi format ke nilai yang dikembalikan. Yaitu ( {field!s: >4}menjadi{field}
Brendan Abel
3

Bagi saya ini cukup baik:

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

Semua solusi yang saya temukan tampaknya memiliki masalah dengan spesifikasi atau opsi konversi yang lebih canggih. @ SvenMarnach's FormatPlaceholder sangat pintar tetapi tidak bekerja dengan baik dengan paksaan (misalnya {a!s:>2s}) karena ia memanggil __str__metode (dalam contoh ini) alih-alih __format__dan Anda kehilangan pemformatan tambahan.

Inilah yang akhirnya saya dapatkan dan beberapa fitur utamanya:

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}'
  • menyediakan antarmuka yang sama seperti str.format(bukan hanya pemetaan)
  • mendukung opsi pemformatan yang lebih kompleks:
    • paksaan {k!s} {!r}
    • bersarang {k:>{size}}
    • getattr {k.foo}
    • getitem {k[0]}
    • paksaan + pemformatan {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)

Saya menemukan masalah dengan berbagai implementasi setelah menulis beberapa tes tentang bagaimana saya ingin metode ini berperilaku. Mereka ada di bawah jika ada yang menganggapnya berwawasan luas.

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
sumber
Saya menambahkan jawaban yang mirip dengan kode @SvenMarnach tetapi yang menangani paksaan dengan benar untuk tes Anda.
Tohiko
1

Saran saya adalah sebagai berikut (diuji dengan 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'

Pembaruan: Cara yang lebih elegan (subclassing dictdan overloading __missing__(self, key)) ditunjukkan di sini: https://stackoverflow.com/a/17215533/333403

cknoll
sumber
0

Dengan asumsi Anda tidak akan menggunakan string sampai benar-benar diisi, Anda bisa melakukan sesuatu seperti kelas ini:

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)

Contoh:

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

Ada satu cara lagi untuk mencapai ini yaitu dengan menggunakan formatdan %mengganti variabel. Sebagai contoh:

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

Solusi yang sangat jelek tapi paling sederhana bagi saya adalah dengan hanya melakukan:

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

Dengan cara ini Anda masih dapat menggunakan tmplsebagai templat biasa dan melakukan pemformatan parsial hanya jika diperlukan. Saya menemukan masalah ini terlalu sepele untuk menggunakan solusi berlebihan seperti Mohan Raj.

michcio1234
sumber
0

Setelah menguji solusi yang paling menjanjikan dari sana - sini , saya menyadari bahwa tidak ada satupun yang benar-benar memenuhi persyaratan berikut:

  1. secara ketat mematuhi sintaks yang dikenali oleh str.format_map()untuk templat;
  2. mampu mempertahankan pemformatan yang kompleks, yaitu sepenuhnya mendukung Format Mini-Bahasa

Jadi, saya menulis solusi sendiri, yang memenuhi persyaratan di atas. ( EDIT : sekarang versi oleh @SvenMarnach - seperti yang dilaporkan dalam jawaban ini - tampaknya menangani kasus sudut yang saya butuhkan).

Pada dasarnya, saya akhirnya mem-parsing string templat, menemukan {.*?}grup bersarang yang cocok (menggunakan find_all()fungsi helper) dan membangun string yang diformat secara progresif dan langsung digunakan str.format_map()sambil menangkap potensi apa pun 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

(Kode ini juga tersedia di FlyingCircus - DISCLAIMER: Saya adalah penulis utamanya.)


Penggunaan kode ini adalah:

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

Mari kita bandingkan ini dengan solusi favorit saya (oleh @SvenMarnach yang dengan ramah membagikan kodenya di sana - sini ):

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

Berikut ini beberapa tes:

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

dan kode untuk membuatnya berjalan:

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

yang menghasilkan:

    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}

seperti yang Anda lihat, versi yang diperbarui sekarang tampaknya menangani dengan baik kasus sudut di mana versi sebelumnya gagal.


Saat itu, mereka berada dalam sekitar. 50% satu sama lain, tergantung pada textformat aktual (dan kemungkinan aktual source), tetapi safe_format_map()tampaknya memiliki keunggulan dalam sebagian besar tes yang saya lakukan (apa pun artinya, tentu saja):

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
sumber
Perhatikan bahwa {d[x]}bukan string format yang valid sejauh yang saya ketahui.
Sven Marnach
@SvenMarnach Dokumen resmi secara eksplisit memberi tahu field_name ::= arg_name ("." attribute_name | "[" element_index "]")*dan keduanya str.format()serta str.format_map()memahaminya. Saya akan mengatakan ada cukup bukti untuk ini menjadi format string yang valid.
norok2
Bisakah Anda memberikan contoh penggunaan str.format()dengan indeks non-integer dalam tanda kurung? Saya hanya bisa membuat indeks integer berfungsi.
Sven Marnach
@VenMarnach a = dict(b='YAY!'); '{a[b]}'.format_map(dict(a=a))membuat Anda `'YAY!'
norok2
1
Ah, begitu. Saya berasumsi ini ditafsirkan seperti a[b]dalam kode Python, tetapi sebenarnya a["b"]Terima kasih!
Sven Marnach
0

Jika Anda ingin membongkar kamus untuk meneruskan argumen format, seperti dalam pertanyaan terkait ini , Anda dapat menggunakan metode berikut.

Pertama-tama anggap string situ sama seperti dalam pertanyaan ini:

s = '{foo} {bar}'

dan nilai-nilai diberikan oleh kamus berikut:

replacements = {'foo': 'FOO'}

Jelas ini tidak akan berhasil:

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

Namun, pertama-tama Anda bisa mendapatkan setdari semua argumen bernamas dan membuat kamus yang memetakan argumen itu sendiri dibungkus dengan kurung kurawal:

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

Sekarang gunakan argskamus untuk mengisi kunci yang hilang replacements. Untuk python 3.5+, Anda bisa melakukan ini dalam satu ekspresi :

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

Untuk versi python yang lebih lama, Anda bisa menelepon update:

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

Saya suka jawaban @ sven-marnach. Jawaban saya hanyalah versi panjangnya. Ini memungkinkan pemformatan non-kata kunci dan mengabaikan kunci tambahan. Berikut adalah contoh penggunaan (nama fungsi adalah referensi ke format python 3.6 f-string):

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

Dan ini kode saya:

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)
ego
sumber
0

Jika Anda melakukan banyak template dan menemukan fungsionalitas templating string bawaan Python tidak mencukupi atau kikuk, lihat Jinja2 .

Dari dokumen:

Jinja adalah bahasa templating modern dan ramah desainer untuk Python, meniru model templat Django.

Vito
sumber
0

Membaca komentar @Sam Bourne, saya memodifikasi kode @ SvenMarnach agar berfungsi dengan baik dengan paksaan (seperti {a!s:>2s}) tanpa menulis parser khusus. Ide dasarnya bukan untuk mengkonversi ke string tetapi menggabungkan kunci yang hilang dengan tag paksaan.

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)

Gunakan (misalnya) seperti ini

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

Tes berikut (terinspirasi oleh tes dari @ norok2) memeriksa output untuk tradisional format_mapdan safe_format_mapberdasarkan pada kelas di atas dalam dua kasus: memberikan kata kunci yang benar atau tanpa mereka.

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

Output yang mana

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

Anda bisa membungkusnya dalam fungsi yang mengambil argumen default:

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

print_foo_bar(bar='BAR') # ' BAR'
Trevor
sumber
Anda mengganti {foo} dengan string kosong. Pertanyaannya adalah tentang pemformatan sebagian untuk pemformatan akhir lebih lanjut, bukan mengabaikan bidang yang hilang.
egvo