部分字符串格式

P3t*_*rus 109 python string-formatting

是否可以使用高级字符串格式化方法进行部分字符串格式化,类似于字符串模板safe_substitute()函数?

例如:

s = '{foo} {bar}'
s.format(foo='FOO') #Problem: raises KeyError 'bar'
Run Code Online (Sandbox Code Playgroud)

aar*_*ren 111

如果你知道你格式化的是什么顺序:

s = '{foo} {{bar}}'
Run Code Online (Sandbox Code Playgroud)

像这样使用它:

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

print ss.format(bar='BAR')
>>> 'FOO BAR'
Run Code Online (Sandbox Code Playgroud)

您不能指定foo,并bar在同一时间-你必须按顺序做.

  • 你不能同时填写两个是令人讨厌的.无论出于何种原因,您必须分阶段格式化字符串并且知道这些阶段的顺序,这非常有用. (7认同)
  • 不知道这个.我有几个用例我希望将一个字符串"填充"为一个迷你模板 (2认同)

Sai*_*ram 62

您可以使用简短,最具可读性的partial函数functools,并且最好地描述编码器的意图:

from functools import partial

s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR
Run Code Online (Sandbox Code Playgroud)

  • @ypercubeᵀᴹ嗯,我不确定这正是大多数人想要的.如果我需要使用部分格式化的字符串(即"FOO {bar}"`)进行一些处理,`partial()`不会帮助我. (8认同)
  • 不仅是最短且最易读的解决方案,而且还描​​述了编码人员的意图.Python3版本:```python来自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``` (2认同)
  • 当您对无法 100% 控制的输入进行操作时,这会更好。想象一下:其他示例中的 `"{foo} {{bar}}".format(foo="{bar}").format(bar="123")` 。我期望“{bar} 123”,但他们输出“123 123”。 (2认同)

Sve*_*ach 48

您可以通过覆盖映射将其欺骗为部分格式化:

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))
Run Code Online (Sandbox Code Playgroud)

印花

FOO {bar}
Run Code Online (Sandbox Code Playgroud)

当然,这种基本实现仅适用于基本情况.

  • 这不适用于更高级的格式,如`{bar:1.2f}` (7认同)
  • @ TadhgMcDonald-Jensen:是的,有办法.而不是在`__missing __()`中返回一个字符串,而是返回一个自定义类的实例,覆盖`__format __()`以返回原始占位符,包括格式规范.概念证明:http://ideone.com/xykV7R (5认同)
  • @norok2这是对评论中提出的问题的回答,所以我将回复放在评论中。最初的问题并没有真正包含该要求,而且我通常仍然认为尝试部分格式化字符串有点奇怪。 (2认同)

Moh*_*Raj 47

这种限制.format()- 无法进行部分替换 - 一直困扰着我.

在评估了Formatter这里的许多答案中描述的自定义类,甚至考虑使用lazy_format之类的第三方软件包之后,我发现了一个更简单的内置解决方案:模板字符串

它提供类似的功能,但也提供部分替换彻底的safe_substitute()方法.模板字符串需要有一个$前缀(感觉有点奇怪 - 但我认为整体解决方案更好).

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'
Run Code Online (Sandbox Code Playgroud)

在此基础上形成了一个方便的包装:

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
Run Code Online (Sandbox Code Playgroud)

类似地,基于Sven的答案的包装器使用默认的字符串格式:

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)
Run Code Online (Sandbox Code Playgroud)


小智 28

不确定这是否可以作为快速解决方法,但如何

s = '{foo} {bar}'
s.format(foo='FOO', bar='{bar}')
Run Code Online (Sandbox Code Playgroud)

?:)

  • 是的,这有效,但简直令人讨厌,特别是如果你有几把钥匙...... (3认同)

Amb*_*ber 11

如果您定义了自己Formatterget_value方法,则可以使用它来将未定义的字段名称映射到您想要的任何内容:

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

例如,你可以映射bar"{bar}"if bar不在kwargs中.

但是,这需要使用format()Formatter对象的format()方法,而不是字符串的方法.


小智 11

>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'
Run Code Online (Sandbox Code Playgroud)

试试吧.

  • `{{` 和 `}}` 是转义格式标记的一种方法,因此 `format()` 不执行替换,而是分别用 `{` 和 `}` 替换 `{{` 和 `}}`。 (2认同)

gat*_*tto 7

感谢Amber的评论,我想出了这个:

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
Run Code Online (Sandbox Code Playgroud)

  • 请注意,如果转换和格式规范存在,它将丢失(并且实际上将格式规范应用于返回的值。即,(`{field!s:> 4}`变为{field} (2认同)

Sam*_*rne 6

我发现的所有解决方案似乎都存在更高级的规格或转换选项的问题。@SvenMarnach 的FormatPlaceholder非常聪明,但它不能与强制(例如{a!s:>2s})正常工作,因为它调用__str__方法(在本例中)而不是并且__format__您会丢失任何其他格式。

这是我最终得到的结果以及它的一些关键功能:

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}'
Run Code Online (Sandbox Code Playgroud)
  • 提供类似的接口str.format(不仅仅是映射)
  • 支持更复杂的格式选项:
    • 强迫{k!s} {!r}
    • 筑巢{k:>{size}}
    • 获取属性{k.foo}
    • 获取项目{k[0]}
    • 强制+格式化{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)
Run Code Online (Sandbox Code Playgroud)

在编写了一些关于我希望此方法如何表现的测试后,我发现了各种实现的问题。如果有人发现他们有洞察力,他们就在下面。

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)
Run Code Online (Sandbox Code Playgroud)