设计和历史常见问题解答 — Python 文档

来自菜鸟教程
Python/docs/3.6/faq/design
跳转至:导航、​搜索

设计和历史常见问题解答

内容


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

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

由于没有开始/结束括号,解析器和人类读者感知的分组之间不会有分歧。 偶尔 C 程序员会遇到这样的代码片段:

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

如果条件为真,则仅执行 x++ 语句,但缩进使您不相信。 即使是有经验的 C 程序员,有时也会盯着它看很长时间,想知道为什么 y 即使是 x > y 也会递减。

因为没有开始/结束括号,Python 不太容易出现编码风格冲突。 在 C 中有许多不同的方法来放置大括号。 如果您习惯于阅读和编写使用一种风格的代码,那么在阅读(或被要求编写)另一种风格时,您至少会感到有些不安。

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


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

请看下一个问题。


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

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

>>> 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 中,任何活动都不会将字符串“8”更改为其他任何值。


为什么必须在方法定义和调用中显式使用“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 使用哪个命名空间。


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

许多习惯于 C 或 Perl 的人抱怨他们想使用这个 C 习语:

while (line = readline(f)) {
    // do something with line
}

在 Python 中你被迫写这个:

while True:
    line = f.readline()
    if not line:
        break
    ...  # do something with line

不允许在 Python 表达式中赋值的原因是其他语言中一个常见的、难以发现的错误,由以下结构引起:

if (x = 0) {
    // error handling
}
else {
    // code that only works for nonzero x
}

该错误是一个简单的错字:x = 0 将 0 分配给变量 x,是在比较 x == 0 时写入的。

已经提出了许多替代方案。 大多数是节省一些输入但使用任意或神秘的语法或关键字的黑客,并且未能满足语言更改建议的简单标准:它应该直观地向尚未被引入该结构的人类读者建议正确的含义。

一个有趣的现象是,大多数有经验的 Python 程序员都认可 while True 习语,并且似乎并没有过多地遗漏表达式构造中的赋值; 只有新人表示强烈希望将其添加到语言中。

有一种替代的拼写方式,看起来很有吸引力,但通常不如“while True”解决方案健壮:

line = f.readline()
while line:
    ...  # do something with line...
    line = f.readline()

这样做的问题是,如果您改变主意,确切地知道如何获得下一行(例如 你想把它改成 sys.stdin.readline()) 你必须记住在你的程序中改变两个地方——第二个地方隐藏在循环的底部。

最好的方法是使用迭代器,从而可以使用 for 语句遍历对象。 比如file objects支持迭代器协议,所以可以简单的写:

for line in f:
    ...  # do something with line...

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

正如圭多所说:

(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 开始,字符串变得更像其他标准类型,当添加的方法提供与使用 string 模块的函数始终可用的相同功能时。 大多数这些新方法已被广泛接受,但似乎让一些程序员感到不舒服的是:

", ".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 语句?

您可以使用 if... elif... elif... else 的序列轻松完成此操作。 已经有一些关于 switch 语句语法的建议,但是关于是否以及如何进行范围测试还没有达成共识。 有关完整的详细信息和当前状态,请参阅 PEP 275

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

def function_1(...):
    ...

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

func = functions[value]
func()

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

def visit_a(self, ...):
    ...
...

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

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


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

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

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


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

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

函数在 Python 中已经是第一类对象,可以在局部作用域中声明。 因此,使用 lambda 而不是本地定义的函数的唯一优点是您不需要为函数命名 - 但这只是函数对象(完全相同类型的对象)的局部变量一个 lambda 表达式产生)被赋值!


Python 可以编译成机器码、C 或其他语言吗?

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


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 的应用程序可能希望用它的 own 代替 malloc() 和 free() ),并且可能不想要 Python 的。 现在,CPython 可以处理任何正确实现 malloc() 和 free() 的东西。


为什么 CPython 退出时没有释放所有内存?

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

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


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

列表和元组虽然在许多方面相似,但通常以根本不同的方式使用。 元组可以被认为类似于 Pascal 记录或 C 结构体; 它们是相关数据的小集合,这些数据可能是作为一个组进行操作的不同类型。 例如,笛卡尔坐标适当地表示为两个或三个数字的元组。

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

元组是不可变的,这意味着一旦创建了元组,就不能用新值替换它的任何元素。 列表是可变的,这意味着您可以随时更改列表的元素。 只有不可变元素可以用作字典键,因此只有元组而不是列表可以用作键。


列表是如何在 CPython 中实现的?

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

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

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


字典是如何在 CPython 中实现的?

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

字典的工作原理是使用 hash() 内置函数为字典中存储的每个键计算哈希码。 哈希码因密钥和每个进程的种子而有很大差异; 例如,“Python”可以散列到 -539294296,而“python”,一个相差一位的字符串,可以散列到 1142331976。 然后使用哈希码计算内部数组中将存储值的位置。 假设您存储的键都具有不同的散列值,这意味着字典需要恒定的时间——在计算机科学符号中为 O(1)——来检索一个键。 这也意味着不维护键的排序顺序,并且像 .keys().items() 那样遍历数组将以某种任意混乱的顺序输出字典的内容,该顺序可以随着每次调用而改变一个程序。


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

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

如果你想要一个用列表索引的字典,只需先将列表转换为元组; 函数 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,接口规范的许多优点都可以通过适当的组件测试规程获得。 还有一个工具,PyChecker,可以用来发现由于子类化引起的问题。

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

适当的测试学科可以帮助在 Python 中构建大型复杂应用程序,以及具有接口规范。 事实上,它可以更好,因为接口规范不能测试程序的某些属性。 例如, append() 方法预计会在某些内部列表的末尾添加新元素; 接口规范无法测试您的 append() 实现实际上是否会正确执行此操作,但在测试套件中检查此属性是微不足道的。

编写测试套件非常有用,您可能希望设计代码时着眼于使其易于测试。 一种越来越流行的技术,测试导向开发,要求在编写任何实际代码之前首先编写测试套件的一部分。 当然,Python 允许你马虎,根本不写测试用例。


为什么没有goto?

您可以使用异常来提供一个“结构化的跳转”,它甚至可以跨函数调用工作。 许多人认为异常可以方便地模拟 C、Fortran 和其他语言的“go”或“goto”结构的所有合理用法。 例如:

class label(Exception): pass  # declare a label

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

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


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

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

原始字符串旨在为想要进行自己的反斜杠转义处理的处理器(主要是正则表达式引擎)轻松创建输入。 这样的处理器无论如何都认为不匹配的尾随反斜杠是一个错误,因此原始字符串不允许这样做。 作为回报,它们允许您通过用反斜杠转义来传递字符串引号字符。 当 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 中在运行时解析,而第二个版本只需要执行一次解析。


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

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