Python 字符串规范 | Google 官方 f-string 最佳实践
字符串
Tip
应该用 f-string、%运算符或format方法来格式化字符串. 即使所有参数都是字符串,也如此。你可以自行评判合适的选项。可以用+实现单次拼接,但是不要用+实现格式化。
正确:
x = f'名称: {name}; 分数: {n}'
x = '%s, %s!' % (imperative, expletive)
x = '{}, {}'.format(first, second)
x = '名称: %s; 分数: %d' % (name, n)
x = '名称: %(name)s; 分数: %(score)d' % {'name':name, 'score':n}
x = '名称: {}; 分数: {}'.format(name, n)
x = a + b
错误:
x = first + ', ' + second
x = '名称: ' + name + '; 分数: ' + str(n)
不要在循环中用 + 和 += 操作符来堆积字符串。这有时会产生平方而不是线性的时间复杂度。有时 CPython 会优化这种情况,但这是一种实现细节。我们无法轻易预测这种优化是否生效,而且未来情况可能出现变化。作为替代方案,你可以将每个子串加入列表,然后在循环结束后用 ''.join 拼接列表。也可以将每个子串写入一个 io.StringIO 缓冲区中。这些技巧保证始终有线性的平摊(amortized)时间复杂度。
正确:
items = ['<table>']
for last_name, first_name in employee_list:
items.append('<tr><td>%s, %s</td></tr>' % (last_name, first_name))
items.append('</table>')
employee_table = ''.join(items)
错误:
employee_table = '<table>'
for last_name, first_name in employee_list:
employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
employee_table += '</table>'
应该保持同一文件中字符串引号的一致性。选择 ' 或者 " 以后不要改变主意。如果需要避免用反斜杠来转义引号,则可以使用另一种引号。
正确:
Python('为什么你要捂眼睛?')
Gollum("I'm scared of lint errors. (我害怕格式错误.)")
Narrator('"很好!" 一个开心的 Python 审稿人心想.')
(译者注:注意 “I’m” 中间有一个单引号,所以这一行的外层引号可以用不同的引号。)
错误:
Python("为什么你要捂眼睛?")
Gollum('格式检查器. 它在闪耀. 它要亮瞎我们.')
Gollum("伟大的格式检查器永在. 它在看. 它在看.")
多行字符串推荐使用 """ 而非 '''。当且仅当项目中用 ' 给常规字符串打引号时,才能在文档字符串以外的多行字符串上使用 '''。无论如何,文档字符串必须使用 """。
多行字符串不会跟进代码其他部分的缩进。如果需要避免字符串中的额外空格,可以用多个单行字符串拼接,或者用 textwrap.dedent() 删除每行开头的空格。
错误:
long_string = """这样很难看.
不要这样做.
"""
正确:
long_string = """如果你可以接受多余的空格,
就可以这样."""
long_string = ("如果你不能接受多余的空格,\n" +
"可以这样.")
long_string = ("如果你不能接受多余的空格,\n"
"也可以这样.")
import textwrap
long_string = textwrap.dedent("""\
这样也行, 因为 textwrap.dedent()
会删除每一行开头共有的空格.""")
注意,这里的反斜杠没有违反 对换行符转义.
日志
对于那些第一个参数是格式字符串 (包含 % 占位符) 的日志函数:一定要用字符串字面量(而非 f-string!)作为第一个参数,并用占位符的参数作为其他参数。有些日志的实现会收集未展开的格式字符串,作为可搜索的项目。这样也可以免于渲染那些被设置为不用输出的消息。
正确:
import tensorflow as tf
logger = tf.get_logger()
logger.info('TensorFlow 的版本是: %s', tf.__version__)
import os
from absl import logging
logging.info('当前的 $PAGER 是: %s', os.getenv('PAGER', default=''))
homedir = os.getenv('HOME')
if homedir is None or not os.access(homedir, os.W_OK):
logging.error('无法写入主目录, $HOME=%r', homedir)
错误:
import os
from absl import logging
logging.info('当前的 $PAGER 是:')
logging.info(os.getenv('PAGER', default=''))
homedir = os.getenv('HOME')
if homedir is None or not os.access(homedir, os.W_OK):
logging.error(f'无法写入主目录, $HOME={homedir!r}')
错误信息
错误信息(例如:诸如 ValueError 等异常的信息字符串和展示给用户的信息)应该遵守以下三条规范:
- 信息需要精确地匹配真正的错误条件。
- 插入的片段一定要能清晰地分辨出来。
- 要便于简单的自动化处理(例如正则搜索,也就是 grepping)。
正确:
if not 0 <= p <= 1:
raise ValueError(f'这不是概率值: {p!r}')
try:
os.rmdir(workdir)
except OSError as error:
logging.warning('无法删除这个文件夹 (原因: %r): %r',
error, workdir)
错误:
if p < 0 or p > 1: # 问题: 遇到 float('nan') 时也为假!
raise ValueError(f'这不是概率值: {p!r}')
try:
os.rmdir(workdir)
except OSError:
# 问题: 信息中存在错误的揣测,
# 删除操作可能因为其他原因而失败, 此时会误导调试人员.
logging.warning('文件夹已被删除: %s', workdir)
try:
os.rmdir(workdir)
except OSError:
# 问题: 这个信息难以搜索, 而且某些 `workdir` 的值会让人困惑.
# 假如有人调用这段代码时让 workdir = '已删除'. 这个警告会变成:
# "无法删除已删除文件夹."
logging.warning('无法删除%s文件夹.', workdir)