Skip to main content

设计和历史常见问题

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

Guido van Rossum认为,使用缩进进行分组是非常优雅的,有助于平均Python程序的清晰度。大多数人学会爱一段时间后这个功能。

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

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

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

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

许多编码风格将开始/结束括号放在一行上。这使得程序相当长,并浪费宝贵的屏幕空间,使得更难以获得一个程序的良好概述。理想情况下,一个函数应该适合一个屏幕(例如,20 - 30行)。 20行的Python可以做比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字符串不可变?

有几个优点。

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

另一个优点是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 成语,似乎没有在表达式构造中缺少赋值;它只是新来的人表达强烈的愿望添加到语言。

有一种替代方法拼写这似乎有吸引力,但通常不如“真正的”解决方案健壮:

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

这样做的问题是,如果你改变主意关于如何获得下一行(例如,你想改变它为 sys.stdin.readline()),你必须记住改变你的程序中的两个地方 - 第二次出现隐藏在底部的循环。

最好的方法是使用迭代器,使得可以使用 for 语句循环访问对象。例如,文件对象 支持迭代器协议,因此可以简单地写:

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

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

主要原因是历史。函数用于那些对于一组类型是通用的操作,并且意图用于即使对于没有方法的对象(例如元组)也是如此。当使用Python的功能特征(map()zip() 等)时,具有可以容易地应用于对象的无定形集合的功能也是方便的。

实际上,实现 len()max()min() 作为内置函数实际上比将它们实现为每种类型的方法更少的代码。人们可以讨论个别案例,但它是Python的一部分,现在太晚了,不能进行这样的根本改变。这些功能必须保留,以避免大量的代码损坏。

注解

对于字符串操作,Python已经从外部函数(string 模块)移动到方法。然而,len() 仍然是一个功能。

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

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

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

这给出结果:

"1, 2, 4, 8, 16"

有两个常见的论据反对这种用法。

第一个沿着以下行:“它看起来真的丑陋使用字符串字面量(字符串常量)”的方法,答案是,它可能,但字符串字面量只是一个固定值。如果允许对绑定到字符串的名称的方法,则没有逻辑原因使它们在字面值上不可用。

第二个异议通常被转换为:“我真的告诉序列连接它的成员与字符串常量”。可悲的是,你不是。由于某种原因,使用 split() 作为字符串方法似乎没有什么困难,因为在这种情况下很容易看到

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

是一个指向字符串文字的指令,用于返回由给定分隔符(或默认为任意空白空格)分隔的子字符串。

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

异常有多快?

如果没有引发异常,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:幸运的是,有一个 无堆栈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库,它有一些 most 通用平台的汇编代码,而不是所有的,虽然它大部分是透明的,但是它不是完全透明的;需要补丁来使Python工作用它。)

当Python嵌入到其他应用程序中时,传统GC也成为一个问题。在独立的Python中,将标准的malloc()和free()替换为由GC库提供的版本是很好的,应用程序嵌入Python可能希望有 own 替代malloc()和free(),并且可能不想Python的。现在,CPython使用任何实现malloc()和free()的东西。

为什么当CPython退出时不会释放所有内存?

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

如果你想强制Python删除某些事情在deallocation使用 atexit 模块运行一个函数,将强制这些删除。

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

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

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

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

如何实现列表?

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

这使得索引列表 a[i] 是其成本独立于列表的大小或索引的值的操作。

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

如何实现字典?

Python的字典被实现为可调整大小的哈希表。与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() 函数。此函数从提供的iterable中创建一个新列表,对其进行排序并返回。例如,下面是如何以排序顺序遍历字典的键:

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?

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

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

原始字符串旨在为处理器(主要是正则表达式引擎)创建输入,以便进行自己的反斜杠转义处理。这样的处理器认为不匹配的尾反斜杠是一个错误,所以原始字符串不允许。作为回报,它们允许你通过使用反斜杠转义来传递字符串引号字符。当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++,使用静态类型,因此可以以明确的方式知道正在分配什么成员。这是静态类型的主要要点 - 编译器 always 知道在编译时每个变量的范围。

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)

注意第二个稍微更容易阅读。进一步注意一个冒号如何在这个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"
]

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

允许尾随逗号也可以使得编程代码生成更容易。