设计和历史常见问题解答

目录

为什么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中的一个bug。不是。这与Python关系不大,而与底层平台如何处理浮点数字关系更大。

这个 float 键入cpython使用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位小数的精度。

如需更全面的解释,请参阅 floating point arithmetic python教程中的一章。

为什么python字符串是不可变的?

有几个优点。

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

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

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

这个想法借鉴了模块3。由于各种原因,这是非常有用的。

首先,更明显的是,您使用的是方法或实例属性而不是局部变量。阅读 self.xself.meth() 非常清楚地表明,即使您根本不知道类定义,也会使用实例变量或方法。在C++中,您可以通过缺少局部变量声明来判断(假设全局变量很少见或容易识别)——但是在Python,没有局部变量声明,因此您必须查找类定义才能确定。一些C++和Java编码标准调用实例属性来创建一个 m_ 前缀,所以这种明确性在这些语言中仍然有用。

第二,它意味着如果您想要显式引用或从特定类调用方法,就不需要特殊的语法。在C++中,如果希望从派生类中重写的基类使用方法,则必须使用 :: operator——在python中,您可以编写 baseclass.methodname(self, <argument list>) . 这对 __init__() 方法,通常在派生类方法希望扩展同名的基类方法,因此必须以某种方式调用基类方法的情况下。

最后,例如变量,它通过赋值解决了一个语法问题:因为Python中的局部变量是(根据定义!)在函数体中为其赋值的那些变量(不是显式地声明为全局变量),必须有某种方法告诉解释器赋值是为了给实例变量而不是局部变量赋值,而且最好是句法的(出于效率原因)。C++通过声明来实现这一点,但是Python没有声明,遗憾的是必须为此而引入它们。使用显式 self.var 很好地解决了这个问题。同样,对于使用实例变量,必须编写 self.var 意味着对方法中非限定名称的引用不必搜索实例的目录。换句话说,局部变量和实例变量存在于两个不同的名称空间中,您需要告诉Python要使用哪个名称空间。

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

从Python3.8开始,你可以!

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

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

PEP 572 更多信息。

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

正如Guido所说:

(A)对于某些操作,前缀表示法比后缀前缀(和infix!)运算在数学中有着悠久的传统,它类似于用符号来表示视觉帮助数学家思考问题。比较我们重写x公式的容易程度 (a+b)为x A+X*B使用一个原始的OO符号做同样的事情是笨拙的。

(b)当我读到表示len(x)i的代码时 know 它要求的是某物的长度。这告诉我两件事:结果是一个整数,参数是某种容器。相反,当我读取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语句?

通过一系列 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 lambda只是一个简短的符号。

函数已经是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中,可以用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的字典实现为可调整大小的hash表。与B-树相比,这在大多数情况下为查找(目前最常见的操作)提供了更好的性能,并且实现更简单。

字典的工作原理是使用 hash() 内置功能。散列代码因密钥和每个进程种子的不同而变化很大;例如,“python”可以散列到-539294296,而“python”(单个位不同的字符串)可以散列到1142331976。然后,hash代码用于计算内部数组中存储值的位置。假设您存储的键都有不同的散列值,这意味着字典需要恒定的时间来检索一个键——用big-o符号表示的o(1)。

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

字典的hash表实现使用从键值计算出的hash值来查找键。如果键是可变对象,则其值可能会更改,因此其hash值也可能更改。但是,由于更改键对象的人不能知道它被用作字典键,所以它不能在字典中移动条目。然后,当您试图在字典中查找同一个对象时,将找不到它,因为它的hash值不同。如果您试图查找旧值,也不会找到它,因为在散列箱中找到的对象的值也会不同。

如果您想要一个用列表索引的字典,只需先将列表转换成一个元组;函数 tuple(L) 使用与列表相同的条目创建元组 L .元组是不可变的,因此可以用作字典键。

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

  • 按地址(对象ID)列出的hash列表。这不起作用,因为如果用相同的值构造一个新列表,将找不到它;例如:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])
    

    会提高 KeyError 异常,因为 [1, 2] 第二行与第一行不同。换句话说,应该使用 == ,不使用 is .

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

  • 允许列表作为键,但告诉用户不要修改它们。当你不小心忘记或修改了一个列表时,这将允许一类难以跟踪程序中的错误。它还使字典的一个重要不变量失效:中的每个值 d.keys() 可用作字典的键。

  • 将列表用作字典键后,将其标记为只读。问题是,不仅顶级对象可以更改其值,还可以使用包含列表的元组作为键。将任何东西作为键输入到字典中都需要将从字典中可访问的所有对象标记为只读——同样,自引用对象也可能导致无限循环。

如果需要的话,有一个技巧可以解决这个问题,但要冒风险使用它:您可以在类实例中封装一个可变结构,该类实例同时具有 __eq__() 和A __hash__() 方法。然后,必须确保驻留在字典(或其他基于hash的结构)中的所有此类封装对象的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

请注意,hash计算由于列表中的某些成员可能不可显示以及算术溢出的可能性而变得复杂。

此外,如果 o1 == o2 (即 o1.__eq__(o2) is Truehash(o1) == hash(o2) (, o1.__hash__() == o2.__hash__() ,无论对象是否在字典中。如果不满足这些限制,字典和其他基于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,接口规范的许多优点可以通过对组件的适当测试规程获得。

一个好的模块测试套件既可以提供回归测试,也可以作为模块接口规范和一组示例。许多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中,这样的构造是不明确的。

其他语言,如对象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"。始终添加逗号可避免此错误源。

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