Python 进阶指南(编程轻松进阶):三、使用 Black 工具来格式化代码

2023-04-09 09:30:48 浏览数 (1)

原文:http://inventwithpython.com/beyond/chapter3.html 代码格式化是将一组规则应用于源代码,从而使得代码风格能够简洁统一。虽然代码格式对解析程序的计算机来说不重要,但代码格式对于可读性是至关重要的,这是维护代码所必需的条件。如果你的代码对人(无论是你还是同事)来说都很难理解,那么就很难修复错误或添加新功能。格式化代码不仅仅是一个风格的问题。并且 Python 的可读性也是该语言受欢迎的一个重要原因。

本章向您介绍 Black,它是一个代码格式化工具,可以自动将您的源代码格式化成一致的、可读的样式,而不改变您的程序的功能。Black 很有用,因为在文本编辑器或 IDE 中手动格式化代码很繁琐。您将首先了解使用 Black 格式化代码的合理性。然后,您将学习如何安装、使用和定制该工具。

如何失去朋友和疏远同事

我们可以用多种方式编写代码,产生相同的行为。例如,我们可以编写一个列表,在每个逗号后加一个空格,并始终使用一种引用字符:

代码语言:javascript复制
spam = ['dog', 'cat', 'moose']

但是,即使我们用不同数量的空格和不同的引号样式编写列表,这在语法上仍然是有效的 Python 代码:

代码语言:javascript复制
spam= [ 'dog'  ,'cat',"moose"]

喜欢前一种方法的程序员可能喜欢空格添加的视觉分隔和引号字符的一致性。但程序员有时会选择后者,因为他们这要保证程序功能正确即可,代码可读性的细节不做过度考虑。

初学者经常忽略代码格式,因为他们专注于编程概念和语言语法。但是对于初学者来说,建立良好的代码格式化习惯是很有价值的。编程已经够难的了,给别人(或者以后给自己)写可以理解的代码,在分析代码问题时就不会因为可读性而头疼了。

尽管您可能需要独自编写代码,但编程通常是一项协作活动。如果几个程序员在同一个源代码文件上工作,用他们自己的风格编写,代码可能会变得不一致,混乱不堪,即使它运行时没有错误。或者更糟的是,程序员会不断地将彼此的代码重新格式化成他们自己的风格,浪费时间并引起争论。比如说,决定在逗号后面加一个还是零个空格是个人喜好的问题。这些代码风格选择很像决定在道路的哪一边行驶;人们是在路的右边还是左边开车并不重要,重要的是大家都要习惯于在同一边开车。

风格指南和 PEP8

编写可读代码的一个简单方法是遵循风格指南,这是一个概述了一套软件项目应该遵循的格式规则的文档。由 Python 核心开发团队编写的 Python 增强提案 8PEP8)就是这样一个风格指南。但是一些软件公司也建立了他们自己的风格指南。

你可以在www.python.org/dev/peps/pep-0008在线阅读 PEP8。许多 Python 程序员将 PEP8 视为一组权威的规则,尽管 PEP8 的创建者们不这么认为。指南的“愚蠢的一致性是心胸狭窄的妖怪”一节提醒读者,保持项目中的一致性和可读性,而不是遵守任何个人的格式规则,是编写这个格式指南的主要原因。

PEP8 甚至包括以下建议:“知道什么时候不一致——有时风格指南的建议并不适用。如有疑问,请做出最佳判断。”无论您是全部遵循、部分遵循还是一点都不遵循,都值得阅读 PEP8 文档。

因为我们使用了 Black 来格式化代码程序,所以我们的代码将遵循 Black 的样式指南,该指南改编自 PEP8 的样式指南。您应该学习这些代码格式指南,因为您在某些特殊环境下编码的时候可能不能使用Black来格式化代码。您在本章中学习的 Python 代码指南通常也适用于其他语言,这些语言可能没有可用的自动格式化程序。

我并不喜欢 Black 的所有代码格式,但我认为这是一个很好的妥协。Black 使用程序员可以忍受的格式规则,让我们花更少的时间争论,花更多的时间编程。

水平间距

空白对于可读性来说和你写的代码一样重要。这些空格有助于将代码的不同部分彼此分开,使它们更容易识别。本节解释了水平间距——即单行代码中空白间隔,包括该行前面的缩进大小。

使用空格字符的缩进

缩进是代码行开头的空格。您可以使用两个空白字符(空格或制表符)中的一个来缩进代码。尽管这两种字符都有效,但最佳实践是使用空格而不是制表符进行缩进。

原因是这两种方式的行为方式不同。一个空格字符总是在屏幕上呈现为带有一个空格的字符串值,就像这个' '。但是制表符,即包含转义字符或't'的字符串值,更不明确。制表符通常(但不总是)呈现为可变的间距量,因此下面的文本从下一个制表位开始。在文本文件的宽度上,制表位代表八个空格符。您可以在下面的交互式 Shell 示例中看到这种变化,该示例首先用空格字符分隔单词,然后用制表符分隔单词:

代码语言:javascript复制
>>> print('Hello there, friend!nHow are you?')
Hello there, friend!
How are you?
>>> print('Hellotthere,tfriend!nHowtaretyou?')
Hello   there,  friend!
How     are     you?

因为制表符代表不同宽度的空白,你应该避免在你的源代码中使用它们。当你按下Tab键键而不是一个制表符时,大多数代码编辑器和 ide 会自动插入四或八个空格字符。

你也不能在同一个代码块中使用制表符和空格来缩进。在早期的 Python 程序中,使用两者进行缩进是一个错误的万恶之源,以至于 Python3 甚至不会运行带有缩进的代码;而是引发一个TabError: inconsistent use of tabs and spaces in indentation异常。Black 会自动将您用于缩进的任何制表符转换为四个空格字符。

至于每一级缩进的长度,Python 代码中通常的做法是每一级缩进四个空格。以下示例中的空格字符用句号标记空格,以使其可见:

代码语言:javascript复制
def getCatAmount():
....numCats = input('How many cats do you have?')

....if int(numCats) < 6:

........print('You should get more cats.')

与备选方案相比,四个空格的标准有实际的好处;在每一级缩进中使用八个空格会导致代码很快超出行长度限制,而在每一级缩进中使用两个空格会使缩进中的差异难以看出。程序员通常不会考虑其他数量,比如三个或六个空格,因为他们和一般的二进制计算一样,首选二的幂:2、4、8、16 等等。

行内间距

水平间距不仅仅是缩进。空格对于让程序员在视觉上对代码进行分块是很重要的。如果您从不使用空格字符调整间距,那么您的行可能会变得密集而难以解析。以下小节提供了一些需要遵循的间距规则。

在操作符和标识符之间加一个空格

如果你不在操作符和标识符之间留空格,你的代码看起来会一起运行。例如,这一行用空格分隔运算符和变量:

代码语言:javascript复制
blanks = blanks[:i]   secretWord[i]   blanks[i   1 :] # YES

这一行删除所有空格:

代码语言:javascript复制
$1 # NOblanks=blanks[:i] secretWord[i] blanks[i 1:]

在这两种情况下,代码都使用了 操作符将三个值相加,但是如果没有空格,blanks[i 1:]中的 看起来像是在添加第四个值。空格使得这个 blanks中值的一部分变得更加明显。

分隔符前不加空格,分隔符后加一个空格

我们用逗号分隔条目列表和字典,以及函数def语句中的参数。您不应在这些逗号前放置空格,而应在逗号后放置一个空格,如下例所示:

代码语言:javascript复制
def spam(eggs, bacon, ham): # YES
    weights = [42.0, 3.1415, 2.718] # YES

否则,您最终会得到更难阅读的“搅合”代码:

代码语言:javascript复制
$1 # NOdef spam(eggs,bacon,ham):
$1 # NO    weights = [42.0,3.1415,2.718]

不要在分隔符前添加空格,因为不要将注意力放在分隔符上:

代码语言:javascript复制
$1 # NOdef spam(eggs , bacon , ham):
$1 # NO    weights = [42.0 , 3.1415 , 2.718]

Black 会自动在逗号后面插入一个空格,并删除逗号前面的空格。

不要在句号之前或之后加空格

Python 允许您在标记 Python 属性开头的点号前后插入空格,但您应该避免这样做。通过不在那里放置空格,可以强调对象与其属性之间的联系,如下例所示:

代码语言:javascript复制
'Hello, world'.upper() # YES

如果您在句号之前或之后添加空格,对象和属性看起来就像彼此无关:

代码语言:javascript复制
$1 # NO'Hello, world' . upper()

Black 会自动删除句号周围的空格。

不要在函数、方法或容器名后加空格

我们很容易识别函数和方法名,因为它们后面有一组括号,所以不要在名字和左括号之间加空格。我们通常会编写这样的函数调用:

代码语言:javascript复制
print('Hello, world!') # YES

但是添加一个空格会使这个函数调用看起来像是在做两件不同的事情:

代码语言:javascript复制
$1 # NOprint ('Hello, world!')

Black 删除函数或方法名与其左括号之间的任何空格。

同样,不要在索引、切片或键的方括号前加空格。我们通常访问容器类型(如列表、字典或元组)中的项,而不在变量名和左方括号之间添加空格,如下所示:

代码语言:javascript复制
spam[2] # YES
spam[0:3] # YES
pet['name'] # YES

再次添加空格会使代码看起来像两个独立的东西:

代码语言:javascript复制
$1 # NOspam [2]
$1 # NOspam    [0:3]
$1 # NOpet ['name']

Black 删除变量名和左方括号之间的任何空格。

不要在左括号后或右括号前加空格

圆括号、方括号或大括号及其内容之间不应有空格。例如,def语句中的参数或列表中的值应该紧接在圆括号和方括号的前后开始和结束:

代码语言:javascript复制
def spam(eggs, bacon, ham): # YES
    weights = [42.0, 3.1415, 2.718] # YES

不应在左括号或右括号或方括号之后或之前添加空格:

代码语言:javascript复制
$1 # NOdef spam( eggs, bacon, ham ):
$1 # NO    weights = [ 42.0, 3.1415, 2.718 ]

添加这些空格不会提高代码的可读性,所以没有必要。如果代码中存在这些空格,Black 将删除它们。

在行尾注释前加两个空格

如果您在代码行的末尾添加注释,请在代码的末尾和开始注释的#字符之前添加两个空格:

代码语言:javascript复制
print('Hello, world!')  # Display a greeting. # YES

这两个空格使得区分代码和注释更加容易。一个空格,或者更糟的是,没有空格,会使人更难注意到代码和注释的分离:

代码语言:javascript复制
$1 # NOprint('Hello, world!') # Display a greeting.
$1 # NOprint('Hello, world!')# Display a greeting.

Black 在代码的结尾和注释的开头之间加了两个空格。

一般来说,我建议不要把注释放在代码行的末尾,因为它们会使代码行太长而无法在屏幕上阅读。

垂直间距

垂直间距是代码行之间空白行的位置。就像书中的新段落可以防止句子形成文本墙一样,垂直间距可以将某些代码行组合在一起,并将这些组彼此分开。

PEP8 有几个在代码中插入空行的准则:它规定你应该用两个空行分隔函数,用两个空行分隔类,用一个空行分隔类内的方法。Black 通过在代码中插入或删除空行来自动遵循这些规则,将代码:

代码语言:javascript复制
$1 # NOclass ExampleClass:
         def exampleMethod1():
             pass
         def exampleMethod2():
             pass
     def exampleFunction():
         pass

。。。到这段代码中:

代码语言:javascript复制
class ExampleClass: # YES
         def exampleMethod1():
             pass

         def exampleMethod2():
             pass

     def exampleFunction():
         pass

垂直间距示例

Black 不能做的是决定你的函数、方法或全局范围内的空行应该在哪里。将那些行组合在一起是程序员的主观决定。

例如,让我们看看 Django Web 应用框架中的validators.py中的EmailValidator类。你没必要去理解这个代码是如何工作的。但是请注意空行是如何将__call__()方法的代码分成四组的:

代码语言:javascript复制
`--snip--`
    def __call__(self, value):
    1 if not value or '@' not in value:
            raise ValidationError(self.message, code=self.code)

    2 user_part, domain_part = value.rsplit('@', 1)

    3 if not self.user_regex.match(user_part):
            raise ValidationError(self.message, code=self.code)

    4 if (domain_part not in self.domain_whitelist and
                not self.validate_domain_part(domain_part)):
            # Try for possible IDN domain-part
            try:
                domain_part = punycode(domain_part)
            except UnicodeError:
                pass
            else:
                if self.validate_domain_part(domain_part):
                    return
            raise ValidationError(self.message, code=self.code)
`--snip--`

尽管没有注释来描述这部分代码,但是空白行表明这些组在概念上是不同的。第一组 1 检查value参数中的@符号。这个任务与第二组 2 的任务不同,第二组将value中的电子邮件地址字符串拆分成两个新变量user_partdomain_part。第三组 3 和第四组 4 分别使用这些变量来验证电子邮件地址的用户和域两个部分是否合法。

虽然第四组有 11 行,远远多于其他组,但它们都是验证电子邮件地址域的任务。如果您认为该任务确实由多个子任务组成,您可以插入空行来分隔它们。

Django 这一部分的程序员决定域验证行应该都属于一个组,但是其他程序员可能不同意。因为这是主观的,所以 Black 不会修改函数或方法中的垂直间距。

垂直间距最佳实践

Python 的一个鲜为人知的特性是,可以使用分号在一行中分隔多个语句。这意味着下面两行:

代码语言:javascript复制
print('What is your name?')
name = input()

。。。如果用分号隔开,可以写在同一行上:

代码语言:javascript复制
print('What is your name?'); name = input()

就像使用逗号一样,分号前不要加空格,分号后加一个空格。

对于以冒号结尾的语句,如ifwhilefordefclass语句,使用单行块,如本例中对print()的调用:

代码语言:javascript复制
if name == 'Alice':
    print('Hello, Alice!')

。。。可以和它的if语句写在同一行:

代码语言:javascript复制
if name == 'Alice': print('Hello, Alice!')

但是仅仅因为 Python 允许在同一行中包含多个语句并不意味着这是一个好的示例。这会导致代码行太宽,一行代码中的内容太多。Black 将这些语句拆分成单独的行。

类似地,您可以用一条import语句导入多个模块:

代码语言:javascript复制
import math, os, sys

即便如此,PEP8 建议您将该语句拆分为每个模块一个import语句:

代码语言:javascript复制
import math
import os
import sys

如果您为导入模块编写单独的行,当您在使用版本控制系统的差异工具中比较更改时,您将更容易发现导入模块的变动。(版本控制系统,如 Git,将在第 12 章中介绍。)

PEP8 还建议将import语句按以下顺序分成三组:

  1. Python 标准库中的模块,如mathossys
  2. 第三方模块,如 Selenium、Requests 或 Django
  3. 作为程序一部分的本地模块

这些准则是可选的,Black 不会改变代码的import语句的格式。

Black:不妥协的代码格式化程序

Black 会自动格式化您的的代码.py文件。虽然你应该理解本章中的格式规则,但是 Black 可以为你做所有定制的样式。如果你正在和其他人一起做一个编码项目,你可以通过自定义 Black 格式化代码风格来降低和其他人的沟通成本。

你不能改变 Black 遵循的许多规则,这就是为什么它被描述为“不妥协的代码格式化程序”事实上,这个工具的名字来源于亨利·福特关于他提供给顾客的汽车颜色选择的名言:“你可以要任何你想要的颜色,只要是 Black 的。”

我刚刚描述了 Black 使用的确切风格;你可以在black.readthedocs.io/en/stable/the_black_code_style找到布莱克的完整风格指南。

安装 Black

使用 Python 自带的pip工具安装 Black。在 Windows 中,通过打开命令提示符窗口并输入以下内容来完成此操作:

代码语言:javascript复制
C:UsersAl>python -m pip install --user black

在 MacOS 和 Linux 上,打开一个终端窗口,输入python3而不是python(本书中所有使用python的指令都应该这样做):

代码语言:javascript复制
Als-MacBook-Pro:~ al$ python3 -m pip install --user black

-m选项告诉 Python 将pip模块作为应用运行,一些 Python 模块就是这样设置的。通过运行python -m black测试安装是否成功。你应该看到消息No paths given. Nothing to do.而不是No module named black.

从命令行运行 Black

您可以从命令提示符或终端窗口对任何 Python 文件运行 Black。此外,您的 IDE 或代码编辑器可以在后台运行 Black。你可以在布莱克的主页github/psf/black上找到让 Black 使用 Jupyter Notebook、Visual Studio Code、PyCharm 和其他编辑器的说明。

假设您想要自动格式化一个名为yourScript.py的文件。在 Windows 的命令行中,运行以下命令(在 MacOS 和 Linux 上,使用python3命令而不是python

):

代码语言:javascript复制
C:UsersAl>python -m black yourScript.py

运行该命令后,yourScript.py的内容将根据 Black 的样式指南进行格式化。

您的PATH环境变量可能已经设置为直接运行 Black,在这种情况下,您只需输入以下内容即可格式化yourScript.py :

代码语言:javascript复制
C:UsersAl>black yourScript.py

如果你想在一个文件夹中所有的.py文件跑一次 Black,指定单个文件夹而不是单个文件。以下 Windows 示例格式化C:yourPythonFiles文件夹中的每个文件,包括其子文件夹:

代码语言:javascript复制
C:UsersAl>python -m black C:yourPythonFiles

如果您的项目包含多个 Python 文件,并且您不想为每个文件输入命令,则指定文件夹非常有用。

尽管 Black 对如何格式化代码有相当严格的要求,但接下来的三个小节描述了一些你可以改变的选项。要查看 Black 提供的全部选项,请运行python -m black --help

调整 Black 行长度设置

Python 代码的标准行长度为 80 个字符。80 字符行的历史可以追溯到 20 世纪 20 年代穿孔卡计算时代,当时 IBM 推出了 80 列 12 行的穿孔卡。在接下来的几十年里,打印机、显示器和命令行窗口都保留了 80 列的标准。

但在 21 世纪,高分辨率屏幕可以显示超过 80 个字符宽的文本。较长的行长度可以让您不必垂直滚动来查看文件。较短的行长度可以防止过多的代码挤在一行上,并允许您并排比较两个源代码文件,而不必水平滚动。

Black 使用默认的每行 88 个字符,这是相当随意的,因为它比标准的 80 个字符多 10%。我倾向于使用 120 个字符。例如,要告诉 Black 使用 120 个字符的行长度限制来格式化您的代码,请使用-l 120(这是小写字母L而不是数字 1)在命令行选项。在 Windows 上,该命令如下所示:

代码语言:javascript复制
C:UsersAl>python -m black -l 120 yourScript.py

无论您为项目选择什么样的行长度限制,所有的.py文件应该使用相同的限制。

禁用 Black 的双引号字符串设置

Black 自动将代码中的任何字符串字面值从使用单引号更改为双引号,除非字符串包含双引号字符,在这种情况下,它使用单引号。例如,假设yourScript.py包含以下内容:

代码语言:javascript复制
a = 'Hello'
b = "Hello"
c = 'Al's cat, Zophie.'
d = 'Zophie said, "Meow"'
e = "Zophie said, "Meow""
f = '''Hello''' 

yourScript.py上运行 Black 会将它格式化成这样:

代码语言:javascript复制
a = "Hello" # 1
b = "Hello"
c = "Al's cat, Zophie."
d = 'Zophie said, "Meow"' # 2
e = 'Zophie said, "Meow"'
f = """Hello""" # 3

Black 对双引号的偏好使您的 Python 代码看起来类似于用其他编程语言编写的代码,这些语言通常对字符串字面值使用双引号。注意变量abc的字符串使用双引号。变量d的字符串保留其原来的单引号,以避免转义字符串 2 中的任何双引号。注意,对于 Python 的三引号多行字符串 3 ,Black 也使用双引号。

但是如果您希望 Black 保留您编写的字符串字面值,并且不改变使用的引号的类型,那么传递给它-S命令行选项。(注意S是大写的。)例如,在 Windows 中对原始的yourScript.py文件运行 Black 会产生以下输出:

代码语言:javascript复制
C:UsersAl>python –m black -S yourScript.py
All done!
1 file left unchanged.

您也可以在同一命令中同时使用-l线长度限制和-S选项来限制引用字符串的转换:

代码语言:javascript复制
C:UsersAl>python –m black –l 120 -S yourScript.py
预览 Black 将做出的更改

虽然 Black 不会重命名您的变量或改变您的程序的行为,但是您可能不喜欢 Black 所做的样式改变。如果您想坚持原来的格式,您可以对您的源代码使用版本控制或者维护您自己的备份。或者,您可以通过使用--diff命令行选项运行 Black 来预览 Black 将做出的更改,而不会让它实际更改您的文件。在 Windows 中,它看起来像这样:

代码语言:javascript复制
C:UsersAl>python -m black --diff yourScript.py

这个命令以版本控制软件通常使用的diff格式输出差异,但是它通常是可读的。例如,如果yourScript.py包含行weights=[42.0,3.1415,2.718],运行--diff选项将显示以下结果:

代码语言:javascript复制
C:UsersAl>python -m black --diff yourScript.py
--- yourScript.py       2020-12-07 02:04:23.141417  0000
    yourScript.py       2020-12-07 02:08:13.893578  0000
@@ -1  1,2 @@
-weights=[42.0,3.1415,2.718]
 weights = [42.0, 3.1415, 2.718]

减号表示 Black 将删除线weights= [42.0,3.1415,2.718]并替换为前缀为加号的线:weights = [42.0, 3.1415, 2.718]。请记住,一旦您运行 Black 来更改您的源代码文件,就没有办法撤销这种更改。在运行 Black 之前,您需要备份源代码或使用版本控制软件,如 Git。

禁用部分代码的 Black

尽管 Black 很棒,但您可能不希望它格式化您代码的某些部分。例如,每当我排列多个相关的赋值语句时,我都喜欢使用自己的特殊间距,如下例所示:

代码语言:javascript复制
# Set up constants for different time amounts:
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR   = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY    = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK   = 7  * SECONDS_PER_DAY

Black 将删除=赋值操作符前的多余空格,在我看来,这会使它们可读性更差:

代码语言:javascript复制
# Set up constants for different time amounts:
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY

通过添加# fmt: off# fmt: on注释,我们可以告诉 Black 关闭这些行的代码格式,然后恢复代码格式:

代码语言:javascript复制
# Set up constants for different time amounts:
# fmt: off
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR   = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY    = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK   = 7  * SECONDS_PER_DAY
# fmt: on

在这个文件上运行 Black 现在不会影响这两个注释之间的代码格式。

总结

虽然好的格式是看不见的,但是糟糕的格式会使阅读代码变得令人心塞。风格是主观的,但是软件开发领域通常制定什么是好的和差的格式时,同时仍然为个人偏好留有余地。

Python 的语法使得它在风格方面相当灵活。如果你正在写别人永远看不到的代码,你可以想怎么写就怎么写。但是大部分软件开发是协作性的。无论您是与他人合作完成一个项目,还是仅仅想请更有经验的开发人员来评审您的工作,格式化您的代码以适应公认的风格指南都是非常重要的。

在编辑器中格式化你的代码是一项枯燥的任务,你可以用 Black 这样的工具来自动完成。本章介绍了 Black 为提高代码可读性而遵循的几条准则,包括垂直和水平方向的代码间距,以防止代码过于密集而不容易阅读,以及设置每行的长度限制。Black 为您执行这些规则,来降低您和其它合作者的沟通成本。

但是代码风格不仅仅是空格和决定单引号和双引号。例如,选择描述性变量名也是代码可读性的一个关键因素。虽然像 Black 这样的自动化工具可以做出语法决定,比如代码应该有多少间距,但是它们不能理解语义,比如什么是好的变量名。这是你的责任,我们将在下一章讨论这个话题。

译者注:随着 NLP 技术的飞速发着,出现了像 Copilot 等一系列优秀的工具,在让计算机理解语义这些方面都有很大的提升。这些工具是可以协助程序员想象出更适合的变量名的。

0 人点赞