设计与历史常见问题解答

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

Guido van Rossum 认为,使用缩进表示分组非常优雅,并且极大地提高了普通 Python 程序的清晰度。大多数人在使用一段时间后都会喜欢上这个功能。

由于没有 begin/end 括号,因此解析器感知的分组与人类阅读者感知的分组之间不会出现分歧。偶尔,C 程序员会遇到类似以下的代码片段

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

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

由于没有 begin/end 括号,Python 就不太容易出现编码风格冲突。在 C 中,有很多不同的方法来放置大括号。在习惯了阅读和编写使用特定风格的代码后,当阅读(或被要求编写)使用不同风格的代码时,通常会感到有些不适。

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

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

查看下一个问题。

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

用户经常会对这样的结果感到惊讶

>>> 1.2 - 1.0
0.19999999999999996

并认为这是 Python 中的错误。事实并非如此。这与 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_。如果没有这样的前缀,如果值来自不可信来源,攻击者将能够调用对象上的任何方法。

模仿 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 编译器,它将 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 的新赋值都会关闭之前打开的文件。然而,使用传统的垃圾回收机制,这些文件对象只有在不同的时间间隔(可能很长)才会被回收(并关闭)。

如果你想编写适用于任何 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 嵌入到其他应用程序中时,传统的垃圾回收也会成为问题。虽然在独立的 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。然后使用哈希码计算内部数组中的一个位置,值将存储在该位置。假设您存储的键都具有不同的哈希值,这意味着字典需要常数时间(在 Big-O 符号中为 *O*(1))来检索键。

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

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

如果您想使用列表作为索引的字典,只需先将列表转换为元组即可;函数 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 语句和 orand 以及 if/else 表达式)和循环(使用 whilefor 语句,可能包含 continuebreak),它也是不需要的。

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

class label(Exception): pass  # declare a label

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

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

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

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

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

如果您尝试构建 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”。始终添加逗号可以避免这种错误来源。

允许尾随逗号还可以使程序代码生成更容易。