花下猫语: 先祝你们假期快乐!今天,我要分享一篇长文,选自 Python 的官方文档。它列举了 27 个设计及历史的问题,其中有些问题我曾经分享过,例如为何使用显式的 self、浮点数的问题、len(x) 而非 x.len() 等等。大部分的回答很简略精要,适合在空闲之余翻阅。建议你先收藏起来,随时查看,温故知新。html
Guido van Rossum 认为使用缩进进行分组很是优雅,而且大大提升了普通Python程序的清晰度。大多数人在一段时间后就学会并喜欢上这个功能。python
因为没有开始/结束括号,所以解析器感知的分组与人类读者之间不会存在分歧。偶尔C程序员会遇到像这样的代码片断:git
if (x <= y) x++; y--; z++;
若是条件为真,则只执行 x++
语句,但缩进会使你认为状况并不是如此。即便是经验丰富的C程序员有时会长时间盯着它,想知道为何即便 x > y
, y
也在减小。程序员
由于没有开始/结束括号,因此Python不太容易发生编码式冲突。在C中,括号能够放到许多不一样的位置。若是您习惯于阅读和编写使用一种风格的代码,那么在阅读(或被要求编写)另外一种风格时,您至少会感到有些不安。github
许多编码风格将开始/结束括号单独放在一行上。这使得程序至关长,浪费了宝贵的屏幕空间,使得更难以对程序进行全面的了解。理想状况下,函数应该适合一个屏幕(例如,20--30行)。 20行Python能够完成比20行C更多的工做。这不只仅是因为缺乏开始/结束括号 -- 缺乏声明和高级数据类型也是其中的缘由 -- 但缩进基于语法确定有帮助。正则表达式
请看下一个问题。算法
用户常常对这样的结果感到惊讶:express
>>> 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 中的字符串被视为与数字同样“基本”。 任何动做都不会将值 8 更改成其余值,在 Python 中,任何动做都不会将字符串 "8" 更改成其余值。
这个想法借鉴了 Modula-3 语言。 出于多种缘由它被证实是很是有用的。
首先,更明显的显示出,使用的是方法或实例属性而不是局部变量。 阅读 self.x
或 self.meth()
能够清楚地代表,即便您不知道类的定义,也会使用实例变量或方法。在 C++ 中,能够经过缺乏局部变量声明来判断(假设全局变量不多见或容易识别) —— 可是在 Python 中没有局部变量声明,因此必须查找类定义才能肯定。 一些 C++ 和 Java 编码标准要求实例属性具备 m_
前缀,所以这种显式性在这些语言中仍然有用。
其次,这意味着若是要显式引用或从特定类调用该方法,不须要特殊语法。 在 C++ 中,若是你想使用在派生类中重写基类中的方法,你必须使用 ::
运算符 -- 在 Python 中你能够编写 baseclass.methodname(self, <argumentlist>)
。 这对于 __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...
正如Guido所说:
(a) 对于某些操做,前缀表示法比后缀更容易阅读 -- 前缀(和中缀!)运算在数学中有着悠久的传统,就像在视觉上帮助数学家思考问题的记法。比较一下咱们将 x (a+b) 这样的公式改写为 xa+x*b 的容易程度,以及使用原始OO符号作相同事情的笨拙程度。(b) 当读到写有len(X)的代码时,就知道它要求的是某件东西的长度。这告诉咱们两件事:结果是一个整数,参数是某种容器。相反,当阅读x.len()时,必须已经知道x是某种实现接口的容器,或者是从具备标准len()的类继承的容器。当没有实现映射的类有get()或key()方法,或者不是文件的类有write()方法时,咱们偶尔会感到困惑。
—https://mail.python.org/pipermail/python-3000/2006-November/004643.html
从Python 1.6开始,字符串变得更像其余标准类型,当添加方法时,这些方法提供的功能与始终使用String模块的函数时提供的功能相同。这些新方法中的大多数已被普遍接受,但彷佛让一些程序员感到不舒服的一种方法是:
", ".join(['1', '2', '4', '8', '16'])
结果以下:
"1, 2, 4, 8, 16"
反对这种用法有两个常见的论点。
第一条是这样的:“使用字符串文本(String Constant)的方法看起来真的很难看”,答案是也许吧,可是字符串文本只是一个固定值。若是在绑定到字符串的名称上容许使用这些方法,则没有逻辑上的理由使其在文字上不可用。
第二个异议一般是这样的:“我其实是在告诉序列使用字符串常量将其成员链接在一块儿”。遗憾的是并不是如此。出于某种缘由,把 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]
只有当你指望dict在任什么时候候都有key时,这才有意义。若是不是这样的话,你就是应该这样编码:
if key in mydict: value = mydict[key] else: value = mydict[key] = getvalue(key)
对于这种特定的状况,您还可使用 value = dict.setdefault(key, getvalue(key))
,但前提是调用 getvalue()
足够便宜,由于在全部状况下都会对其进行评估。
你能够经过一系列 if... elif... elif... else
.轻松完成这项工做。对于switch语句语法已经有了一些建议,但还没有就是否以及如何进行范围测试达成共识。有关完整的详细信息和当前状态,请参阅 PEP 275 。
对于须要从大量可能性中进行选择的状况,能够建立一个字典,将case 值映射到要调用的函数。例如:
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堆栈。
Python的 lambda表达式不能包含语句,由于Python的语法框架不能处理嵌套在表达式内部的语句。然而,在Python中,这并非一个严重的问题。与其余语言中添加功能的lambda表单不一样,Python的 lambdas只是一种速记符号,若是您懒得定义函数的话。
函数已是Python中的第一类对象,能够在本地范围内声明。 所以,使用lambda而不是本地定义的函数的惟一优势是你不须要为函数建立一个名称 -- 这只是一个分配了函数对象(与lambda表达式生成的对象类型彻底相同)的局部变量!
Cython 将带有可选注释的Python修改版本编译到C扩展中。 Nuitka 是一个将Python编译成 C++ 代码的新兴编译器,旨在支持完整的Python语言。要编译成Java,能够考虑 VOC 。
Python 内存管理的细节取决于实现。 Python 的标准实现 CPython 使用引用计数来检测不可访问的对象,并使用另外一种机制来收集引用循环,按期执行循环检测算法来查找不可访问的循环并删除所涉及的对象。 gc
模块提供了执行垃圾回收、获取调试统计信息和优化收集器参数的函数。
可是,其余实现(如 Jython 或 PyPy ),)能够依赖不一样的机制,如彻底的垃圾回收器 。若是你的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)
首先,这不是C标准特性,所以不能移植。(是的,咱们知道Boehm GC库。它包含了 大多数 常见平台(但不是全部平台)的汇编代码,尽管它基本上是透明的,但也不是彻底透明的; 要让Python使用它,须要使用补丁。)
当Python嵌入到其余应用程序中时,传统的GC也成为一个问题。在独立的Python中,能够用GC库提供的版本替换标准的malloc()和free(),嵌入Python的应用程序可能但愿用 它本身 替代malloc()和free(),而可能不须要Python的。如今,CPython能够正确地实现malloc()和free()。
当Python退出时,从全局命名空间或Python模块引用的对象并不老是被释放。 若是存在循环引用,则可能发生这种状况 C库分配的某些内存也是不可能释放的(例如像Purify这样的工具会抱怨这些内容)。 可是,Python在退出时清理内存并尝试销毁每一个对象。
若是要强制 Python 在释放时删除某些内容,请使用 atexit
模块运行一个函数,强制删除这些内容。
虽然列表和元组在许多方面是类似的,但它们的使用方式一般是彻底不一样的。能够认为元组相似于Pascal记录或C结构;它们是相关数据的小集合,能够是不一样类型的数据,能够做为一个组进行操做。例如,笛卡尔坐标适当地表示为两个或三个数字的元组。
另外一方面,列表更像其余语言中的数组。它们倾向于持有不一样数量的对象,全部对象都具备相同的类型,而且逐个操做。例如, os.listdir('.')
返回表示当前目录中的文件的字符串列表。若是向目录中添加了一两个文件,对此输出进行操做的函数一般不会中断。
元组是不可变的,这意味着一旦建立了元组,就不能用新值替换它的任何元素。列表是可变的,这意味着您始终能够更改列表的元素。只有不变元素能够用做字典的key,所以只能将元组和非列表用做key。
CPython的列表其实是可变长度的数组,而不是lisp风格的链表。该实现使用对其余对象的引用的连续数组,并在列表头结构中保留指向该数组和数组长度的指针。
这使得索引列表 a[i]
的操做成本与列表的大小或索引的值无关。
当添加或插入项时,将调整引用数组的大小。并采用了一些巧妙的方法来提升重复添加项的性能; 当数组必须增加时,会分配一些额外的空间,以便在接下来的几回中不须要实际调整大小。
CPython的字典实现为可调整大小的哈希表。与B-树相比,这在大多数状况下为查找(目前最多见的操做)提供了更好的性能,而且实现更简单。
字典的工做方式是使用 hash()
内置函数计算字典中存储的每一个键的hash代码。hash代码根据键和每一个进程的种子而变化很大;例如,"Python" 的hash值为-539294296,而"python"(一个按位不一样的字符串)的hash值为1142331976。而后,hash代码用于计算内部数组中将存储该值的位置。假设您存储的键都具备不一样的hash值,这意味着字典须要恒定的时间 -- O(1),用Big-O表示法 -- 来检索一个键。
字典的哈希表实现使用从键值计算的哈希值来查找键。若是键是可变对象,则其值可能会发生变化,所以其哈希值也会发生变化。可是,因为不管谁更改键对象都没法判断它是否被用做字典键值,所以没法在字典中修改条目。而后,当你尝试在字典中查找相同的对象时,将没法找到它,由于其哈希值不一样。若是你尝试查找旧值,也不会找到它,由于在该哈希表中找到的对象的值会有所不一样。
若是你想要一个用列表索引的字典,只需先将列表转换为元组;用函数 tuple(L)
建立一个元组,其条目与列表 L
相同。 元组是不可变的,所以能够用做字典键。
已经提出的一些不可接受的解决方案:
哈希按其地址(对象ID)列出。这不起做用,由于若是你构造一个具备相同值的新列表,它将没法找到;例如:
mydict = {[1, 2]: '12'} print(mydict[[1, 2]])
会引起一个 KeyError
异常,由于第二行中使用的 [1, 2]
的 id 与第一行中的 id 不一样。换句话说,应该使用 ==
来比较字典键,而不是使用 is
。
d.keys()
中的每一个值均可用做字典的键。若是须要,可使用如下方法来解决这个问题,但使用它须要你自担风险:你能够将一个可变结构包装在一个类实例中,该实例同时具备 __eq__()
和 __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
注意,哈希计算因为列表的某些成员可能不可用以及算术溢出的可能性而变得复杂。
此外,必须始终如此,若是 o1 == o2
(即 o1.__eq__(o2) is True
)则 hash(o1) == hash(o2)
`(即`o1.__hash__() == o2.__hash__()
),不管对象是否在字典中。 若是你不能知足这些限制,字典和其余基于 hash 的结构将会出错。
对于 ListWrapper ,只要包装器对象在字典中,包装列表就不能更改以免异常。除非你准备好认真考虑需求以及不正确地知足这些需求的后果,不然不要这样作。请留意。
在性能很重要的状况下,仅仅为了排序而复制一份列表将是一种浪费。所以, list.sort()
对列表进行了适当的排序。为了提醒您这一事实,它不会返回已排序的列表。这样,当您须要排序的副本,但也须要保留未排序的版本时,就不会意外地覆盖列表。
若是要返回新列表,请使用内置 sorted()
函数。此函数从提供的可迭代列表中建立新列表,对其进行排序并返回。例如,下面是如何迭代遍历字典并按keys排序:
for key in sorted(mydict): ... # do whatever with mydict[key]...
由C++和Java等语言提供的模块接口规范描述了模块的方法和函数的原型。许多人认为接口规范的编译时强制执行有助于构建大型程序。
Python 2.6添加了一个 abc
模块,容许定义抽象基类 (ABCs)。而后可使用 isinstance()
和 issubclass()
来检查实例或类是否实现了特定的ABC。 collections.abc
模块定义了一组有用的ABCs 例如 Iterable
, Container
, 和 MutableMapping
对于Python,经过对组件进行适当的测试规程,能够得到接口规范的许多好处。还有一个工具PyChecker,可用于查找因为子类化引发的问题。
一个好的模块测试套件既能够提供回归测试,也能够做为模块接口规范和一组示例。许多Python模块能够做为脚本运行,以提供简单的“自我测试”。即便是使用复杂外部接口的模块,也经常可使用外部接口的简单“桩代码(stub)”模拟进行隔离测试。可使用 doctest
和 unittest
模块或第三方测试框架来构造详尽的测试套件,以运行模块中的每一行代码。
适当的测试规程能够帮助在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-string用于它们的预期目的时,这些规则工做的很好。
若是您正在尝试构建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' 语句,它封装了块的执行,在块的入口和出口调用代码。有些语言的结构是这样的:
with obj: a = 1 # equivalent to obj.a = 1 total = total + 1 # obj.total = obj.total + 1
在Python中,这样的结构是不明确的。
其余语言,如ObjectPascal、Delphi和C++ 使用静态类型,所以能够绝不含糊地知道分配给什么成员。这是静态类型的要点 -- 编译器 老是 在编译时知道每一个变量的做用域。
Python使用动态类型。事先不可能知道在运行时引用哪一个属性。能够动态地在对象中添加或删除成员属性。这使得没法经过简单的阅读就知道引用的是什么属性:局部属性、全局属性仍是成员属性?
例如,采用如下不完整的代码段:
def foo(a): with a: print(x)
该代码段假设 "a" 必须有一个名为 "x" 的成员属性。然而,Python中并无告诉解释器这一点。假设 "a" 是整数,会发生什么?若是有一个名为 "x" 的全局变量,它是否会在with块中使用?如您所见,Python的动态特性使得这样的选择更加困难。
然而,Python 能够经过赋值轻松实现 "with" 和相似语言特性(减小代码量)的主要好处。代替:
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在运行时解析名称绑定,而第二个版本只须要执行一次解析。
冒号主要用于加强可读性(ABC语言实验的结果之一)。考虑一下这个:
if a == b print(a)
与
if a == b: print(a)
注意第二种方法稍微容易一些。请进一步注意,在这个FAQ解答的示例中,冒号是如何设置的;这是英语中的标准用法。
另外一个次要缘由是冒号使带有语法突出显示的编辑器更容易工做;他们能够寻找冒号来决定什么时候须要增长缩进,而没必要对程序文本进行更精细的解析。
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" 。老是加上逗号能够避免这个错误的来源。
容许尾随逗号也可使编程代码更容易生成。
公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写做、优质英文推荐与翻译等等,欢迎关注哦。