《流畅的Python》
《流畅的Python》中文版2017年出版,豆瓣评分9.2。
《Fluent Python》英文原版2015年出版,豆瓣评分9.5。 Python进阶书,必须认真阅读之。书中示例代码github地址。
作者简介
Luciano Ramalho在1995年Netscape首次公开募股以前就是一名Web开发者了,他先后用过Perl和 Java,1998年开始使用Python。自那以后,他在巴西的几个新闻门户网站工作,使用Python做开发,还为巴西的媒体、银行和政府部门做Python Web开发培训。他经常在开发者大会上演讲,比如PyCon US(2013)、OSCON(2002、2013 和 2014),还 有多年在PythonBrasil(在巴西举办的 PyCon)以及 FISL(南半球最大 的 FLOSS 大会)上做过的15次演讲。Ramalho是Python软件基金会的成员,还是巴西第一个众创空间Garoa Hacker Clube的联合创始人。他也是培训公司Python.pro.br的共同所有人。
正文之前
Python官方教程的开头是这样写的:“Python是一门既容易上手又强大的编程语言。”这句话本身并无大碍,但需要注意的是,正因为它既好学又好用,所以很多Python程序员只用到了其强大功能的一小部分。
在学校里,抑或是那 些入门书上,教授者往往会有意避免只跟语言本身相关的特性。
人们总是倾向于寻求自己熟悉的东西。受到其他语言的影响,你大概能猜到Python会支持正则表达式,然后就会去查阅文档。但是如果你从来没见过元组拆包(tuple unpacking),也没 听过描述符(descriptor)这个概念,那么估计你也不会特地去搜索它们,然后就永远失去了使用这些Python独有的特性的机会。这也是本书试图解决的一个问题。
这本书并不是一本完备的Python使用手册,而是会强调Python作为编程语言独有的特性,这些特性或者是只有Python才具备的,或者是在其他大众语言里很少见的。Python语言核心以及它的一些库会是本书的重点。尽管Python的包索引现在已经有6万多个库了,而且其中很多都异常实用,但是我几乎不会提到Python标准库以外的包。
本书的目标读者是那些正在使用Python,又想熟悉Python3的程序员。
如果在学习Python的过程中过早接触本书的内容,你可能会误以为所有的Python代码都应该利用特殊方法和元编程(metaprogramming)技巧。我们知道,不成熟的抽象和过早的优化一样,都会坏事。
知道有什么现成的工具可用,能避免重新发明轮子。
从1998年起,我一直在使用Python,也做Python教学,另外还一直在为它辩护。我一直都很享受这个过程,尤其是喜欢研究Python同其他语言在设计和理论上的不同。
Josef Hartwig设计的包豪斯国际象棋套装体现了最佳的设计理念:美观、简洁而清晰。有一位建筑师父亲,以及一位字体设计师弟弟,Guido van Rossum设计出了一门经典的编程语言。我之所以热衷于教授Python,也正是因为它的美观、简洁和清晰。
(前言成功吊起了胃口,入门总是容易,真正的高手和大师却总是少数,这定律哪都适用!Python也是博大精深的。看看大师是怎么介绍Python的核心知识,好好学习。)
2017.11.25 阅读
第1章 Python数据模型
很多书把数据模型叫做对象模型,对象模型指的是编程语言中对象的属性。这正好是Python数据模型所要描述的概念。本书中一直都会用数据模型这个词,首先是因为在 Python文档里对这个词有偏爱,另外一个原因是Python语言参考手册中与这里讨论的内容最相关的一章的标题就是数据模型。
元对象协议是对象模型的同义词,它们的意思都是构建核心语言的API。
Python中的特殊方法有人也叫做魔法方法。(因为Ruby社区相应的就叫魔法方法)
Python最好的品质之一是一致性。
数据模型其实是对Python框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。
用一个非常简单的例子来展示如何实现 __getitme__
和 __len__
这两个特殊方法,通过这个例子我们也能见识到特殊方法的强大。
# -*- coding: utf-8 -*-
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
''' 定义len() '''
return len(self._cards)
def __getitem__(self, position):
''' 定义下标index访问 '''
return self._cards[position]
deck = FrenchDeck()
from random import choice
print(choice(deck))
# __len__
print(len(deck))
# __getitem__
print(deck[:3])
print(deck[12::13])
# 反向迭代 reversed 返回是迭代对象 注意同一个迭代对象只能遍历一次
# print(reversed(deck))
for card in reversed(deck):
print(card)
# 迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方法,
# 那么in运算符就会按顺序做一次迭代搜索。于是,in运算符可以用在我们的
# FrenchDeck 类上,因为它是可迭代的:
print(Card('Q', 'hearts') in deck)
print(Card('Q', 'beasts') in deck)
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
''' 排序函数算法:52张牌 0-51 数值对应的下标x4+花色权值'''
rank_value = FrenchDeck.ranks.index(card.rank)
# print(rank_value, len(suit_values), suit_values[card.suit])
return rank_value * len(suit_values) + suit_values[card.suit]
card = choice(deck)
print(card)
print(spades_high(card))
for card in sorted(deck, key=spades_high):
print(card)
自Python 2.6开始,namedtuple就加入到Python里,用以构建只有少数属性但是没有方法的对象,比如数据库条目。
在Python 2中,对object的继承需要显式地写为FrenchDeck(object);而在 Python 3中,这个继承关系是默认的。
虽然FrenchDeck隐式地继承了object类,但功能却不是继承而来的。我们通过数据模型和一些合成来实现这些功能。通过实现 __len__
和 __getitem__
这两个特殊方法,FrenchDeck就跟一个Python自有的序列数据类型一样,可以体现出Python的核心语言特性(例如迭代和切片)。同时这个类还可以用于标准库中诸如 random.choice、reversed 和 sorted 这些函数。另外,对合成的运用使得__len__
和 __getitem__
的具体实现可以代理给self._cards这个Python列表(即 list对象)。
按照目前的设计,FrenchDeck是不能洗牌的,因为这摞牌是不可变的(immutable):卡牌和它们的位置都是固定的,除非我们破坏这个类的封装性,直接对_cards进行操作。其实只需要一行代码来实现__setitem__
方法,洗牌功能就不是问题了。
特殊方法的存在是为了被Python解释器调用的,你自己并不需要调用它们。在执行 len(my_object) 的时候,如果my_object是一个自定义类的对象,那么Python会自己去调用其中由你实现的 __len__
方法。
然而如果是Python内置的类型,比如列表(list)、字符串(str)、 字节序列(bytearray)等,那么CPython会抄个近路,__len__
实际上会直接返回 PyVarObject里的ob_size属性。PyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值比调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的,比如for i in x: 这个语句,背后其实用的是iter(x),而这个函数的背后则是x.__iter__
()方法。当然前提是这个方法在x中被实现了。
通常你的代码无需直接使用特殊方法。唯一的例外可能是 __init__
方法,你的代码里可能经常会用到它,目的是在你自己的子类的 __init__
方法中调用超类的构造器。
通过内置的函数(例如 len、iter、str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常它们的速度更快。
一个简单的二维向量类实现:
# -*- coding: utf-8 -*-
# hypot求直角三角形的斜边长度
from math import hypot
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vector(%r, %r)' % (self.x, self.y)
def __abs__(self):
return hypot(self.x, self.y)
def __bool__(self):
''' 向量的模不等于0则返回True否则返回False '''
return bool(abs(self))
# 更高效的实现
# return bool(self.x or self.y)
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2) # Vector(4, 5)
v = Vector(3, 4)
print(abs(v)) # 5.0
虽然代码里有6个特殊方法,但这些方法(除了 __init__
)并不会在这个类自身的代码中使用。即便其他程序要使用这个类的这些方法,也不会直接调用它们,就像我们在上面的控制台对话中看到的。上文也提到过,一般只有Python的解释器会频繁地直接调用这些方法。
% 和 str.format这两种格式化字符串的手段在本书中都会使用。其实整个Python社区都在同时使用这两种方法。个人来讲,我越来越喜欢str.format了,但是Python程序员更喜欢简单的%。因此,这两种形式并存的情况还会持续下去。
__repr__
所返回的字符串应该准确、无歧义,__repr__
和 __str__
的区别在于,后者是在str()函数被使用,或是在用print函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这两个特殊方法中的一个,__repr__
是更好的选择,因为如果一个对象没有__str__
函数,而Python又需要调用它的时候,解释器会用__repr__
作为替代。
如果x是一个内置类型的实例,那么len(x)的速度会非常快。背后的原因是CPython会直接从一个C结构体里读取对象的长度,完全不会调用任何方法。
len之所以不是一个普通方法,是为了让Python自带的数据结构可以走后门,abs也是同理。如果把abs和len都看作一元运算符的话,你也许更能接受它们——虽然看起来像面向对象语言中的函数,但实际上又不是函数。计算s中x出现的次数。在Python里对应的写法是s.count(x)。注意这里的s是一个序列类型。
本章小结: 通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具Python风格的代码。
Python对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过 __repr__
和 __str__
来满足这个要求。前者方便我们调试和记录日志,后者则是给终端用户看的。这就是数据模型中存在特殊方法 __repr__
和 __str__
的原因。
对序列数据类型的模拟是特殊方法用得最多的地方,这一点在FrenchDeck类的示例中有所展现。
对本章内容和本书主题来说,Python语言参考手册里的Data Model一章是最符合规范的知 识来源。
2017.11.26 阅读
第2章 序列构成的数组
Python从ABC那里继承了序列的泛型操作、内置的元组和映射类型、用缩进来架构的源码、无需变量声明的强类型,等等,还有用统一的风格去处理序列数据这一特点。不管是哪种数据结构,字符串、列表、字节序列、数组、XML元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。
本章讨论的内容几乎可以应用到所有的序列类型上,从我们熟悉的list,到Python 3中特有的str和bytes。我还会特别提到跟列表、元组、数组以及队列有关的话题。但是Unicode 字符串和字节序列这方面的内容被放在了第4章。
Python标准库用C实现了丰富的序列类型,列举如下。
容器序列 list、tuple 和 collections.deque 这些序列能存放不同类型的数据。 扁平序列 str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。
容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。
序列类型还能按照能否被修改来分类。
可变序列(MutableSequence) list、bytearray、array.array、collections.deque 和 memoryview。
不可变序列(Sequence) tuple、str 和 bytes。
Sequence和MutableSequence的抽象基类是Abstract Base Class,即ABC
列表推导(list comprehension)和生成器表达式(generator expression) 列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。如果你的代码里并不经常使用它们,那么很可能你错过了许多写出可读性更好且更高效的代码的机会。
Python会忽略代码里 []、{} 和 () 中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符 \。
列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在Python 3 中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的 上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。
列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新建一个列表。Python内置的filter和map函数组合起来也能达到这一效果,但是可读性上打了不小的折扣。
filter 和 map 合起来能做的事情,列表推导也可以做,而且还不需要借助难以理解和阅读的 lambda表达式。
用列表推导和 map/filter 组合来创建同样的表单:
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
map/filter 组合起来用不一定比列表推导快。
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存。
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
用生成器表达式初始化元组和数组:
symbols = '$¢£¥€¤'
# 示例1 构造 (36, 162, 163, 165, 8364, 164)
tuple(ord(symbol) for symbol in symbols)
# 示例2 构造array('I', [36, 162, 163, 165, 8364, 164])
import array
array.array('I', (ord(symbol) for symbol in symbols))
示例1: 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。 示例2: array的构造方法需要两个参数,因此括号是必需的。array构造方法的第一个参数指定了数组中数字的存储方式。
与列表推导相比,用到生成器表达式之后,内存里不会留下一个列表,因为生成器表达式会在每次for 循环运行时才生成一个组合。如果要计算两个各有1000个元素的列表的笛卡儿积,生成器表达式就可 以帮忙省掉运行for循环的开销,即一个含有100万个元素的列表。
使用生成器表达式计算笛卡儿积:
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes):
print(tshirt)
有些Python入门教程把元组称为“不可变列表”,然而这并没有完全概括元组的特点。除了用作不可变的列表,它还可以用于没有字段名的记录。鉴于后者常常被忽略,我们先来看看元组作为记录的功用。
如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息就变得非常重要了。
lax_coordinates = (33.9425, -118.408056)
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),
('ESP', 'XDA205856')]
for passport in sorted(traveler_ids):
print('%s/%s' % passport)
for country, _ in traveler_ids:
print(country)
for循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因为元组中第二个元素对我们没有什么用,所以它赋值给“_”占位符。
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。
可以用 * 运算符把一个可迭代对象拆开作为函数的参数:
t = (20, 8)
func(*t)
这里元组拆包的用法则是让一个函数可以用元组的形式返回多个值,然后调用函数的代码就能轻松地接受这些返回值。比如 os.path.split() 函数就会返回以路径和最后一个文件名组成的元组 (path, last_part):
import os
_, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
在元组拆包中使用 * 也可以帮助我们把注意力集中在元组的部分元素上。 用*来处理剩下的元素。
在Python中,函数用 *args 来获取不确定数量的参数算是一种经典写法了。Python 3 里,这个概念被扩展到了平行赋值中:
a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
在Python 3之前,元组可以作为形参放在函数声明中,例如def fn(a, (b, c), d):。然而Python 3不再支持这种格式。
元组已经设计得很好用了,但作为记录来用的话,还是少了一个功能:我们时常会需要给记录中的字段命名。namedtuple命函数的出现帮我们解决了这个问题。
具名元组namedtuple:
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。用 namedtuple 构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为Python不会用 __dict__
来存放这些实例的属性。
作为不可变列表的元组
除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。还有一个例外,元组没有 __reversed__
方法,但是这个方法只是个优化而已,reversed(my_tuple) 这个用法在没有 __reversed__
的情况下也是合法的。
在Python里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。
在切片和区间操作里不包含区间范围的最后一个元素是Python的风格, 这个习惯符合 Python、C 和其他语言里以0作为起始下标的传统。
a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)
Python序列的+、*
、+=、*=
用返回 None 来表示就地改动这个惯例有个弊端,那就是调用者无法将其串联起来。而返回一个新对象的方法(比如说 str 里的 所有方法)则正好相反,它们可以串联起来调用,从而形成连贯接口(fluent interface)。
Python的排序算法——Timsort——是稳定的,意思是就算两个元素比不出大小,在每次排序的结果里它们的相对位置是固定的。
已排序的序列可以用来进行快速搜索,而标准库的 bisect 模块给我们提供了二分查找算法。bisect 模块包含两个主要函数,bisect 和 insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。如果在你的代码里,包含操作(比如检查一个元素是否出现在一个集合中)的频率很高,用 set(集合)会更合适。set专为检查元素是否存在做过优化。但是它并不是序列,因为 set 是无序的。
数组的使用示例
内存视图memoryview
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview 的概念受到了 NumPy 的启发。内存视图其实是泛化和去数学化的 NumPy 数组。它让你在不需要复制内容的前提下,在数据结构之间共享内存。其中数据结构可以是任何形式,比如 PIL 图片、SQLite 数据库和 NumPy 的数组,等等。这个功能在处理大型数据集合的时候非常重要。
memoryview的使用示例
如果利用数组来做高级的数字处理是你的日常工作,那么 NumPy 和 SciPy 应该是你的常用武器。下面就是对这两个库的简单介绍。
整本书我都在强调如何最大限度地利用 Python 标准库。但是 NumPy 和 SciPy 的优秀让我觉得偶尔跑个题来谈谈它们也是很值得的。
在介绍完扁平序列(包括标准数组和 NumPy 数组)之后,让我们把目光投向 Python 中可以取代列表的另外一种数据结构:队列。
除了 deque 之外,还有些其他的 Python 标准库也有对队列的实现。
queue: 提供了同步(线程安全)类 Queue、LifoQueue 和 PriorityQueue,不同的线程可以利用这些数据类型来交换信息。这三个类的构造方法都有一个可选参数 maxsize,它接收正整数作为输入值,用来限定队列的大小。但是在满员的时候,这些类不会扔掉旧的元素来腾出位置。相反,如果队列满了,它就会被锁住,直到另外的线程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量。
multiprocessing: 这个包实现了自己的 Queue,它跟 queue.Queue 类似,是设计给进程间通信用的。同时还有一个专门的 multiprocessing.JoinableQueue 类型,可以让任务管理变得更方便。
asyncio: Python 3.4 新提供的包,里面有 Queue、LifoQueue、PriorityQueue 和 JoinableQueue,这些类受到 queue 和 multiprocessing 模块的影响,但是为异步编程里的任务管理提供了专门的便利。
heapq: 跟上面三个模块不同的是,heapq 没有队列类,而是提供了 heappush 和 heappop 方法,让用户可以把可变序列当作堆队列或者优先队列来使用。
本章小结:
要想写出准确、高效和地道的 Python 代码,对标准库里的序列类型的掌握是不可或缺的。
Python 序列类型最常见的分类就是可变和不可变序列。但另外一种分类方式也很有用,那就是把 它们分为扁平序列和容器序列。扁平序列因为只能包含原子数据类型,比如整数、浮点 数或字符,所以不能嵌套使用。有些对象里包含对其他对象的引用;这些对象称为容器。特别使用了“容器序列”这个词,因为 Python 里有是容器但并非序列的类型,比如 dict 和 set。容器序列可以嵌套着使用,因为容器里的引用可以针对包括自身类型在内的任何类型。
列表推导和生成器表达式则提供了灵活构建和初始化序列的方式,这两个工具都异常强大。如果你还不能熟练地使用它们,可以专门花时间练习一下。它们其实不难,而且用起来让人上瘾。
除了列表和元组,Python 标准库里还有 array.array。另外,虽然 NumPy 和 SciPy 都不是 Python 标准库的一部分,但稍微学习一下它们,会让你在处理大规模数值型数据时如有神助。
collections.deque 这个类型,它具有灵活多用和线程安全的特性。
Python 入门教材往往会强调列表是可以同时容纳不同类型的元素的,但是实际上这样做并没有什么特别的好处。在 Python 3 中,如果列表里的东西不能比较大小,那么我们就不能对列表进行排序。元组则恰恰相反,它经常用来存放不同类型的的元素。这也符合它的本质,元组就是用作存放彼此之间没有关系的数据的记录。
key参数很妙: list.sort、sorted、max 和 min 函数的 key 参数是一个很棒的设计。其他语言里的排序函数需要用户提供一个接收两个参数的比较函数作为参数,像是 Python 2 里的 cmp(a, b)。用 key 参数能把事情变得简单且高效。说它更简单,是因为只需要提供一个单参数函数来提取或者计算一个值作为比较大小的标准即可,而 Python 2 的这种设计则需要用户写一个返回值是—1、0 或者 1 的双参数函数。说它更高效,是因为在每个元素上,key 函数只会被调用一次。而双参数比较函数则在每一次两两比较的时候都会被调用。诚然,在排序的时候,Python 总会比较两个键(key),但是那一阶段的计算会发生在 C 语言那一层,这样会比调用用户自定义的 Python 比较函数更快。
另外,key参数也能让你对一个混有数字字符和数值的列表进行排序。你只需要决定到底是把字符看作数值,还是把数值看作字符:
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
>>> sorted(l, key=int)
[0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
>>> sorted(l, key=str)
[0, '1', 14, 19, '23', 28, '28', 5, 6, '9']
Oracle、Google 和 Timbot 之间的八卦: sorted 和 list.sort 背后的排序算法是 Timsort,它是一种自适应算法,会根据原始数据的顺序特点交替使用插入排序和归并排序,以达到最佳效率。这样的算法被证明是很有效的,因为来自真实世界的数据通常是有一定的顺序特点的。维基百科上有一个条目是关于这个算法的。(统计学的思想,实用是王道)
Timsort 在 2002 年的时候首次用在 CPython 中;自 2009 年起,Java 和 Android 也开始使用这个算法。
Timsort 的创始人是 Tim Peters,他同时也是一位高产的 Python 核心开发者。由于他贡献了太多代码,以至于很多人都说他其实是人工智能,他也就有了“Timbot”这一绰号。在Python Humor里可以读到相关的故事。Tim 也是“Python 之禅”(import this)的作者。(膜拜大师)
2017.11.26 & 2017.11.27 阅读
第3章 字典和集合
字典这个数据结构在Python里无处不在,不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在 __builtins__.__dict__
模块中。正是因为字典至关重要,Python 对它的实现做了高度优化,而散列表则是字典类型性能出众的根本原因。集合(set)的实现其实也依赖于散列表。
2017.11.27 阅读
第4章 文本和字节序列
人类使用文本,计算机使用字节序列。 Python 3 明确区分了人类可读的文本字符串和原始的字节序列。隐式地把字节序列转换成 Unicode 文本已成过去。
memoryview 类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列,例如 Python Imaging Library(PIL)就是这样处理图像的。
Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如 ‘utf_8’, 而且经常有几个别名,如 ‘utf8’、’utf-8’ 和 ‘U8’。这些名称可以传 给 open()、str.encode()、bytes.decode() 等函数的 encoding 参数。
使用不同的编解码器把相同的文本编码成不同的字节序列,差异是很大的:
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
... print(codec, 'El Niño'.encode(codec))
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
unicode仍有不尽如人意的地方:
- 文本规范化(即为了比较而把文本转换成统一的表述)
- 排序
因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
例如,“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样:
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False
U+0301 是 COMBINING ACUTE ACCENT,加在“e”后面得到“é”。在 Unicode 标准中,’é’ 和 ‘e\u0301’ 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。
这个问题的解决方案是使用 unicodedata.normalize 函数提供的 Unicode 规范化。这个函数的第一个参数是这 4 个字符串中的一 个:’NFC’、’NFD’、’NFKC’ 和 ‘NFKD’。
西方键盘通常能输出组合字符,因此用户输入的文本默认是 NFC 形式。不过,安全起见,保存文本之前,最好使用 normalize(‘NFC’, user_text) 清洗字符串。NFC 也是 W3C 推荐的规范化形式。
大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold() 方法(Python 3.3 新增)支持。
自 Python 3.4 起,str.casefold() 和 str.lower() 得到不同结果的有 116 个码位。Unicode 6.3 命名了 110 122 个字符,这只占 0.11%。
与 Unicode 相关的任何问题一样,大小写折叠是个复杂的问题,有很多语言上的特殊情况,但是 Python 核心团队尽力提供了一种方案,能满足大多数用户的需求。
Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽如人意。例如:重音字母
标准库提供的国际化排序方案可用,但是似乎只支持 GNU/Linux(可能也支持 Windows,但你得是专家)。即便如此,还要依赖区域设置,而这会为部署带来问题。(确实不方便)
幸好,有个较为简单的方案:PyPI 中的 PyUCA 库。
James Tauber,一位高产的 Django 贡献者,他一定是感受到了这一痛点,因此开发了PyUCA库,这是 Unicode 排序算法(Unicode Collation Algorithm,UCA)的纯 Python 实现。
一个简单使用示例:
>>> import pyuca
>>> coll = pyuca.Collator()
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=coll.sort_key)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
这样做更友好,而且恰好可用。
小结:
讨论 Unicode 规范化时,我经常使用“往往”“多数”和“通常”等不确 定的修饰语。很遗憾,我不能提供更可靠的建议,因为 Unicode 规 则有很多例外,很难百分之百确定。研究 Unicode 几小时之后,我猜测的原因是:Unicode 异常复杂,充满特殊情况,而且要覆盖各种人类语言和产业标准策略。
Python 官方文档对字符串的码位在内存中如何存储避而不谈。毕竟,这是实现细节。理论上,怎么存储都没关系:不管内部表述如何,输出时每个字符串都要编码成字节序列。
在内存中,Python 3 使用固定数量的字节存储字符串的各个码位,以便高效访问各个字符或切片。
从 Python 3.3 起,创建 str 对象时,解释器会检查里面的字符,然后为该字符串选择最经济的内存布局:如果字符都在 latin1 字符集中,那就使用 1 个字节存储每个码位;否则,根据字符串中的具体字符,选择 2 个或 4 个字节存储每个码位。这是简述,完整细节参阅PEP 393—Flexible String Representation。
灵活的字符串表述类似于 Python 3 对 int 类型的处理方式:如果一个整数在一个机器字中放得下,那就存储在一个机器字中;否则解释器切换成变长表述,类似于 Python 2 中的 long 类型。这种聪明的做法得到推广,真是让人欢喜!
2017.11.28 阅读
第5章 一等函数
本章的目标是探讨 Python 函数的一等本性。这意味着,我们可以把函数赋值给变量、传给其他函数、存储在数据结构中,以及访问函数的属性,供框架和一些工具使用。高阶函数是函数式编程的重要组成部分,即使现在不像以前那样经常使用 map、filter 和 reduce 函数了,但是还有列表推导(以及类似的结构,如生成器表达式)以及 sum、all 和 any 等内置的归约函数。Python 中常用的高阶函数有内置函数 sorted、min、max 和 functools. partial。
Python 有 7 种可调用对象,从 lambda 表达式创建的简单函数到实现 __call__
方法的类实例。这些可调用对象都能通过内置的 callable() 函数检测。每一种可调用对象都支持使用相同的丰富句法声明形式参数,包括仅限关键字参数和注解——二者都是 Python 3 引入的新特性。
Python 函数及其注解有丰富的属性,在 inspect 模块的帮助下,可以读取它们。例如,Signature.bind 方法使用灵活的规则把实参绑定到形参上,这与 Python 使用的规则一样。
最后,本章介绍了 operator 模块中的一些函数,以及 functools.partial 函数,有了这些函数,函数式编程就不太需要功能有限的 lambda 表达式了。
有人问Guido Python 的哪些特性是从其他语言借鉴而来的。他答道:“Python 中一切好的特性都是从其他语言中借鉴来的。”(有趣,应该算是事实吧,好的东西当然要借鉴并发扬之,正如科学,所有的科学都是继承和发展而来的。)
为 Python 提供一等函数打开了函数式编程的大门,不过这并不是 Guido 的目的。他说map、filter 和 reduce 的最初目的是为 Python 增加 lambda 表达式。
lambda、map、filter 和 reduce 首次出现在 Lisp 中,这是最早的一门函数式语言。然而,Lisp 没有限制在 lambda 表达式中能做什么,因为 Lisp 中的一切都是表达式。 Python 使用的是面向语句的句法,表达式中不能包含语句,而很多语言结构都是语句,例如 try/catch,我编写 lambda 表达式时最想念这个语句。Python 为了提高句法的可读性,必须付出这样的代价。Lisp 有很多优点,可读性一定不是其中之一。 讽刺的是,从另一门函数式语言(Haskell)中借用列表推导之后, Python 对 map、filter,以及 lambda 表达式的需求极大地减少了。(对coder来说,可读性可以说是最重要的。。赞同Python的取舍!)
Python 作为一门易于使用、学习和教授的语言并非偶然,有 Guido 在为我们把关。(有一位仁慈的独裁者并不是坏事,想想苹果的闭源专治,却能把手机和电脑做到最流畅最好用!类比并不恰当,也能说明一定的问题吧。。)
综上,从设计上看,不管函数式语言的定义如何,Python 都不是一门函数式语言。Python 只是从函数式语言中借鉴了一些好的想法。
函数有名称,栈跟踪更易于阅读。匿名函数是一种便利的简洁方式,人们乐于使用它们,但是有时会忘乎所以,尤其是在鼓励深层嵌套匿名函数的语言和环境中,如 JavaScript 和 Node.js。匿名函数嵌套的层级太深,不利于调试和处理错误。 Python 中的异步编程结构更好,或许就是因为 lambda 表达式有局限。(Node.js的代码要想写得简洁好看,必须要求coder是高手,除非业务逻辑很简单。这个要求简直是致命的和非主流的,相比Python的简洁和美好的哲学向往来说。)
2017.11.29 阅读
第6章 使用一等函数实现设计
经典的《设计模式:可复用面向对象软件的基础》一书出版几年后 Peter Norvig 指出,“在 Lisp 或 Dylan 中,23 个设计模式中有 16 个的实现方式比 C++ 中更简单,而且能保持同等质量,至少各个模式的某些 用途如此”。Python 有些动态特性与 Lisp 和 Dylan 一样,尤其是本书这一部分着重讨论的一等函数。
很多情况下,在 Python 中使用函数或可调用对象实现回调更自然,这比模仿 Gamma、Helm、Johnson 和 Vlissides 在书中所述的“策略”或“命令”模式要好。本章对“策略”模式的重构和对“命令”模式的讨论是为了通过示例说明一个更为常见的做法:有时,设计模式或 API 要求组件实现单方法接口,而那个方法的名称很宽泛,例 如“execute”“run”或“doIt”。在 Python 中,这些模式或 API 通常可以使用一等函数或其他可调用的对象实现,从而减少样板代码。
《Python 高级编程》(Tarek Ziadé著)是市面上最好的 Python 中级书, 第 14 章“有用的设计模式”从 Python 程序员的视角介绍了 7 种经典模式。
与 Java、C++ 或 Ruby 相比,Python 设计模式方面的书籍都很薄。
Peter Norvig 那次设计模式演讲想表达的观点是,“命令”和“策略”模式 (以及“模板方法”和“访问者”模式)可以使用一等函数实现,这样更简单,甚至“不见了”,至少对这些模式的某些用途来说是如此。
2017.11.29 & 2017.11.30 阅读
第7章 函数装饰器和闭包
nonlocal 是新近出现的保留关键字,在 Python 3.0 中引入。作为 Python 程序员,如果严格遵守基于类的面向对象编程方式,即便不知道这个关键字也不会受到影响。然而,如果你想自己实现函数装饰器,那就必须了解闭包的方方面面,因此也就需要知道 nonlocal。
除了在装饰器中有用处之外,闭包还是回调式异步编程和函数式编程风格的基础。
本章的最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。
@decorate
def target():
print('running target()')
# 以上代码等价于
def target():
print('running target()')
target = decorate(target)
两种写法的最终结果一样:上述两个代码片段执行完毕后得到的 target 是 decorate(target) 返回的函数。
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。
简单的装饰器的实现,以及三个好用的Python自带装饰器:functools.wraps、functools.lru_cache、functools.singledispatch
小结:
本章介绍了很多基础知识,虽然学习之路崎岖不平,我还是尽可能让路途平坦顺畅。毕竟,我们已经进入元编程领域了。
开始,我们先编写了一个没有内部函数的 @register 装饰器;最后,我们实现了有两层嵌套函数的参数化装饰器 @clock()。
尽管注册装饰器在多数情况下都很简单,但是在高级的 Python 框架中却有用武之地。我们使用注册方式对第 6 章的“策略”模式做了重构。
参数化装饰器基本上都涉及至少两层嵌套函数,如果想使用 @functools.wraps 生成装饰器,为高级技术提供更好的支持,嵌套层级可能还会更深,比如前面简要介绍过的叠放装饰器。
我们还讨论了标准库中 functools 模块提供的两个出色的函数装饰器:@lru_cache() 和 @singledispatch。
若想真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的 nonlocal 声明。掌握闭包和 nonlocal 不仅对构建装饰器有帮助,还能协助你在构建 GUI 程序时面向事件编程,或者使用回调处理异步 I/O。
2017.12.01 阅读
第8章 对象引用、可变性和垃圾回收
del 语句删除名称(引用),而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
如果两个对象相互引用,当它们的引用只存在二者之间时,垃圾回收程序会判定它们都无法获取,进而把它们都销毁。
在 CPython 中,对象的引用数量归零后,对象会被立即销毁。如果除了循环引用之外没有其他引用,两个对象都会被销毁。某些情况下,可能需要保存对象的引用,但不留存对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一种低层机制,是 weakref 模块中 WeakValueDictionary、WeakKeyDictionary 和 WeakSet 等有用的集合类,以及 finalize 函数的底层支持。
2017.12.02 阅读
第9章 符合Python风格的对象
如果你是从 Python 2 转过来的,记住,在 Python 3 中,__repr__
、__str__
和 __format__
都必须返回 Unicode 字符串(str 类型)。只有 __bytes__
方法应该返回字节序列 (bytes 类型)。
repr() 以便于开发者理解的方式返回对象的字符串表示形式。 str() 以便于用户理解的方式返回对象的字符串表示形式。
__x 私有属性
私有属性的名称会被“改写”,在前面加上下划线和类名,如
__x
会被改写成_Class__x
名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事
不是所有 Python 程序员都喜欢名称改写功能,也不是所有人都喜欢self.__x这种不对称的名称。
约定使用一个下划线前缀编写“受保护”的属性(如 self._x)。
Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多Python程序员严格遵守的约定,他们不会在类外部访问这种属性。遵守使用一个下划线标记对象的私有属性很容易,就像遵守使用全大写字母编写常量那样容易。
默认情况下,Python在各个实例中名为 __dict__
的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__
类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。在类中定义 __slots__
属性的目的是告诉解释器:“这个类中的所有实例属性都在这了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__
属性。如果有数百万个实例同时活动,这样做能节省大量内存。但是,如果要处理数百万个数值对象,应该使用NumPy数组。NumPy 数组能高效使用内存,而且提供了高度优化的数值处理函数,其中很多都一次操作整个数组。
综上,__slots__
属性有些需要注意的地方,而且不能滥用。__slots__
是用于优化的,不是为了约束程序 员。粗心的优化甚至比提早优化还糟糕。(使用__slots__
优化之前先要先彻底弄清楚其使用细节。)
在类的属性的设计上,Java与Python截然相反,Java 程序员不能先定义简单的公开属性,然后在需要时再实现特性,因为 Java 语言没有特性。因此,在 Java 中编写读值方法和设值方法是常态,就算这些方法没做什么有用的事情也得这么做,因为 API 不能从简单的公开属性变成读值方法和设值方法。
本书的技术审校 Alex Martelli 指出,到处都使用读值方法和设值方法是愚蠢的行为。
维基的发明人和极限编程先驱 Ward Cunningham 建议问这个问题:“做这件事最简单的方法是什么?”意即,我们应该把焦点放在目标上。提前实现设值方法和读值方法偏离了目标。在 Python 中,我们可以先使用公开属性,然后等需要时再变成特性。
(嗯,从设计哲学上来说,Python更简洁扼要,更加灵活。)
Java 中的访问控制修饰符基本上也是安全措施,不能保证万无一失——至少实践中是如此。因此,安心享用 Python 提供的强大功能吧,放心去用吧!
(语言设计的不同,归根结底是设计哲学和思想上的不同,并不是说一定会有优劣。如同信仰一样。)
2017.12.22 阅读
第10章 序列的修改、散列和切片
在第九章的基础上,实现类的序列性、散列和切片的功能,越来越符合Python的风格。 此章略读。
2017.12.25 阅读