跳到内容

已知与 Black 的差异

本文档列举了 Black 和 Ruff 的格式化程序在代码风格上的已知且有意为之的差异。

有关无意偏差的列表,请参阅问题跟踪器

行尾注释

Black 的首要任务是将整个语句放在一行上,即使它包含行尾注释。在这种情况下,Black 会折叠语句,并将注释移动到折叠语句的末尾

# Input
while (
    cond1  # almost always true
    and cond2  # almost never true
):
    print("Do something")

# Black
while cond1 and cond2:  # almost always true  # almost never true
    print("Do something")

Ruff 像 Prettier 一样,会展开任何包含行尾注释的语句。例如,Ruff 将避免折叠上面代码片段中的 while 测试。这确保了注释保持在其原始位置附近,并保留其原始意图,但代价是保留了额外的垂直空间。

此偏差仅影响未格式化的代码,因为 Ruff 的输出不应偏离已被 Black 格式化的代码。

计算行宽时忽略 Pragma 注释

计算行的宽度时,Pragma 注释(# type# noqa# pyright# pylint 等)将被忽略。这可以防止 Ruff 移动 Pragma 注释,从而修改其含义和行为

有关详细信息,请参阅 Ruff 的 Pragma 注释处理提案

这类似于 Pyink,但与 Black 不同。Black 避免拆分包含 # type 注释的任何行(#997),但在其他情况下避免特殊处理 Pragma 注释。

由于 Ruff 展开了行尾注释,因此 Ruff 也会避免移动以下情况下的 Pragma 注释,其中将 # noqa 移动到行尾会导致它抑制 first()second() 上的错误

# Input
[
    first(),  # noqa
    second()
]

# Black
[first(), second()]  # noqa

# Ruff
[
    first(),  # noqa
    second(),
]

行宽与行长

Ruff 使用行的 Unicode 宽度来确定行是否合适。Black 对字符串使用 Unicode 宽度,对所有其他标记使用字符宽度。Ruff 对标识符和注释使用 Unicode 宽度。

用括号括起来的长嵌套表达式

Black 24 及更高版本用括号括起来函数参数中的长条件表达式和类型注释

# Black
[
    "____________________________",
    "foo",
    "bar",
    (
        "baz"
        if some_really_looooooooong_variable
        else "some other looooooooooooooong value"
    ),
]


def foo(
    i: int,
    x: (
        Loooooooooooooooooooooooong
        | Looooooooooooooooong
        | Looooooooooooooooooooong
        | Looooooong
    ),
    *,
    s: str,
) -> None:
    pass

# Ruff
[
    "____________________________",
    "foo",
    "bar",
    "baz"
    if some_really_looooooooong_variable
    else "some other looooooooooooooong value",
]


def foo(
    i: int,
    x: Loooooooooooooooooooooooong
    | Looooooooooooooooong
    | Looooooooooooooooooooong
    | Looooooong,
    *,
    s: str,
) -> None:
    pass

我们同意 Ruff 的格式化(与 Black 23 匹配)难以阅读,需要改进。但是我们不相信用括号括起来长嵌套表达式是最好的解决方案,尤其是在全面考虑表达式格式化时。这就是为什么我们希望推迟该决定,直到我们探索了其他嵌套表达式格式化样式。有关我们的担忧以及可能的替代方案的概述,请参阅 psf/Black#4123

带有单个多行字符串参数的调用表达式

与 Black 不同,Ruff 保留了调用表达式中单个多行字符串参数的缩进

# Input
call(
  """"
  A multiline
  string
  """
)

dedent(""""
    A multiline
    string
""")

# Black
call(
  """"
  A multiline
  string
  """
)

dedent(
  """"
  A multiline
  string
"""
)


# Ruff
call(
  """"
  A multiline
  string
  """
)

dedent(""""
    A multiline
    string
""")

Black 打算发布一个类似风格的更改作为 2024 风格的一部分,该风格始终删除缩进。事实证明,此更改具有太大的破坏性,无法证明其改进格式的情况。Ruff 引入了保留缩进的新启发式方法。我们相信这是一个很好的折衷方案,可以改进格式,但最大限度地减少对用户的干扰。

块开头的空行

Black 24 及更高版本允许块开头有空行,而 Ruff 始终删除它们

# Black
if x:

  a = 123

# Ruff
if x:
  a = 123

目前,我们担心允许块开头的空行会导致在重构或移动代码时出现意外的空行。但是,我们将在稍后使用改进的启发式方法考虑采用 Black 的格式。样式更改在 #9745 中跟踪。

F 字符串

Ruff 格式化 f 字符串中的表达式部分,而 Black 不会

# Input
f'test{inner   + "nested_string"} including math {5 ** 3 + 10}'

# Black
f'test{inner   + "nested_string"} including math {5 ** 3 + 10}'

# Ruff
f"test{inner + 'nested_string'} including math {5**3 + 10}"

有关格式化样式的更多详细信息,请参阅 f 字符串格式化 部分。

隐式连接字符串

如果整个字符串适合单行,Ruff 会合并隐式连接的字符串

# Input
def test(max_history):
    raise argparse.ArgumentTypeError(
        f"The value of `--max-history {max_history}` " f"is not a positive integer."
    )

# Black
def test(max_history):
    raise argparse.ArgumentTypeError(
        f"The value of `--max-history {max_history}` " f"is not a positive integer."
    )

# Ruff
def test(max_history):
    raise argparse.ArgumentTypeError(
        f"The value of `--max-history {max_history}` is not a positive integer."
    )

Black 的不稳定样式应用相同的格式。

在极少数情况下,Ruff 无法自动合并隐式连接的字符串。在这些情况下,如果隐式连接的字符串格式化为多行,Ruff 将保留

# Input
a = (
    r"aaaaaaa"
    "bbbbbbbbbbbb"
)

# Black
a = r"aaaaaaa" "bbbbbbbbbbbb"

# Ruff
a = (
    r"aaaaaaa"
    "bbbbbbbbbbbb"
)

这确保了与 ISC001 的兼容性(#8272)。

assert 语句

与 Black 不同,Ruff 更喜欢中断消息而不是中断断言,类似于 Ruff 和 Black 更喜欢中断赋值值而不是中断赋值目标

# Input
assert (
    len(policy_types) >= priority + num_duplicates
), f"This tests needs at least {priority+num_duplicates} many types."


# Black
assert (
    len(policy_types) >= priority + num_duplicates
), f"This tests needs at least {priority+num_duplicates} many types."

# Ruff
assert len(policy_types) >= priority + num_duplicates, (
    f"This tests needs at least {priority + num_duplicates} many types."
)

globalnonlocal 名称通过延续符跨多行中断

如果 globalnonlocal 语句包含多个名称,并且超出配置的行宽,Ruff 将使用延续符跨多行中断它们

# Input
global analyze_featuremap_layer, analyze_featuremapcompression_layer, analyze_latencies_post, analyze_motions_layer, analyze_size_model

# Ruff
global \
    analyze_featuremap_layer, \
    analyze_featuremapcompression_layer, \
    analyze_latencies_post, \
    analyze_motions_layer, \
    analyze_size_model

导入上的尾随自有行注释不会移动到下一行

Black 强制导入和尾随自有行注释之间有一个空行。Ruff 将此类注释留在原位

# Input
import os
# comment

import sys

# Black
import os

# comment

import sys

# Ruff
import os
# comment

import sys

等待集合周围的括号不会保留

Black 保留等待集合周围的括号

await ([1, 2, 3])

Ruff 将改为删除它们

await [1, 2, 3]

这与其他等待表达式的格式更一致:Ruff 和 Black 都删除例如 await (1) 周围的括号,仅在语法上需要时保留它们,例如 await (x := 1)

属性访问中的隐式字符串连接

给定以下未格式化的代码

print("aaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaa".format(bbbbbbbbbbbbbbbbbb + bbbbbbbbbbbbbbbbbb))

在内部,Black 的逻辑将首先展开最外层的 print 调用

print(
    "aaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaa".format(bbbbbbbbbbbbbbbbbb + bbbbbbbbbbbbbbbbbb)
)

由于参数仍然太长,Black 将然后按具有最高拆分优先级的运算符拆分。在这种情况下,Black 在隐式字符串连接上拆分,以生成以下 Black 格式化的代码

print(
    "aaaaaaaaaaaaaaaa"
    "aaaaaaaaaaaaaaaa".format(bbbbbbbbbbbbbbbbbb + bbbbbbbbbbbbbbbbbb)
)

Ruff 在中断行时为隐式连接赋予“较低”优先级。因此,Ruff 将改为将上述内容格式化为

print(
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".format(bbbbbbbbbbbbbbbbbb + bbbbbbbbbbbbbbbbbb)
)

通常,即使隐式连接可以放在一行上,Black 也比 Ruff 更频繁地将隐式字符串连接拆分为多行。Ruff 改为避免拆分此类连接,除非这样做是为适合配置的行宽所必需的。

表达式上的自有行注释不会导致表达式展开

给定一个像这样的表达式

(
    # A comment in the middle
    some_example_var and some_example_var not in some_example_var
)

Black 将注释与 some_example_var 关联,从而将其拆分为两行

(
    # A comment in the middle
    some_example_var
    and some_example_var not in some_example_var
)

Ruff 将改为将注释与整个布尔表达式关联,从而保留初始格式

(
    # A comment in the middle
    some_example_var and some_example_var not in some_example_var
)

元组在展开时用括号括起来

Ruff 倾向于用括号括起来元组(有一些例外),而 Black 倾向于更频繁地删除元组括号。

特别是,Ruff 将始终在展开到多行的元组周围插入括号

# Input
(a, b), (c, d,)

# Black
(a, b), (
    c,
    d,
)

# Ruff
(
    (a, b),
    (
        c,
        d,
    ),
)

这里有一个例外。在 for 循环中,Ruff 和 Black 都将避免插入不必要的括号

# Input
for a, [b, d,] in c:
    pass

# Black
for a, [
    b,
    d,
] in c:
    pass

# Ruff
for a, [
    b,
    d,
] in c:
    pass

单元素元组始终用括号括起来

Ruff 始终在单元素元组周围插入括号,而 Black 在某些情况下会省略它们

# Input
(a, b),

# Black
(a, b),

# Ruff
((a, b),)

在单元素元组周围添加括号可以增加视觉区分度,并有助于避免因多余的尾随逗号而创建的“意外”元组(参见,例如,#17181)。

调用链赋值值周围的括号不会保留

给定

def update_emission_strength():
    (
        get_rgbw_emission_node_tree(self)
        .nodes["Emission"]
        .inputs["Strength"]
        .default_value
    ) = (self.emission_strength * 2)

Black 将保留 (self.emission_strength * 2) 中的括号,而 Ruff 将删除它们。

Black 和 Ruff 都会在更简单的赋值中删除此类括号,例如

# Input
def update_emission_strength():
    value = (self.emission_strength * 2)

# Black
def update_emission_strength():
    value = self.emission_strength * 2

# Ruff
def update_emission_strength():
    value = self.emission_strength * 2

在某些情况下,调用链调用以不同的方式中断

Black 有时会以不同于 Ruff 的方式中断调用链;特别是,Black 有时会展开链中最后一个调用的参数,如

# Input
df.drop(
    columns=["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
).drop_duplicates().rename(
    columns={
        "a": "a",
    }
).to_csv(path / "aaaaaa.csv", index=False)

# Black
df.drop(
    columns=["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
).drop_duplicates().rename(
    columns={
        "a": "a",
    }
).to_csv(
    path / "aaaaaa.csv", index=False
)

# Ruff
df.drop(
    columns=["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
).drop_duplicates().rename(
    columns={
        "a": "a",
    }
).to_csv(path / "aaaaaa.csv", index=False)

仅当这样做是为适合配置的行宽所必需时,Ruff 才会展开参数。

请注意,Black 不会普遍应用此最后一个调用的参数中断。例如,Black 和 Ruff 都将以相同的方式格式化以下内容

# Input
df.drop(
    columns=["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
).drop_duplicates(a).rename(
    columns={
        "a": "a",
    }
).to_csv(
    path / "aaaaaa.csv", index=False
).other(a)

# Black
df.drop(columns=["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]).drop_duplicates(a).rename(
    columns={
        "a": "a",
    }
).to_csv(path / "aaaaaa.csv", index=False).other(a)

# Ruff
df.drop(columns=["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]).drop_duplicates(a).rename(
    columns={
        "a": "a",
    }
).to_csv(path / "aaaaaa.csv", index=False).other(a)

类似地,在某些情况下,如果这样做允许表达式适合配置的行宽,Ruff 将比 Black 更积极地折叠复合二进制表达式

# Black
assert AAAAAAAAAAAAAAAAAAAAAA.bbbbbb.fooo(
    aaaaaaaaaaaa=aaaaaaaaaaaa
).ccccc() == (len(aaaaaaaaaa) + 1) * fooooooooooo * (
    foooooo + 1
) * foooooo * len(
    list(foo(bar(4, foo), foo))
)

# Ruff
assert AAAAAAAAAAAAAAAAAAAAAA.bbbbbb.fooo(
    aaaaaaaaaaaa=aaaaaaaaaaaa
).ccccc() == (len(aaaaaaaaaa) + 1) * fooooooooooo * (
    foooooo + 1
) * foooooo * len(list(foo(bar(4, foo), foo)))

面向 Python 3.8 或更早版本的单个 with

与 Black 不同,Ruff 对带有单个上下文管理器的 with 语句使用与 whileif 和其他复合语句相同的布局

# Input
def run(data_path, model_uri):
    with pyspark.sql.SparkSession.builder.config(
        key="spark.python.worker.reuse", value=True
    ).config(key="spark.ui.enabled", value=False).master(
        "local-cluster[2, 1, 1024]"
    ).getOrCreate():
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))

# Black
def run(data_path, model_uri):
    with pyspark.sql.SparkSession.builder.config(
        key="spark.python.worker.reuse", value=True
    ).config(key="spark.ui.enabled", value=False).master(
        "local-cluster[2, 1, 1024]"
    ).getOrCreate():
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))

# Ruff
def run(data_path, model_uri):
    with (
        pyspark.sql.SparkSession.builder.config(
            key="spark.python.worker.reuse", value=True
        )
        .config(key="spark.ui.enabled", value=False)
        .master("local-cluster[2, 1, 1024]")
        .getOrCreate()
    ):
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))

Ruff 的格式与格式化其他复合语句的格式匹配

def test():
    if (
        pyspark.sql.SparkSession.builder.config(
            key="spark.python.worker.reuse", value=True
        )
        .config(key="spark.ui.enabled", value=False)
        .master("local-cluster[2, 1, 1024]")
        .getOrCreate()
    ):
        # ignore spark log output
        spark.sparkContext.setLogLevel("OFF")
        print(score_model(spark, data_path, model_uri))

with 语句中的最后一个上下文管理器可以折叠到一行上

当使用带有多个未用括号括起来的上下文管理器的 with 语句时,如果这样做允许 with 语句适合配置的行宽,Ruff 可能会将最后一个上下文管理器折叠到一行上。

与此同时,Black 倾向于以略有不同的方式中断最后一个上下文管理器,如以下示例中所示

# Black
with tempfile.TemporaryDirectory() as d1:
    symlink_path = Path(d1).joinpath("testsymlink")
    with tempfile.TemporaryDirectory(dir=d1) as d2, tempfile.TemporaryDirectory(
        dir=d1
    ) as d4, tempfile.TemporaryDirectory(dir=d2) as d3, tempfile.NamedTemporaryFile(
        dir=d4
    ) as source_file, tempfile.NamedTemporaryFile(
        dir=d3
    ) as lock_file:
        pass

# Ruff
with tempfile.TemporaryDirectory() as d1:
    symlink_path = Path(d1).joinpath("testsymlink")
    with tempfile.TemporaryDirectory(dir=d1) as d2, tempfile.TemporaryDirectory(
        dir=d1
    ) as d4, tempfile.TemporaryDirectory(dir=d2) as d3, tempfile.NamedTemporaryFile(
        dir=d4
    ) as source_file, tempfile.NamedTemporaryFile(dir=d3) as lock_file:
        pass

当面向 Python 3.9 或更高版本时,将在上下文管理器周围插入括号,以便更清晰地跨多行中断,如

with tempfile.TemporaryDirectory() as d1:
    symlink_path = Path(d1).joinpath("testsymlink")
    with (
        tempfile.TemporaryDirectory(dir=d1) as d2,
        tempfile.TemporaryDirectory(dir=d1) as d4,
        tempfile.TemporaryDirectory(dir=d2) as d3,
        tempfile.NamedTemporaryFile(dir=d4) as source_file,
        tempfile.NamedTemporaryFile(dir=d3) as lock_file,
    ):
        pass

保留单元素列表周围的括号

Ruff 保留列表元素周围的至少一个括号,即使该列表仅包含一个元素。另一方面,Black 2025 或更高版本会删除单元素列表的括号,如果它们不是多行的并且这样做不会更改语义

# Input
items = [(True)]
items = [(((((True)))))]
items = {(123)}

# Black
items = [True]
items = [True]
items = {123}

# Ruff
items = [(True)]
items = [(True)]
items = {(123)}