设计与历史常见问题

为什么 Python 使用缩进来分组语句?

Guido van Rossum 认为使用缩进进行分组非常优雅,并且为普通 Python 程序的清晰度做出了很大贡献。大多数人在一段时间后都会爱上这个功能。

由于没有 begin/end 括号,因此解析器和人类读者感知的分组之间不会存在分歧。有时 C 程序员会遇到如下代码片段

if (x <= y)
        x++;
        y--;
z++;

只有 x++ 语句在条件为真时执行,但缩进使许多人认为情况并非如此。即使是有经验的 C 程序员有时也会长时间盯着它,想知道为什么即使对于 x > y 也递减了 y

由于没有 begin/end 括号,Python 更不容易发生编码风格冲突。在 C 中,有很多不同的方法来放置大括号。在习惯了使用特定样式读取和编写代码后,当以不同的样式读取(或被要求编写)时,会感到有些不安。

许多编码风格将 begin/end 括号放在单独的一行上。这使得程序变得更长,并浪费了宝贵的屏幕空间,从而更难获得程序的良好概览。理想情况下,一个函数应该适合一个屏幕(例如,20-30 行)。 20 行 Python 可以比 20 行 C 做更多的工作。这不仅仅是因为缺少 begin/end 括号——缺少声明和高级数据类型也是原因——但基于缩进的语法当然有帮助。

为什么我在简单的算术运算中得到奇怪的结果?

请参阅下一个问题。

为什么浮点计算如此不精确?

用户经常会对如下结果感到惊讶

>>> 1.2 - 1.0
0.19999999999999996

并认为这是 Python 中的一个 bug。事实并非如此。这与 Python 关系不大,而与底层平台如何处理浮点数关系更大。

CPython 中的 float 类型使用 C double 进行存储。 float 对象的值以固定精度(通常为 53 位)的二进制浮点数存储,Python 使用 C 操作(而 C 操作又依赖于处理器中的硬件实现)来执行浮点运算。这意味着就浮点运算而言,Python 的行为与包括 C 和 Java 在内的许多流行语言的行为类似。

许多可以用十进制表示法轻松写出的数字不能用二进制浮点数精确表示。例如,之后

>>> x = 1.2

x 存储的值是十进制值 1.2 的(非常好的)近似值,但并不完全等于它。在典型机器上,实际存储的值是

1.0011001100110011001100110011001100110011001100110011 (binary)

这正好是

1.1999999999999999555910790149937383830547332763671875 (decimal)

53 位的典型精度为 Python 浮点数提供了 15-16 位十进制数字的精度。

有关更完整的解释,请参阅 Python 教程中的 浮点算术 章节。

为什么 Python 字符串是不可变的?

有几个优点。

一个优点是性能:知道字符串是不可变的意味着我们可以在创建时为其分配空间,并且存储需求是固定的且不变的。这也是元组和列表之间区别的原因之一。

另一个优点是 Python 中的字符串被认为是与数字一样的“基本”类型。任何活动都不会将值 8 更改为其他任何值,在 Python 中,任何活动都不会将字符串“eight”更改为其他任何值。

为什么必须在方法定义和调用中显式使用 'self'?

这个想法是从 Modula-3 中借鉴的。事实证明它非常有用,原因有很多。

首先,更明显的是,您正在使用方法或实例属性,而不是局部变量。读取 self.xself.meth() 可以清楚地表明正在使用实例变量或方法,即使您不熟悉类定义。在 C++ 中,您可以通过缺少局部变量声明来判断(假设全局变量很少或容易识别)——但在 Python 中,没有局部变量声明,因此您必须查找类定义才能确定。一些 C++ 和 Java 编码标准要求实例属性具有 m_ 前缀,因此这种显式性在这些语言中也很有用。

其次,这意味着如果您想从特定类显式引用或调用该方法,则不需要任何特殊的语法。在 C++ 中,如果您想使用在派生类中被覆盖的基类方法,则必须使用 :: 运算符——在 Python 中,您可以编写 baseclass.methodname(self, <argument list>)。这对于 __init__() 方法特别有用,并且通常在派生类方法想要扩展同名的基类方法,因此必须以某种方式调用基类方法的情况下非常有用。

最后,对于实例变量,它解决了赋值的语法问题:由于 Python 中的局部变量(根据定义!)是在函数体中被赋值的变量(并且没有显式声明为全局变量),因此必须有一种方法来告诉解释器赋值的目的是赋值给实例变量而不是局部变量,并且最好是语法的(为了效率)。C++ 通过声明来实现这一点,但 Python 没有声明,并且仅仅为此目的引入声明将是一种遗憾。使用显式 self.var 可以很好地解决这个问题。类似地,对于使用实例变量,必须编写 self.var 意味着方法内部对非限定名称的引用不必搜索实例的目录。换句话说,局部变量和实例变量位于两个不同的命名空间中,您需要告诉 Python 使用哪个命名空间。

为什么我不能在表达式中使用赋值?

从 Python 3.8 开始,您可以!

使用海象运算符 := 的赋值表达式会在表达式中赋值给变量

while chunk := fp.read(200):
   print(chunk)

有关更多信息,请参见 PEP 572

为什么 Python 对某些功能使用方法(例如 list.index()),而对其他功能使用函数(例如 len(list))?

正如 Guido 所说

(a) 对于某些操作,前缀表示法比后缀表示法更易读——前缀(和中缀!)操作在数学中有着悠久的传统,数学喜欢视觉效果有助于数学家思考问题的符号。将诸如 x*(a+b) 之类的公式轻松重写为 x*a + x*b,并将其与使用原始 OO 表示法执行相同操作的笨拙性进行比较。

(b) 当我读到代码 `len(x)` 时,我知道它是在请求某个事物的长度。这告诉我两件事:结果是一个整数,并且参数是某种容器。相反,当我读到 `x.len()` 时,我必须已经知道 `x` 是某种实现了接口或继承自具有标准 `len()` 的类的容器。 看看我们有时会遇到的困惑,当一个没有实现映射的类有一个 `get()` 或 `keys()` 方法,或者一个不是文件的东西有一个 `write()` 方法时。

https://mail.python.org/pipermail/python-3000/2006-November/004643.html

为什么 join() 是一个字符串方法而不是列表或元组方法?

从 Python 1.6 开始,字符串变得更像其他标准类型,当时添加了方法,这些方法提供了与使用字符串模块的函数一直可用的相同功能。 大多数这些新方法已被广泛接受,但似乎让一些程序员感到不舒服的方法是

", ".join(['1', '2', '4', '8', '16'])

这会得到结果

"1, 2, 4, 8, 16"

关于这种用法,通常有两种常见的反对意见。

第一个意见是:“使用字符串字面量(字符串常量)的方法看起来真的很丑”,对此的回答是,它可能看起来是这样,但字符串字面量只是一个固定值。 如果允许在绑定到字符串的名称上使用方法,那么没有逻辑上的理由使它们在字面量上不可用。

第二个反对意见通常是:“我实际上是在告诉一个序列,用一个字符串常量将其成员连接在一起”。 可惜,你不是。 出于某种原因,似乎对于将 split() 作为字符串方法没有那么大的困难,因为在这种情况下,很容易看出

"1, 2, 4, 8, 16".split(", ")

是对字符串字面量的指令,要求返回由给定分隔符(或默认情况下,任意运行的空白字符)分隔的子字符串。

join() 是一个字符串方法,因为在使用它时,您是在告诉分隔符字符串迭代字符串序列,并在相邻元素之间插入自身。 此方法可以与任何遵守序列对象规则的参数一起使用,包括您自己可能定义的任何新类。 字节和字节数组对象也存在类似的方法。

异常有多快?

如果没有引发异常,try/except 块非常高效。 实际捕获异常是昂贵的。 在 2.0 之前的 Python 版本中,通常使用这种习惯用法

try:
    value = mydict[key]
except KeyError:
    mydict[key] = getvalue(key)
    value = mydict[key]

只有当您期望字典几乎总是具有该键时,这才有意义。 如果不是这种情况,您可以像这样编码

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

对于这种情况,您还可以使用 value = dict.setdefault(key, getvalue(key)),但前提是 getvalue() 调用足够便宜,因为它在所有情况下都会被求值。

为什么 Python 中没有 switch 或 case 语句?

通常,结构化的 switch 语句在表达式具有特定值或一组值时执行一个代码块。 从 Python 3.10 开始,可以使用 match ... case 语句轻松匹配字面值或命名空间中的常量。 较旧的替代方法是一系列的 if... elif... elif... else

对于需要从大量可能性中进行选择的情况,您可以创建一个将 case 值映射到要调用的函数的字典。 例如

functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1}

func = functions[value]
func()

对于调用对象的方法,您可以通过使用 getattr() 内置函数来检索具有特定名称的方法,从而进一步简化

class MyVisitor:
    def visit_a(self):
        ...

    def dispatch(self, value):
        method_name = 'visit_' + str(value)
        method = getattr(self, method_name)
        method()

建议您为方法名称使用前缀,例如本例中的 visit_。 如果没有这样的前缀,并且值来自不受信任的来源,则攻击者将能够调用您对象上的任何方法。

模仿具有 fallthrough 的 switch,就像 C 的 switch-case-default 一样,是可能的,但难度更大,并且不太需要。

为什么不能在解释器中模拟线程,而不是依赖于特定于操作系统的线程实现?

答案 1:不幸的是,解释器为每个 Python 堆栈帧至少推送一个 C 堆栈帧。 此外,扩展可以在几乎随机的时刻回调到 Python。 因此,完整的线程实现需要对 C 的线程支持。

答案 2:幸运的是,有 Stackless Python,它具有完全重新设计的解释器循环,可以避免 C 堆栈。

为什么 lambda 表达式不能包含语句?

Python lambda 表达式不能包含语句,因为 Python 的语法框架无法处理嵌套在表达式中的语句。 但是,在 Python 中,这不是一个严重的问题。 与其他语言中的 lambda 形式(它们添加了功能)不同,Python lambda 只是一个简写符号,如果您懒得定义函数。

函数在 Python 中已经是第一类对象,并且可以在局部作用域中声明。 因此,使用 lambda 而不是局部定义函数的唯一优点是您无需为函数发明一个名称 - 但这只是一个局部变量,函数对象(它与 lambda 表达式产生的对象完全相同)被分配给它!

可以将 Python 编译为机器代码、C 或其他某种语言吗?

Cython 将带有可选注释的修改版 Python 编译为 C 扩展。Nuitka 是一个新兴的 Python 到 C++ 代码的编译器,旨在支持完整的 Python 语言。

Python 如何管理内存?

Python 内存管理的细节取决于实现。 Python 的标准实现 CPython 使用引用计数来检测不可访问的对象,并使用另一种机制来收集引用循环,定期执行循环检测算法,查找不可访问的循环并删除涉及的对象。gc 模块提供了执行垃圾收集、获取调试统计信息和调整收集器参数的函数。

但是,其他实现(例如 JythonPyPy)可以依赖于不同的机制,例如完全成熟的垃圾收集器。 如果您的 Python 代码依赖于引用计数实现的行为,则这种差异可能会导致一些细微的移植问题。

在某些 Python 实现中,以下代码(在 CPython 中没问题)可能会耗尽文件描述符

for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

实际上,使用 CPython 的引用计数和析构器方案,每次向 f 进行新赋值都会关闭上一个文件。 但是,使用传统的 GC,这些文件对象只会在不同的且可能较长的时间间隔内被收集(和关闭)。

如果您想编写可以与任何 Python 实现一起使用的代码,您应该显式关闭文件或使用 with 语句;无论内存管理方案如何,这都将起作用

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

为什么 CPython 不使用更传统的垃圾收集方案?

首先,这不是 C 标准功能,因此不可移植。(是的,我们知道 Boehm GC 库。它具有适用于大多数常见平台的汇编代码片段,而不是全部,虽然它在很大程度上是透明的,但它并非完全透明;需要补丁才能使 Python 与之一起工作。)

当 Python 嵌入到其他应用程序中时,传统的 GC 也会成为问题。 虽然在独立的 Python 中,用 GC 库提供的版本替换标准 malloc()free() 很好,但嵌入 Python 的应用程序可能希望拥有自己的 malloc()free() 的替代品,并且可能不想要 Python 的替代品。 目前,CPython 可以与任何正确实现 malloc()free() 的东西一起工作。

为什么 CPython 退出时并非所有内存都被释放?

当 Python 退出时,从 Python 模块的全局命名空间引用的对象并非总是被释放。 如果存在循环引用,则可能会发生这种情况。 还有某些由 C 库分配的内存无法释放(例如,像 Purify 这样的工具会抱怨这些内存)。 但是,Python 在退出时会积极清理内存,并尝试销毁每个对象。

如果您想强制 Python 在释放时删除某些内容,请使用 atexit 模块来运行一个强制这些删除的函数。

为什么有单独的元组和列表数据类型?

列表和元组在许多方面都很相似,但通常在根本不同的方式中使用。元组可以被认为类似于 Pascal 的 records 或 C 的 structs;它们是相关数据的少量集合,这些数据可能具有不同的类型,并且作为一个组进行操作。例如,笛卡尔坐标通常表示为包含两个或三个数字的元组。

另一方面,列表更像其他语言中的数组。它们倾向于保存数量可变的对象,所有这些对象都具有相同的类型,并且逐个进行操作。例如,os.listdir('.') 返回一个字符串列表,表示当前目录中的文件。如果向目录中添加一两个文件,通常不会破坏对该输出进行操作的函数。

元组是不可变的,这意味着一旦创建了元组,就无法将其任何元素替换为新值。列表是可变的,这意味着您始终可以更改列表的元素。只有不可变的元素可以用作字典的键,因此只有元组而不是列表可以用作键。

CPython 中列表是如何实现的?

CPython 的列表实际上是可变长度的数组,而不是 Lisp 风格的链表。该实现使用指向其他对象的连续引用数组,并在列表头结构中保留指向此数组的指针和数组的长度。

这使得索引列表 a[i] 的操作成本与列表的大小或索引的值无关。

当追加或插入项目时,引用的数组会调整大小。应用了一些巧妙的方法来提高重复追加项目的性能;当必须增长数组时,会分配一些额外的空间,以便接下来的几次不需要实际调整大小。

CPython 中字典是如何实现的?

CPython 的字典实现为可调整大小的哈希表。与 B 树相比,这在大多数情况下为查找(迄今为止最常见的操作)提供了更好的性能,并且实现更简单。

字典的工作原理是使用 hash() 内置函数计算存储在字典中的每个键的哈希码。哈希码因键和每个进程的种子而异;例如,'Python' 可以哈希到 -539294296,而 'python'(一个仅相差一位的字符串)可以哈希到 1142331976。然后,使用哈希码计算内部数组中存储值的位置。假设您存储的键都具有不同的哈希值,这意味着字典检索键需要恒定的时间——O(1),用大 O 表示法表示。

为什么字典键必须是不可变的?

字典的哈希表实现使用从键值计算出的哈希值来查找键。如果键是可变对象,则其值可能会更改,因此其哈希值也可能会更改。但是,由于更改键对象的人无法知道它被用作字典键,因此它无法在字典中移动条目。然后,当您尝试在字典中查找同一对象时,由于其哈希值不同,因此将找不到它。如果您尝试查找旧值,也找不到它,因为在该哈希桶中找到的对象的值将不同。

如果希望使用列表索引字典,只需先将列表转换为元组即可;函数 tuple(L) 创建一个与列表 L 具有相同条目的元组。元组是不可变的,因此可以用作字典键。

一些已被提出的不可接受的解决方案

  • 通过地址(对象 ID)哈希列表。这不起作用,因为如果构造一个具有相同值的新列表,则将找不到它;例如:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])
    

    会引发 KeyError 异常,因为第二行中使用的 [1, 2] 的 id 与第一行中的 id 不同。换句话说,应该使用 == 而不是 is 来比较字典键。

  • 在将列表用作键时进行复制。这不起作用,因为列表作为可变对象,可能包含对其自身的引用,然后复制代码将陷入无限循环。

  • 允许将列表作为键,但告诉用户不要修改它们。当您忘记或意外修改列表时,这将允许程序中出现一类难以跟踪的错误。它还使字典的一个重要不变量失效:d.keys() 中的每个值都可用作字典的键。

  • 一旦列表用作字典键,就将其标记为只读。问题在于,不仅顶层对象可能会更改其值;您可以使用包含列表的元组作为键。将任何内容作为键输入字典将需要将所有可从该处访问的对象标记为只读——并且同样,自引用对象可能导致无限循环。

如果需要,有一种技巧可以解决此问题,但请自行承担风险:您可以将可变结构包装在具有 __eq__()__hash__() 方法的类实例中。然后,您必须确保字典(或其他基于哈希的结构)中所有此类包装器对象的哈希值在该对象位于字典(或其他结构)中时保持固定。

class ListWrapper:
    def __init__(self, the_list):
        self.the_list = the_list

    def __eq__(self, other):
        return self.the_list == other.the_list

    def __hash__(self):
        l = self.the_list
        result = 98767 - len(l)*555
        for i, el in enumerate(l):
            try:
                result = result + (hash(el) % 9999999) * 1001 + i
            except Exception:
                result = (result % 7777777) + i * 333
        return result

请注意,列表的某些成员可能不可哈希,并且还可能发生算术溢出,这使得哈希计算变得复杂。

此外,必须始终满足以下条件:如果 o1 == o2(即 o1.__eq__(o2) is True),则 hash(o1) == hash(o2)(即,o1.__hash__() == o2.__hash__()),无论对象是否在字典中。如果您未能满足这些限制,字典和其他基于哈希的结构将会出现错误行为。

ListWrapper 的情况下,无论何时包装器对象在字典中,都不能更改包装的列表以避免异常。除非您准备好认真考虑要求以及未正确满足要求造成的后果,否则不要这样做。请注意您的安全。

为什么 list.sort() 不返回排序后的列表?

在性能至关重要的情况下,仅为了对列表进行排序而复制列表会很浪费。因此,list.sort() 对列表进行原地排序。为了提醒您注意这一事实,它不返回排序后的列表。这样,当您需要排序后的副本,但又需要保留未排序的版本时,您就不会被误导而意外覆盖列表。

如果希望返回一个新列表,请改用内置的 sorted() 函数。此函数从提供的可迭代对象创建一个新列表,对其进行排序并返回。例如,以下是如何按排序顺序迭代字典的键

for key in sorted(mydict):
    ...  # do whatever with mydict[key]...

如何在 Python 中指定和强制执行接口规范?

由 C++ 和 Java 等语言提供的模块的接口规范描述了模块的方法和函数的原型。许多人认为,在大型程序的构建中,对接口规范进行编译时强制执行是有帮助的。

Python 2.6 添加了一个 abc 模块,可让您定义抽象基类 (ABC)。然后,您可以使用 isinstance()issubclass() 来检查实例或类是否实现了特定的 ABC。collections.abc 模块定义了一组有用的 ABC,例如 IterableContainerMutableMapping

对于 Python 而言,通过对组件进行适当的测试规范,可以获得接口规范的许多优点。

一个模块的良好测试套件既可以提供回归测试,又可以作为模块接口规范和一组示例。许多 Python 模块可以作为脚本运行以提供简单的“自检”。即使是使用复杂外部接口的模块,通常也可以通过使用外部接口的简单“桩”仿真来进行隔离测试。可以使用 doctestunittest 模块或第三方测试框架来构建详尽的测试套件,以测试模块中的每一行代码。

适当的测试规范可以帮助使用 Python 构建大型复杂应用程序,就像使用接口规范一样。事实上,它可能更好,因为接口规范无法测试程序的某些属性。例如,list.append() 方法应该将新元素添加到某个内部列表的末尾;接口规范无法测试您的 list.append() 实现是否真的能正确地执行此操作,但在测试套件中检查此属性是很简单的。

编写测试套件非常有帮助,您可能希望设计代码以使其易于测试。一种越来越流行的技术,即测试驱动开发,要求首先编写测试套件的部分内容,然后再编写任何实际代码。当然,Python 允许您马虎并不编写测试用例。

为什么没有 goto 语句?

在 20 世纪 70 年代,人们意识到不受限制的 goto 可能导致难以理解和修改的混乱“意大利面条”代码。在高级语言中,只要有分支(在 Python 中,使用 if 语句和 or, and, 和 if/else 表达式)和循环(使用 whilefor 语句,可能包含 continuebreak)的方式,它也是不需要的。

还可以使用异常来提供一个“结构化的 goto”,即使跨函数调用也能工作。许多人认为,异常可以方便地模拟 C、Fortran 和其他语言的 gogoto 结构的所有合理用法。例如

class label(Exception): pass  # declare a label

try:
    ...
    if condition: raise label()  # goto label
    ...
except label:  # where to goto
    pass
...

这不允许您跳到循环的中间,但这通常被认为是滥用 goto。谨慎使用。

为什么原始字符串 (r 字符串) 不能以反斜杠结尾?

更准确地说,它们不能以奇数个反斜杠结尾:末尾不成对的反斜杠会转义闭合引号字符,从而留下一个未终止的字符串。

原始字符串旨在简化为需要进行自己的反斜杠转义处理的处理器(主要是正则表达式引擎)创建输入。此类处理器认为不匹配的尾随反斜杠也是一个错误,因此原始字符串禁止这样做。作为回报,它们允许您通过使用反斜杠转义字符串引号字符来传递该字符。当 r 字符串用于其预期目的时,这些规则效果很好。

如果您试图构建 Windows 路径名,请注意所有 Windows 系统调用也接受正斜杠

f = open("/mydir/file.txt")  # works fine!

如果您试图为 DOS 命令构建路径名,请尝试例如以下之一

dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"

为什么 Python 没有用于属性赋值的“with”语句?

Python 有一个 with 语句,它包装一个块的执行,在进入和退出该块时调用代码。某些语言具有如下结构

with obj:
    a = 1               # equivalent to obj.a = 1
    total = total + 1   # obj.total = obj.total + 1

在 Python 中,这样的结构会有歧义。

其他语言(例如 Object Pascal、Delphi 和 C++)使用静态类型,因此可以明确地知道正在分配给哪个成员。这是静态类型的主要观点——编译器在编译时总是知道每个变量的作用域。

Python 使用动态类型。不可能预先知道在运行时将引用哪个属性。成员属性可以在运行时动态地添加到对象或从对象中删除。这使得我们不可能从简单的阅读中知道正在引用哪个属性:是本地属性、全局属性还是成员属性?

例如,以下面的不完整代码片段为例

def foo(a):
    with a:
        print(x)

该片段假设 a 必须具有一个名为 x 的成员属性。但是,Python 中没有任何内容告诉解释器这一点。如果 a 是一个整数,那么应该发生什么?如果有一个名为 x 的全局变量,它会在 with 块内使用吗?如您所见,Python 的动态特性使得此类选择更加困难。

with 和类似语言特性(减少代码量)的主要好处,可以通过在 Python 中进行赋值轻松实现。而不是

function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63

写成这样

ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63

这还具有提高执行速度的副作用,因为名称绑定是在 Python 中在运行时解析的,而第二个版本只需要执行一次解析。

进一步减少代码量的类似提案(例如使用“前导点”)已被拒绝,而倾向于显式性(请参阅 https://mail.python.org/pipermail/python-ideas/2016-May/040070.html)。

为什么生成器不支持 with 语句?

出于技术原因,直接用作上下文管理器的生成器将无法正常工作。当一个生成器像最常见的用法那样用作迭代器并运行完成时,不需要关闭。当需要关闭时,请在 with 语句中将其包装为 contextlib.closing(generator)

为什么 if/while/def/class 语句需要冒号?

冒号的主要作用是提高可读性(ABC 语言实验的结果之一)。考虑一下

if a == b
    print(a)

if a == b:
    print(a)

请注意,第二个更容易阅读。还要注意,冒号如何在此 FAQ 答案中突出显示示例;这是英语中的标准用法。

另一个次要原因是冒号使具有语法突出显示的编辑器更容易;它们可以查找冒号来决定何时需要增加缩进,而无需对程序文本进行更详细的解析。

为什么 Python 允许在列表和元组末尾使用逗号?

Python 允许您在列表、元组和字典的末尾添加尾随逗号

[1, 2, 3,]
('a', 'b', 'c',)
d = {
    "A": [1, 5],
    "B": [6, 7],  # last trailing comma is optional but good style
}

允许这样做有几个原因。

当您有一个跨多行的列表、元组或字典的字面值时,添加更多元素会更容易,因为您不必记住在前一行添加逗号。这些行也可以重新排序,而不会产生语法错误。

意外省略逗号可能会导致难以诊断的错误。例如

x = [
  "fee",
  "fie"
  "foo",
  "fum"
]

此列表看起来有四个元素,但它实际上包含三个:“fee”、“fiefoo” 和 “fum”。始终添加逗号可避免这种错误来源。

允许尾随逗号也可能使程序代码的生成更容易。