一览 Python 3.12!PEP 701 —— f-string 语法规范化

1,061 阅读2分钟

f-string 是 Python 中一种用于字符串插值的语法,它可以让我们在字符串中使用表达式。例如:

>>> name = "Alice"
>>> f"Hello, {name}!"
'Hello, Alice!'

遗留问题

f-string 最初在 PEP 498 中引入,但是当时并没有提供一个正式的语法规则,而且还有有一些限制。这使得 f-string 的解析不能直接在词法分析器(lexer)中完成,而需要额外的后期处理。这些限制包括:

  • 不能在表达式中使用和 f-string 相同的引号字符,例如:
    >>> f'Magic wand: { bag['wand'] }'
                                 ^
    SyntaxError: invalid syntax
    
  • 不能在表达式中使用反斜杠,因为这会导致转义序列的问题,例如:
    >>> f'Magic wand { bag['wand'] } string'
    SyntaxError: f-string expression portion cannot include a backslash
    
  • 不能在多行 f-string 中使用注释,例如:
    >>> f'''A complex trick: {
    ... bag['bag']  # recursive bags!
    ... }'''
    SyntaxError: f-string expression part cannot include '#'
    
  • 虽然当时提出 PEP 498 时并没有明确限制嵌套,但由于“引号不可复用”的缘故,使得我们无法任意嵌套表达式。这是可以编写的“最嵌套”的 f-string:
    >>> f"""{f'''{f'{f"{1+1}"}'}'''}"""
    '2'
    
    嵌套这一点在其他语言中是支持的,例如 Ruby 和 JavaScript:
    "# { "# {1+2}" }"
    
    `${ `\${1+2}` }`
    

改动

从我们 Python 使用者的角度来说,这篇文章到这里应该已经非常明了了,这里稍微概括一下:

从 Python 3.12 开始,

  1. f-string 中可复用引号,且可嵌套;
  2. f-string 内可以是任意的 Python 表达式;
  3. 多行 f-string 支持注释。

如果我在这里结束的话,那属实是有点肤浅了(手动狗头)。

接下来带大家直观的感受一下这个改动在代码的词法解析(lexing)层面的不同~


要了解这一过程,我们需要使用内建的 tokenize 库。不必过度关注以下代码,理解结果即可:

import tokenize
from io import BytesIO


def tokenize_string(code):
    tokens = []
    code_bytes = code.encode('utf-8')
    code_stream = BytesIO(code_bytes)
    tokenize_generator = tokenize.tokenize(code_stream.readline)

    for token in tokenize_generator:
        token_type = tokenize.tok_name[token.type]
        tokens.append((token.string, token_type))

    return tokens


code_string = "..."  # 填上要解析的代码
for token in tokenize_string(code_string):
    token_string, token_type = token
    print(token_type, '\t', repr(token_string))

让我们看看下面这个例子在 Python 3.12 和 <Python 3.12 中是怎么解析的:

f'some words {a+b:.3f} more words {c+d=} final words'

运行结果

< Python 3.12
ENCODING         'utf-8'
STRING   "f'some words {a+b:.3f} more words {c+d=} final words'"
NEWLINE          ''
ENDMARKER        ''
Python 3.12
ENCODING         'utf-8'
FSTRING_START    "f'"
FSTRING_MIDDLE   'some words '
OP       '{'
NAME     'a'
OP       '+'
NAME     'b'
OP       ':'
FSTRING_MIDDLE   '.3f'
OP       '}'
FSTRING_MIDDLE   ' more words '
OP       '{'
NAME     'c'
OP       '+'
NAME     'd'
OP       '='
OP       '}'
FSTRING_MIDDLE   ' final words'
FSTRING_END      "'"
NEWLINE          '\n'
ENDMARKER        ''

可以看到,在 Python 3.12 之前,f-string 是直接解析成 STRING 标记(token)的(这也造成了许多衍生问题,这里不细讲)。

Python 3.12 引入了 3 个新标记:

  1. FSTIRNG_START:包含 f-string 前缀(fFfr)和开引号。
  2. FSTRING_MIDDLE:包含 f-string 内的不是表达式、也不是 {} 的文本。
  3. FSTRING_END:包含关引号。

什么是标记?代码是怎么解析的?感兴趣的可以看看这篇文章:我用 Python 创造了一门自己的编程语言


大家怎么看这一次的改动?是让代码更灵活了呢,还是更加难读懂了呢?

参考资料