为什么`'{x[1:3]}'.format(x="asd")`会导致TypeError错误?

35
考虑一下:
>>> '{x[1]}'.format(x="asd")
's'
>>> '{x[1:3]}'.format(x="asd")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: string indices must be integers

这种行为的原因可能是什么?


6
漂亮。这在使用 f-strings 时也是有效的:x = 'asd' ; f'{x[1:3]}' - DeepSpace
如果有什么问题,我本来期望它会引发语法错误,因为它违反了格式化迷你语言规则,而不是类型错误。 - DeepSpace
@DeepSpace 它是如何违反规则的? - Kelly Bundy
@DeepSpace 它并没有违反规则,而是规则不允许切片。 - wjandrea
@DeepSpace 相似的代码:i = 1; x = 'asd'; f'{x[i]'}'{x[i]}'.format(i=1, x='asd') 的工作方式非常不同。 - Bergi
2个回答

28

基于你的评论的实验,检查对象的__getitem__方法实际接收到的值:

class C:
    def __getitem__(self, index):
        print(repr(index))

'{c[4]}'.format(c=C())
'{c[4:6]}'.format(c=C())
'{c[anything goes!@#$%^&]}'.format(c=C())
C()[4:6]

输出(在线尝试!):
4
'4:6'
'anything goes!@#$%^&'
slice(4, 6, None)

因此,尽管4被转换为int,但4:6并未像通常的切片一样被转换为slice(4, 6, None)。相反,它仍然是一个简单的字符串'4:6'。这不是索引/切片字符串的有效类型,因此您会收到TypeError:string indices must be integers的错误消息。 更新: 这是否有记录?嗯...我没有看到真正清晰的东西,但@GACy20 指出了一些微妙之处。语法具有以下规则。
field_name        ::=  arg_name ("." attribute_name | "[" element_index "]")*
element_index     ::=  digit+ | index_string
index_string      ::=  <any source character except "]"> +

我们的c[4:6]是field_name,我们关心的是element_index部分4:6。我认为如果digit+有一个有意义的名称的规则会更清晰:{{rule_with_meaningful_name}}。
field_name        ::=  arg_name ("." attribute_name | "[" element_index "]")*
element_index     ::=  index_integer | index_string
index_integer     ::=  digit+
index_string      ::=  <any source character except "]"> +

我认为使用index_integerindex_string会更清晰地表明digit+被转换为整数(而不是保持数字字符串),而<any source character except "]"> +将保持字符串
话虽如此,看着现有的规则,也许我们应该想一想:“将数字情况与任意字符情况分开有什么意义?后者也能匹配前者。”并且认为这样做的目的是特别处理纯数字,可能是将它们转换为整数。或者文档的其他部分甚至说明了digitdigits+通常被转换为整数。

1
从哲学上讲,你可以争论这里的index_string情况是特殊的:'{c[4]}'.format(c=c)执行与单独使用c[4]相同的查找;但是'{c[anything goes!@#$%^&]}'.format(c=c)表现得好像方括号内有引号,即c['anything goes!@#$%^&']。从这个观点来看,切片语法不起作用,因为它已经被“隐含的引号”情况所取代。(我强调这是一个哲学上的论点,而不是实际上的。) - IMSoP

17

'{x[1]}'.format(x="asd") 这里的 [1] 语法不是正常的字符串索引语法,即使在这种情况下它看起来像是以相同的方式工作。

它使用了格式规范迷你语言。这个机制还允许在格式化字符串中传递对象和访问任意属性(例如'{x.name}'.format(x=some_object))。

这个“假”的索引语法也允许将可索引对象传递给 format 并直接从格式化的字符串中获取所需的元素:

'{x[0]}'.format(x=('a', 'tuple'))
# 'a'
'{x[1]}'.format(x=('a', 'tuple'))
# 'tuple'

文档中唯一提到的关于此的参考资料(至少我能找到的)是这个段落:

field_name 本身以 arg_name 开头,arg_name 可以是数字或关键字。如果它是数字,则表示位置参数;如果是关键字,则表示命名关键字参数。如果格式字符串中的数字 arg_name 依次为 0、1、2…,则可以省略所有数字(而不仅仅是某些数字),然后数字 0、1、2… 将按照该顺序自动插入其中。由于 arg_name 没有引号定界,因此无法在格式字符串中指定任意字典键(例如字符串 '10' 或 ':-]')。arg_name 可以跟随任意数量的索引或属性表达式。形如 '.name' 的表达式使用 getattr() 选择命名属性,而形如 '[index]' 的表达式使用 __getitem__() 进行索引查找。

虽然它提到了

形如 '[index]' 的表达式使用 __getitem__() 进行索引查找。

但它没有提到不支持切片语法的任何内容。

对我来说,这感觉像是文档中的一个疏漏,特别是因为 '{x[1:3]}'.format(x="asd") 生成如此神秘的错误消息,更重要的是因为 __getitem__ 已经支持切片。


有趣!我尝试运行以下代码来查看底层发生了什么:class C:^M def __getitem__(self, x):^M print(repr(x))^Mf"{C()[1:2]}我得到了 slice(1, 2, None)。然后我尝试了 print("asd"[slice(1, 2, None)),但它没有导致 TypeError。我是否误解了正在发生的事情? - d33tah
1
实际上,语法非常清晰:element_index ::= digit+ | index_string index_string ::= <any source character except "]"> + 因此,element_index 要么是没有前导 + 的正整数,要么是 "索引字符串":"{x[1]} {x[+1]}".format(x={1: 'integer', '+1': 'string'}) -> 'integer string'。换句话说,在使用 [index_element] 语法时,index_element 总是被视为字面字符串,除非它与正则表达式 ^\d+$ 匹配,此时它将被解释为整数。他们本可以包含切片语法,但他们没有这样做。 - GACy20
1
请注意,即使添加一个空格也会破坏整数转换: "{x[ 1]}".format(x={' 1': 'string'}) -> 'string' - GACy20
@GACy20 我认为你说得对,但我觉得这点有点微妙。现在我已经把它包含在我的回答中了。 - Kelly Bundy

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