python / 3.7.2rc1 / all / faq-design.html

设计和历史常见问题

Contents

为什么 Python 将缩进用于语句分组?

Guido van Rossum 相信使用缩进进行分组非常优雅,并且为普通 Python 程序的清晰度做出了很大贡献。一段时间后,大多数人学会了爱上此Function。

由于没有开始/结束括号,因此在解析器和人类 Reader 所感知的分组之间不会存在分歧。有时,C 程序员会遇到如下代码片段:

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

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

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

许多编码样式自己将开始/结束括号放在一行上。这使程序更长,并浪费了宝贵的屏幕空间,因此很难获得程序的良好概览。理想情况下,一个Function应该适合一个屏幕(例如 20 至 30 行)。 Python 的 20 行可以比 C 的 20 行做更多的工作。这不仅是由于缺少开始/结尾括号-缺少语句和高级数据类型也要负责-而是基于缩进语法肯定有帮助。

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

请参阅下一个问题。

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

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

>>> 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 字符串是不可变的?

有几个优点。

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

另一个优点是 Python 中的字符串被视为数字的“元素”。活动量不会将值 8 更改为其他任何值,在 Python 中,活动量也不会将字符串“八”更改为其他任何值。

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

这个想法是从 Modula-3 借来的。出于多种原因,它非常有用。

首先,很明显,您使用的是方法或实例属性而不是局部变量。读取self.xself.meth()绝对清楚,即使您不了解类的定义,也会使用实例变量或方法。在 C 中,您可以pass缺少局部变量语句(假设全局变量很少或容易识别)来判断-但是在 Python 中,没有局部变量语句,因此您必须查找类定义才能当然。一些 C 和 Java 编码标准要求实例属性具有m_前缀,因此在这些语言中,这种显式性仍然有用。

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

最后,例如,变量解决了赋值的语法问题:由于 Python 中的局部变量(按定义!)是在函数体中为其赋值(并且未明确语句为全局)的那些变量,因此必须告诉解释器某种分配是要分配给实例变量而不是局部变量的某种方式,并且它最好是语法上的(出于效率考虑)。 C pass 语句来做到这一点,但是 Python 没有语句,因此仅出于此目的而引入它们是可惜的。使用明确的self.var可以很好地解决此问题。同样,对于使用实例变量,必须写入self.var意味着对方法内部对不合格名称的引用不必搜索实例的目录。换句话说,局部变量和实例变量位于两个不同的名称空间中,您需要告诉 Python 使用哪个名称空间。

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

从 Python 3.8 开始,您可以!

使用 walrus 运算符的赋值表达式:=在表达式中分配变量:

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

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

为什么 Python 使用方法来实现某些Function(例如 list.index()),却使用其他方法(例如 len(list))呢?

正如 Guido 所说:

Note

(a)对于某些运算,前缀表示法比后缀读得更好-前缀(和 infix!)运算符在 math 中具有悠久的传统,喜欢在视觉上帮助 math 家思考问题的表示法。将我们将 x *(a b)之类的公式重写为 x * a x * b 的简便性与使用原始 OO 符号做相同事情的笨拙性进行比较。

(b)当我读到写着 len(x)的代码时,我知道它在问某物的长度。这告诉我两件事:结果是整数,参数是某种容器。相反,当我阅读 x.len()时,我必须已经知道 x 是某种实现接口或从具有标准 len()的类继承的容器。当未实现 Map 的类具有 get()或 keys()方法,或者不是文件的东西具有 write()方法时,我们有时会感到困惑。

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

为什么 join()是字符串方法而不是列表或 Tuples 方法?

字符串变得越来越像其他从 Python 1.6 开始的标准类型,当时添加了方法,这些方法提供的Function与使用字符串模块的Function始终可用的Function相同。这些新方法中的大多数已被广泛接受,但是使某些程序员感到不舒服的方法是:

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

结果如下:

"1, 2, 4, 8, 16"

有两种常见的说法反对这种用法。

第一种运行方式是:“使用字符串 Literals(字符串常量)的方法看起来确实很丑”,答案是可能的,但字符串 Literals 只是一个固定值。如果允许在绑定到字符串的名称上使用这些方法,则没有逻辑上的理由使它们在 Literals 上不可用。

第二个异议通常被表述为:“我实际上是在说一个将其成员与字符串常量连接在一起的序列”。可悲的是你不是。由于某种原因,使用split()作为字符串方法似乎没有那么多的困难,因为在这种情况下,很容易看到

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

是对字符串 Literals 的指令,用于返回由给定分隔符(或默认情况下,任意空格运行)分隔的子字符串。

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

exceptions 有多快?

如果没有引发异常,则 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 语句?

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

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

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_。没有这样的前缀,如果值来自不受信任的来源,则攻击者将能够在您的对象上调用任何方法。

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

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

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

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

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

函数已经是 Python 中的第一类对象,并且可以在本地范围内语句。因此,使用 lambda 代替本地定义的函数的唯一好处是,您无需为函数创建名称-但这只是函数对象(与对象的类型完全相同)的局部变量会产生 lambda 表达式)!

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

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

Python 如何 Management 内存?

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

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

在某些 Python 实现中,以下代码(在 CPython 中很好)可能会用完文件 Descriptors:

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

确实,使用 CPython 的引用计数和析构函数方案,对* f *的每次新分配都会关闭前一个文件。但是,使用传统的 GC,这些文件对象只能以变化的间隔(可能很长的间隔)收集(并关闭)。

如果要编写适用于任何 Python 实现的代码,则应显式关闭文件或使用with语句;无论内存 Management 方案如何,它都将起作用:

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

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

一方面,这不是 C 标准Function,因此不可移植。 (是的,我们了解 Boehm GC 库.它具有大部分通用平台的汇编代码,而不是所有通用平台的汇编代码,尽管它大部分是透明的,但并不完全透明;需要使用补丁才能获取 Python 使用它.)

当将 Python 嵌入其他应用程序时,传统的 GC 也成为问题。虽然在独立的 Python 中可以用 GC 库提供的版本替换标准的 malloc()和 free(),但是嵌入 Python 的应用程序可能希望用* own *替代 malloc()和 free(),并且不想要 Python 的。现在,CPython 可以与任何可以正确实现 malloc()和 free()的东西一起使用。

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

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

如果要强制 Python 删除释放对象上的某些内容,请使用atexit模块运行将强制执行这些删除操作的函数。

为什么会有单独的 Tuples 和列表数据类型?

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

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

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

如何在 CPython 中实现列表?

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

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

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

如何在 CPython 中实现字典?

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

字典pass使用hash()内置函数为存储在字典中的每个键计算哈希码来工作。哈希码的变化取决于密钥和每个进程的种子。例如,“ Python”可以哈希为-539294296,而“ python”(一个相差一个位的字符串)可以哈希为 1142331976.哈希码然后用于计算内部数组中将存储值的位置。假设您存储的键均具有不同的哈希值,这意味着字典需要恒定的时间(以 Big-O 表示法表示为 O(1))来检索键。

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

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

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

提出了一些不可接受的解决方案:

  • 哈希列表按其地址(对象 ID)显示。这是行不通的,因为如果您构造一个具有相同值的新列表,则将找不到它。例如。:
mydict = {[1, 2]: '12'}
print(mydict[[1, 2]])

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

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

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

  • 将列表用作字典键后,将其标记为只读。问题在于,不仅仅是顶级对象可以更改其值。您可以使用包含列表作为键的 Tuples。在字典中 Importing 任何内容作为键将需要将所有从那里可以访问的对象标记为只读–而且,自引用对象可能会导致无限循环。

如果需要,有一个解决方法,但是要冒此风险,但要自己承担风险:可以在具有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()函数。此函数根据提供的可迭代对象创建一个新列表,对其进行排序并返回。例如,以下是按排序 Sequences 遍历字典键的方法:

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

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

由诸如 C 和 Java 之类的语言提供的模块接口规范描述了该模块的方法和Function的原型。许多人认为接口规范的编译时实施有助于构建大型程序。

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

对于 Python,可以pass对组件进行适当的测试来获得接口规范的许多优点。还有一个工具 PyChecker,可用于查找由于子类化而引起的问题。

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

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

编写测试套件非常有帮助,您可能希望设计代码以使其易于测试。一种越来越流行的技术,即测试导向的开发,要求在编写任何实际代码之前先编写测试套件的各个部分。当然,Python 允许您草率而不用编写测试用例。

为什么没有 goto?

您可以使用异常来提供“结构化的 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)不能以反斜杠结尾?

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

原始字符串旨在简化为想要执行自己的反斜杠转义处理的处理器(主要是正则表达式引擎)创建 Importing 的过程。这样的处理器无论如何都认为不匹配的尾部反斜杠是一个错误,因此原始字符串不允许这样做。作为回报,它们允许您pass使用反斜杠转义来传递字符串引号字符。当 r 字符串用于其预期目的时,这些规则会很好地起作用。

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

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

如果您要为 DOS 命令构建路径名,请try例如之一

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”语句,用于包装块的执行,在块的入口和 Export 调用代码。一些语言的结构如下所示:

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 的动态特性使这种选择变得更加困难。

但是,“赋”和类似语言Function(减少代码量)的主要好处是可以pass Python 在赋值中轻松实现。代替:

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

write this:

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)

versus

if a == b:
    print(a)

请注意,第二个稍微更易于阅读。请进一步注意冒号是如何引起该 FAQ 回答中示例的;这是英语的标准用法。

另一个次要原因是冒号使使用语法突出显示的编辑器更加容易。他们可以寻找冒号来决定何时需要增加缩进量,而不必对程序文本进行更复杂的解析。

为什么 Python 在列表和 Tuples 的末尾允许逗号?

Python 使您可以在列表,Tuples 和字典的末尾添加尾随逗号:

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

允许这样做有几个原因。

当您具有分布在多行中的列表,Tuples 或字典的 Literals 值时,添加更多元素会更容易,因为您不必记住在前一行添加逗号。行也可以重新排序而不会产生语法错误。

不小心忽略逗号会导致难以诊断的错误。例如:

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

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

允许以逗号结尾也可以使程序代码生成更加容易。