partial string formatting

前端 未结 21 1020
野的像风
野的像风 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 05:10

    Here's a mildly-hacky regex-based solution. Note that this will NOT work with nested format specifiers like {foo:{width}}, but it does fix some of the problems that other answers have.

    def partial_format(s, **kwargs):
        parts = re.split(r'(\{[^}]*\})', s)
        for k, v in kwargs.items():
            for idx, part in enumerate(parts):
                if re.match(rf'\{{{k}[!:}}]', part):  # Placeholder keys must always be followed by '!', ':', or the closing '}'
                    parts[idx] = parts[idx].format_map({k: v})
        return ''.join(parts)
    
    # >>> partial_format('{foo} {bar:1.3f}', foo='FOO')
    # 'FOO {bar:1.3f}'
    # >>> partial_format('{foo} {bar:1.3f}', bar=1)
    # '{foo} 1.000'
    
    0 讨论(0)
  • 2020-11-28 05:10

    Assuming you won't use the string until it's completely filled out, you could do something like this class:

    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)
    

    Example:

    template = '#{a}:{}/{}?{c}'
    message = IncrementalFormatting(template)
    message.add('abc')
    message.add('xyz', a=24)
    message.add(c='lmno')
    assert message.get() == '#24:abc/xyz?lmno'
    
    0 讨论(0)
  • 2020-11-28 05:10

    Reading @Sam Bourne comment, I modified @SvenMarnach's code to work properly with coercion (like {a!s:>2s}) without writing a custom parser. The basic idea is not to convert to strings but concatenate missing keys with coercion tags.

    import string
    class MissingKey(object):
        def __init__(self, key):
            self.key = key
    
        def __str__(self):  # Supports {key!s}
            return MissingKeyStr("".join([self.key, "!s"]))
    
        def __repr__(self):  # Supports {key!r}
            return MissingKeyStr("".join([self.key, "!r"]))
    
        def __format__(self, spec): # Supports {key:spec}
            if spec:
                return "".join(["{", self.key, ":", spec, "}"])
            return "".join(["{", self.key, "}"])
    
        def __getitem__(self, i): # Supports {key[i]}
            return MissingKey("".join([self.key, "[", str(i), "]"]))
    
        def __getattr__(self, name): # Supports {key.name}
            return MissingKey("".join([self.key, ".", name]))
    
    
    class MissingKeyStr(MissingKey, str):
        def __init__(self, key):
            if isinstance(key, MissingKey):
                self.key = "".join([key.key, "!s"])
            else:
                self.key = key
    
    class SafeFormatter(string.Formatter):
        def __init__(self, default=lambda k: MissingKey(k)):
            self.default=default
    
        def get_value(self, key, args, kwds):
            if isinstance(key, str):
                return kwds.get(key, self.default(key))
            else:
                return super().get_value(key, args, kwds)
    

    Use (for example) like this

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

    The following tests (inspired by tests from @norok2) check the output for the traditional format_map and a safe_format_map based on the class above in two cases: providing correct keywords or without them.

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

    Which outputs

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

    If you're doing a lot of templating and finding Python's built in string templating functionality to be insufficient or clunky, look at Jinja2.

    From the docs:

    Jinja is a modern and designer-friendly templating language for Python, modelled after Django’s templates.

    0 讨论(0)
  • 2020-11-28 05:16

    You can trick it into partial formatting by overwriting the mapping:

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

    printing

    FOO {bar}
    

    Of course this basic implementation only works correctly for basic cases.

    0 讨论(0)
  • 2020-11-28 05:18

    My suggestion would be the following (tested with 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: An even more elegant way (subclassing dict and overloading __missing__(self, key)) is shown here: https://stackoverflow.com/a/17215533/333403

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