partial string formatting

前端 未结 21 1018
野的像风
野的像风 2020-11-28 04:30

Is it possible to do partial string formatting with the advanced string formatting methods, similar to the string template safe_substitute() function?

F

相关标签:
21条回答
  • 2020-11-28 04:56

    You could wrap it in a function that takes default arguments:

    def print_foo_bar(foo='', bar=''):
        s = '{foo} {bar}'
        return s.format(foo=foo, bar=bar)
    
    print_foo_bar(bar='BAR') # ' BAR'
    
    0 讨论(0)
  • 2020-11-28 04:57

    A very ugly but the simplest solution for me is to just do:

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

    This way you still can use tmpl as regular template and perform partial formatting only when needed. I find this problem too trivial to use a overkilling solution like Mohan Raj's.

    0 讨论(0)
  • 2020-11-28 04:59

    This limitation of .format() - the inability to do partial substitutions - has been bugging me.

    After evaluating writing a custom Formatter class as described in many answers here and even considering using third-party packages such as lazy_format, I discovered a much simpler inbuilt solution: Template strings

    It provides similar functionality but also provides partial substitution thorough safe_substitute() method. The template strings need to have a $ prefix (which feels a bit weird - but the overall solution I think is better).

    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'
    

    Formed a convenience wrapper based on this:

    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
    

    Similarly a wrapper based on Sven's answer which uses the default string formatting:

    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)
    
    0 讨论(0)
  • 2020-11-28 05:00

    All the solutions I've found seemed to have issues with more advanced spec or conversion options. @SvenMarnach's FormatPlaceholder is wonderfully clever but it doesn't work properly with coercion (e.g. {a!s:>2s}) because it calls the __str__ method (in this example) instead of __format__ and you lose any additional formatting.

    Here's what I ended up with and some of it's key features:

    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}'
    
    • provides similar interface as str.format (not just a mapping)
    • supports more complex formatting options:
      • coercion {k!s} {!r}
      • nesting {k:>{size}}
      • getattr {k.foo}
      • getitem {k[0]}
      • coercion+formatting {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)
    

    I discovered the issues with the various implementations after writing some tests on how I wanted this method to behave. They're below if anyone finds them insightful.

    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)
    
    0 讨论(0)
  • 2020-11-28 05:03

    If you define your own Formatter which overrides the get_value method, you could use that to map undefined field names to whatever you wanted:

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

    For instance, you could map bar to "{bar}" if bar isn't in the kwargs.

    However, that requires using the format() method of your Formatter object, not the string's format() method.

    0 讨论(0)
  • 2020-11-28 05:03
    >>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
    'fd:123:{topic_id}'
    

    Try this out.

    0 讨论(0)
提交回复
热议问题