I've run into a fairly simple issue that I can't come up with an elegant solution for.

I'm creating a string using str.format in a function that is passed in a dict of substitutions to use for the format. I want to create the string and format it with the values if they're passed and leave them blank otherwise.

Ex

kwargs = {"name": "mark"}
"My name is {name} and I'm really {adjective}.".format(**kwargs)

should return

"My name is mark and I'm really ."

instead of throwing a KeyError (Which is what would happen if we don't do anything).

Embarrassingly, I can't even come up with an inelegant solution for this problem. I guess I could solve this by just not using str.format, but I'd rather use the built-in (which mostly does what I want) if possible.

Note: I don't know in advance what keys will be used. I'm trying to fail gracefully if someone includes a key but doesn't put it in the kwargs dict. If I knew with 100% accuracy what keys would be looked up, I'd just populate all of them and be done with it.

有帮助吗?

解决方案

You can follow the recommendation in PEP 3101 and use a subclass Formatter:

import string

class BlankFormatter(string.Formatter):
    def __init__(self, default=''):
        self.default=default

    def get_value(self, key, args, kwds):
        if isinstance(key, str):
            return kwds.get(key, self.default)
        else:
            return string.Formatter.get_value(key, args, kwds)

kwargs = {"name": "mark", "adj": "mad"}     
fmt=BlankFormatter()
print fmt.format("My name is {name} and I'm really {adj}.", **kwargs)
# My name is mark and I'm really mad.
print fmt.format("My name is {name} and I'm really {adjective}.", **kwargs)
# My name is mark and I'm really .  

As of Python 3.2, you can use .format_map as an alternative:

class Default(dict):
    def __missing__(self, key):
        return '{'+key+'}'

kwargs = {"name": "mark"}

print("My name is {name} and I'm really {adjective}.".format_map(Default(kwargs)))

which prints:

My name is mark and I'm really {adjective}.

其他提示

Here is one option which uses collections.defaultdict:

>>> from collections import defaultdict
>>> kwargs = {"name": "mark"}
>>> template = "My name is {0[name]} and I'm really {0[adjective]}."
>>> template.format(defaultdict(str, kwargs))
"My name is mark and I'm really ."

Note that we aren't using ** to unpack the dictionary into keyword arguments anymore, and the format specifier uses {0[name]} and {0[adjective]}, which indicates that we should perform a key lookup on the first argument to format() using "name" and "adjective" respectively. By using defaultdict a missing key will result in an empty string instead of raising a KeyError.

For the record:

s = "My name is {name} and I'm really {adjective}."
kwargs = dict((x[1], '') for x in s._formatter_parser())
# Now we have: `kwargs = {'name':'', 'adjective':''}`.
kwargs.update(name='mark')
print s.format(**kwargs)  # My name is mark and I'm really .

Wanted to add a pretty simple solution to the substituting any default values needed.

import string

class SafeDict(dict):
    def __init__(self, missing='#', empty='', *args, **kwargs):
        super(SafeDict, self).__init__(*args, **kwargs)
        self.missing = missing
        self.empty = empty
    def __getitem__(self, item):
        return super(SafeDict, self).__getitem__(item) or self.empty
    def __missing__(self, key):
        return self.missing

values = SafeDict(a=None, c=1})
string.Formatter().vformat('{a} {c} {d}', (), values)
# ' 1 #'

While subclassing a Formatter is probably the "right" answer, it is also possible to follow Python's strong ask-for-forgiveness-not-permission vein by catching the KeyError. The advantage of this approach is that it is easily flexible: In particular, it is easy to have "default" values which are not static (i.e., just a possibly blank constant) but can depend on the name of the key, such as here:

def f(s, **kwargs):
    """Replaces missing keys with a pattern."""
    RET = "{{{}}}"
    try:
        return s.format(**kwargs)
    except KeyError as e:
        keyname = e.args[0]
        return f(s, **{ keyname: RET.format(keyname) }, **kwargs)

which will work in the following way:

In [1]: f("My name is {name} and I'm really {adjective}.", **{"name": "Mark"})
Out[1]: "My name is Mark and I'm really {adjective}."

This can be specialized easily to do what the OP wanted:

def f_blank(s, **kwargs):
    """Replaces missing keys with a blank."""
    try:
        return s.format(**kwargs)
    except KeyError as e:
        keyname = e.args[0]
        return f(s, **{ keyname: "" }, **kwargs)

I had a bit more fun with this idea: https://gist.github.com/jlumbroso/57951c06a233c788e00d0fc309a93f91

# (not a real import! just saying importing the code from the Gist)
from gist.57951c06a233c788e00d0fc309a93f91 import _make_f

# Define replacement f"..." compatible with Python 2 and 3
_f = _make_f(globals=lambda: globals(), locals=lambda: locals())

# Use:
s = "Test"
var = 1
assert _f("{s} {var}") == "Test 1"

# Inside a non-global scope, you may have to provide locals
def test():
    l_s = "Test"
    l_var = 1
    assert _f("{l_s} {l_var} / {s} {var}") == "{l_s} {l_var} / Test 1"
    assert _f("{l_s} {l_var} / {s} {var}", **locals()) == "Test 1 / Test 1"

If you are still on Python 2, you can use defaultdict with string.Formatter to achieve this:

>>> format_string = '{foo:<2s}{bar:<3s}'
>>> data = {'bar': 'baz'}
>>> string.Formatter().vformat(format_string, (), defaultdict(str, data))
'  baz'

Other solutions have one or more of the following characteristics, which won't work for my needs: * don't support compound / nested names * require python 3 * don't work * don't give control over hierarchical default names in the case of a missing key

So here's a solution that addresses these issues. Note that this solution only works for text names e.g. "{key}" or "{nested[key]}". Not sure it will work for more than that e.g. "{foo:<2s}".

This also doesn't handle arrays, but could easily be extended to do so. You could also substitute your own function to output whatever placeholder result for missing values you wanted.

Usage examples:

my_data = {
    'hi': 'there',
    'abc': {
        'def': 'ghi'
    },
    'level1': {
        'level2': {
            'level3': 'nested'
        }
    }
}

s = "{hi} there"
print FormatDict(my_data).format(s) # 'there there'

s = "{hi} there {abc[def]}"
print FormatDict(my_data).format(s) # 'there there ghi'

s = "{hix} there {abc[def]}"
print FormatDict(my_data).format(s) # '{hix} there ghi'

s = "{hix} there {abc[defx]}"
print FormatDict(my_data).format(s) # '{hix} there {abc[defx]}'

s = "{hi} there {level1[level2][level3]}"
print FormatDict(my_data).format(s) # 'there there nested'

s = "{hix} there {level1[level2][level3x]}"
print FormatDict(my_data).format(s) # '{hix} there {level1[level2][level3x]}'

Here's the code:

import string

class FormatDict(dict):
    def set_parent(self, parent):
        self.parent = parent

    def __init__(self, *args, **kwargs):
        self.parent = None
        self.last_get = ''
        for arg in (args or []):
            if isinstance(arg, dict):
                for k in arg:
                    self.__setitem__(k, arg[k])
        for k in (kwargs or {}):
            self.__setitem__(k, kwargs[k])

    def __getitem__(self, k):
        self.last_get = k
        try:
            val = dict.__getitem__(self, k)
            return val
        except:
            ancestry = [k]
            x = self.parent
            while x:
                ancestry.append(x.last_get)
                x = x.parent
            ancestry.reverse()
            return '{' + ancestry[0] + ''.join(['[' + x + ']' for x in ancestry[1:]]) + '}'

    def __setitem__(self, k, v):
        if isinstance(v, dict):
            v = FormatDict(v)
            v.set_parent(self)
        dict.__setitem__(self, k, v)

    def format(self, s):
        return string.Formatter().vformat(s, (), self)

A way to avoid the key error is to include in the dict but leave it blank:

kwargs = {"name": "mark", "adjective": ""}
"My name is {name} and I'm really {adjective}.".format(**kwargs)

Keyword arguments expect their to be a key in kwargs. Another way to do it would be positional arguments:

"My name is {0} and I'm really {1}.".format("mark")

Prints "My name is Mark and I'm really." While

"My name is {0} and I'm really {1}.".format("mark","black")

Prints "My name is Mark and I'm really black."

Alternatively, you can catch the ValueError.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top