格式化字符串未使用命名参数

75

假设我有:

action = '{bond}, {james} {bond}'.format(bond='bond', james='james')

这将输出:

'bond, james bond' 

接下来我们有:

 action = '{bond}, {james} {bond}'.format(bond='bond')

这将输出:

KeyError: 'james'

有没有一些解决方案来防止这个错误发生,比如:

  • 如果keyerror:忽略,不做处理(但会解析其他内容)
  • 将格式字符串与可用的命名参数进行比较,如果缺失则添加

你想要哪一个 bond, bond / bond, {james}, bond - falsetru
我认为第二个更好。第一个可能会创建奇怪的内容...第二个会让人们想“嘿,这里有些问题”,在这种情况下是一件好事。 - nelsonvarela
我已经更新了两种情况的答案。 - falsetru
https://dev59.com/Em035IYBdhLWcg3wW-z1 - Qlimax
参见: https://dev59.com/KVsV5IYBdhLWcg3w-i0C - dreftymac
有关此问题的有趣博客文章:正确处理str.format_map中缺失的键 - Delgan
9个回答

128

如果您使用的是Python 3.2+,可以使用str.format_map()

对于bond, bond

from collections import defaultdict
'{bond}, {james} {bond}'.format_map(defaultdict(str, bond='bond'))

结果:

'bond,  bond'

对于债券,{詹姆斯}债券

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

'{bond}, {james} {bond}'.format_map(SafeDict(bond='bond'))

结果:

'bond, {james} bond'

在Python 2.6/2.7中

对于bond, bond

from collections import defaultdict
import string
string.Formatter().vformat('{bond}, {james} {bond}', (), defaultdict(str, bond='bond'))

结果:

'bond,  bond'

对于邦德,{詹姆斯}邦德

from collections import defaultdict
import string

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

string.Formatter().vformat('{bond}, {james} {bond}', (), SafeDict(bond='bond'))

结果:

'bond, {james} bond'

我正在使用2.7版本。谢谢!我将测试这个。 - nelsonvarela
奇怪的是,在Python3中,format在这个例子中给出了与format_map相同的结果。 - frnhr
1
@frnhr,你的意思是什么?'{bond}, {james} {bond}'.format(defaultdict(str, bond='bond'))让我出现了KeyError错误。 - falsetru
1
这里是:'{bond},{james} {bond}'。format(**SafeDict(bond='bond')) 返回 'bond,{james} bond'。请注意 ** 的使用。直到现在我才注意到它 :) Python 3.4.3 - frnhr
2
这会丢失任何格式,例如 "{bond:<5}"' - HoosierDaddy
显示剩余3条评论

30

你可以使用 模板字符串safe_substitute 方法。

from string import Template

tpl = Template('$bond, $james $bond')
action = tpl.safe_substitute({'bond': 'bond'})

1
我早在几年前就知道这个技巧了,但整个问答仍然再次确认了Template在这种情况下是最好的选择。点赞。 :-) - RayLuo
迄今为止最简单的方法,不会让你陷入认为它可以处理更复杂格式化程序(如{my_float:>5.2f})的陷阱中。它显然是普通的字符串替换。 - BallpointBen

18

您可以遵循 PEP 3101 中的建议并创建子类 Formatter:

from __future__ import print_function
import string

class MyFormatter(string.Formatter):
    def __init__(self, default='{{{0}}}'):
        self.default=default

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

现在试试吧:
>>> fmt=MyFormatter()
>>> fmt.format("{bond}, {james} {bond}", bond='bond', james='james')
'bond, james bond'
>>> fmt.format("{bond}, {james} {bond}", bond='bond')
'bond, {james} bond'

您可以通过更改 self.default 中的文本来更改如何标记关键错误,以显示您想要显示的 KeyError 信息:

>>> fmt=MyFormatter('">>{{{0}}} KeyError<<"')
>>> fmt.format("{bond}, {james} {bond}", bond='bond', james='james')
'bond, james bond'
>>> fmt.format("{bond}, {james} {bond}", bond='bond')
'bond, ">>{james} KeyError<<" bond'

这段代码在Python 2.6、2.7和3.0以上版本中可以不做任何修改地运行。


1
在这里提供的答案中,我认为这个在可移植性/优雅方面是最好的 +1。 - Ajay
我认为在你的代码中,Formatter.get_value(key, args, kwds) 应该改为 return string.Formatter.get_value(self, key, args, kwds) - Grijesh Chauhan
@GrijeshChauhan 我不确定...该函数被递归调用,唯一真正的返回发生在最后一个终端调用中,所以...无论如何,我没有成功让它工作。我最终使用了更简单的代码,它可以实现我想要的功能。class URLFormatter(string.Formatter): def init(self, default='{}'): self.default=default def get_value(self, key, args, kwds): return kwds.get(key, self.default.format(key)) - Stéphane

10

你也可以做简单易懂,尽管有些愚蠢:

'{bar}, {fro} {bar}'.format(bar='bar', fro='{fro}')
我知道这个答案需要对期望的键有所了解, 但我正在寻找一个简单的两步替换方法(首先是问题名称,然后是循环内的问题索引),而创建整个类或难以阅读的代码比实际需要的更加复杂。

10

falsetru的答案巧妙地使用了一个默认字典和vformat(),而dawg的答案可能更符合Python文档的要求,但两者都不能处理复合字段名(例如,具有显式转换(!r)或格式规范(:+10g)的字段名)。

例如,使用falsetru的SafeDict:

>>> string.Formatter().vformat('{one} {one:x} {one:10f} {two!r} {two[0]}', (), SafeDict(one=215, two=['James', 'Bond']))
"215 d7 215.000000 ['James', 'Bond'] James"
>>> string.Formatter().vformat('{one} {one:x} {one:10f} {two!r} {two[0]}', (), SafeDict(one=215))
"215 d7 215.000000 '{two}' {"

使用 dawg 的 MyFormatter:

>>> MyFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215, two=['James', 'Bond'])
"215 d7 215.000000 ['James', 'Bond'] James"
>>> MyFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215)
"215 d7 215.000000 '{two}' {"

由于值查找(在get_value()中)已经剥离了格式规范,因此两者都不适用于第二种情况。相反,您可以重新定义vformat()parse(),以便这些规范可用。我的解决方案通过重新定义vformat()以执行关键字查找,并且如果键缺失,则使用双括号转义格式字符串(例如{{two!r}}),然后执行正常的vformat()

class SafeFormatter(string.Formatter):
    def vformat(self, format_string, args, kwargs):
        args_len = len(args)  # for checking IndexError
        tokens = []
        for (lit, name, spec, conv) in self.parse(format_string):
            # re-escape braces that parse() unescaped
            lit = lit.replace('{', '{{').replace('}', '}}')
            # only lit is non-None at the end of the string
            if name is None:
                tokens.append(lit)
            else:
                # but conv and spec are None if unused
                conv = '!' + conv if conv else ''
                spec = ':' + spec if spec else ''
                # name includes indexing ([blah]) and attributes (.blah)
                # so get just the first part
                fp = name.split('[')[0].split('.')[0]
                # treat as normal if fp is empty (an implicit
                # positional arg), a digit (an explicit positional
                # arg) or if it is in kwargs
                if not fp or fp.isdigit() or fp in kwargs:
                    tokens.extend([lit, '{', name, conv, spec, '}'])
                # otherwise escape the braces
                else:
                    tokens.extend([lit, '{{', name, conv, spec, '}}'])
        format_string = ''.join(tokens)  # put the string back together
        # finally call the default formatter
        return string.Formatter.vformat(self, format_string, args, kwargs)

这是它的实际效果:

>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215, two=['James', 'Bond'])
"215 d7 215.000000 ['James', 'Bond'] James"
>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215)
'215 d7 215.000000 {two!r} {two[0]}'
>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}')
'{one} {one:x} {one:10f} {two!r} {two[0]}'
>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', two=['James', 'Bond'])
"{one} {one:x} {one:10f} ['James', 'Bond'] James"

这种解决方法有点过于hacky(也许重新定义parse()会减少一些修补),但应该适用于更多的格式化字符串。


非常好的解决方案 - user8162

3

当逐步填写格式字符串时,例如对于SQL查询,需要部分填充格式字符串是一个常见问题。

format_partial() 方法使用stringast中的 Formatter来解析格式字符串,并查找命名参数哈希是否具有部分计算格式所需的所有值:

import ast
from collections import defaultdict
from itertools import chain, ifilter, imap
from operator import itemgetter
import re
from string import Formatter

def format_partial(fstr, **kwargs):
    def can_resolve(expr, **kwargs):
        walk = chain.from_iterable(imap(ast.iter_fields, ast.walk(ast.parse(expr))))
        return all(v in kwargs for k,v in ifilter(lambda (k,v): k=='id', walk))

    ostr = fstr
    fmtr = Formatter()
    dd = defaultdict(int)
    fmtr.get_field = lambda field_name, args, kwargs: (dd[field_name],field_name)
    fmtr.check_unused_args = lambda used_args, args, kwargs: all(v in dd for v in used_args)
    for t in ifilter(itemgetter(1), Formatter().parse(fstr)):
        f = '{'+t[1]+(':'+t[2] if t[2] else '')+'}'
        dd = defaultdict(int)
        fmtr.format(f,**kwargs)
        if all(can_resolve(e,**kwargs) for e in dd):
            ostr = re.sub(re.escape(f),Formatter().format(f, **kwargs),ostr,count=1)
    return ostr

format_partial 会保留格式字符串中未解决的部分,因此可以在数据可用时使用后续调用来解决这些部分。

goodmami和dawg的答案似乎更为简洁,但它们都未能完全捕获格式迷你语言,如{x:>{x}}; format_partial将无法解决任何string.format()解决的格式字符串:

from datetime import date
format_partial('{x} {} {y[1]:x} {x:>{x}} {z.year}', **{'x':30, 'y':[1,2], 'z':date.today()})

'30 {} 2                             30 2016'

使用正则表达式而不是字符串格式化程序甚至可以更轻松地将功能扩展到旧样式格式字符串,因为旧样式格式子字符串是常规的(即没有嵌套标记)。


1
代码通过格式模板替换已解决的部分的方式现在存在很多漏洞。 - topkara
也许你应该说明一下问题是什么? - norok2

2

以下是使用python27的另一种方法:

action = '{bond}, {james} {bond}'
d = dict((x[1], '') for x in action._formatter_parser())
# Now we have: `d = {'james': '', 'bond': ''}`.
d.update(bond='bond')
print action.format(**d)  # bond,  bond

1
有些人可能会反对使用_formatter_parser,但对我来说,这是最符合Python风格的方法:简单、易于理解、使用开箱即用的功能。如果你修改第二行为d = dict((x[1], '{'+str(x[1])+'}') for x in action._formatter_parser()),你可以像使用bond, bond格式一样轻松地得到bond, {james} bond格式。 - hlongmore

1
根据其他答案,我扩展了解决方案。这将处理带有格式规范"{a:<10}"的字符串。
我发现一些来自selenium日志记录的字符串会导致vformat(和format_map)达到递归限制。我还想确保我能处理存在空花括号的字符串。
def partialformat(s: str, recursionlimit: int = 10, **kwargs):
    """
    vformat does the acutal work of formatting strings. _vformat is the 
    internal call to vformat and has the ability to alter the recursion 
    limit of how many embedded curly braces to handle. But for some reason 
    vformat does not.  vformat also sets the limit to 2!   

    The 2nd argument of _vformat 'args' allows us to pass in a string which 
    contains an empty curly brace set and ignore them.
    """

    class FormatPlaceholder:
        def __init__(self, key):
            self.key = key

        def __format__(self, spec):
            result = self.key
            if spec:
                result += ":" + spec
            return "{" + result + "}"

    class FormatDict(dict):
        def __missing__(self, key):
            return FormatPlaceholder(key)

    class PartialFormatter(string.Formatter):
        def get_field(self, field_name, args, kwargs):
            try:
                obj, first = super(PartialFormatter, self).get_field(field_name, args, kwargs)
            except (IndexError, KeyError, AttributeError):
                first, rest = formatter_field_name_split(field_name)
                obj = '{' + field_name + '}'

                # loop through the rest of the field_name, doing
                #  getattr or getitem as needed
                for is_attr, i in rest:
                    if is_attr:
                        try:
                            obj = getattr(obj, i)
                        except AttributeError as exc:
                            pass
                    else:
                        obj = obj[i]

            return obj, first

    fmttr = string.Formatter()
    fs, _ = fmttr._vformat(s, ("{}",), FormatDict(**kwargs), set(), recursionlimit)
    return fs

class ColorObj(object):
    blue = "^BLUE^"
s = '{"a": {"b": {"c": {"d" : {} {foo:<12} & {foo!r} {arg} {color.blue:<10} {color.pink} {blah.atr} }}}}'
print(partialformat(s, foo="Fooolery", arg="ARRrrrrrg!", color=ColorObj))

输出:

{"a": {"b": {"c": {"d" : {} Fooolery             & 'Fooolery' Fooolery ARRrrrrrg! ^BLUE^ {color.pink} {blah.atr} }}}}

1

对于Python 3,采用经过批准的答案,这是一个不错、紧凑、Pythonic的实现:

def safeformat(str, **kwargs):
    class SafeDict(dict):
        def __missing__(self, key):
            return '{' + key + '}'
    replacements = SafeDict(**kwargs)
    return str.format_map(replacements)

# In [1]: safeformat("a: {a}, b: {b}, c: {c}", a="A", c="C", d="D")
# Out[1]: 'a: A, b: {b}, c: C'

很遗憾,这无法处理"{a:<10}"的字符串情况。 - Marcel Wilson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接