设计和历史常见问题

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

Guido van Rossum 认为使用缩进来进行分组非常优雅,并且对普通 Python 程序的清晰度有很大贡献。大多数人过一段时间就会喜欢上这个功能。

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

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

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

因为没有 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 操作,这些操作又依赖于处理器中的硬件实现来执行浮点操作。这意味着就浮点操作而言,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 中的局部变量(根据定义!)是在函数体中赋值(并且没有显式声明为全局变量)的变量,因此必须有某种方法告诉解释器赋值 intended to assign to an instance variable instead of to a local variable,并且最好是语法的(出于效率原因)。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 的容易程度,以及使用原始面向对象表示法做同样事情的笨拙程度。

(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 块效率极高。实际捕获异常是昂贵的。在 Python 2.0 之前的版本中,通常使用此习语

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

这只有在你期望 dict 几乎总是包含该键时才有意义。如果不是这种情况,你的代码会像这样

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 的序列。

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

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 到 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 中替换标准的 malloc()free() 与 GC 库提供的版本是没问题的,但嵌入 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 与第一行中的不同。换句话说,字典键应该使用 == 进行比较,而不是使用 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 语句和 orandif/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 语句?

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

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

冒号主要用于增强可读性(实验性 ABC 语言的结果之一)。考虑这个

if a == b
    print(a)

对比

if a == b:
    print(a)

请注意,第二个更容易阅读。进一步注意,冒号如何分隔此常见问题解答中的示例;这是英语中的标准用法。

另一个次要原因是,冒号使带有语法高亮的编辑器更容易;它们可以查找冒号来决定何时需要增加缩进,而不必对程序文本进行更复杂的解析。

为什么 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”。始终添加逗号可以避免这种错误源。

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