目录
前言
受到新型冠状肺炎影响,在家闭关修炼,所以学习一下《流畅的Python》这本python高阶书,个人愚见:《流畅的Python》这本书绝大部分都是讲解python的偏底层实现原理和方法,因此不太适合新手入门,如果想学习python基础的话建议访问我的另外一篇博客:https://blog.csdn.net/GuoQiZhang/article/details/90142322
最后希望疫情快快结束,武汉加油,中国加油!
第 1 章 Python 数据模型(系统级API)
1.1、特殊方法
数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。
Python系统级API接口(特殊方法)使用 ‘’两个下划线‘’ 表示,这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:
- 迭代
- 集合类
- 属性访问
- 运算符重载
- 函数和方法的调用
- 对象的创建和销毁
- 字符串表示形式和格式化
- 管理上下文(with 语块)
例如
obj[key]
的底层执行方法为:obj.__getitem__(key)
;len(obj)
底层实现方法为:obj.__len__()
;
1.2、如何使用特殊方法
首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。通过内置的函数(例如 len、iter、str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。
- 字符串:将一个对象用字符串的形式表示时可以使用
__repr__()
或__str__()
特殊方法返回,两者基本无差别。 - 算数运算符:加法符号(+)使用
__add__()
;乘法符号(*)使用__mul__()
; - 布尔值:
bool()
方法调用__bool__()
方法
1.3、特殊方法一览表
- 跟运算符无关的特殊方法
类别 | 方法名 |
---|---|
字符串 / 字节序列 | __repr__ , __str__ ,__format__ , __bytes__ |
数值转换 | __abs__ 、__bool__ 、__complex__ 、__int__ 、__float__ 、__hash__ 、__index__ |
集合模拟 | __len__ 、__getitem__ 、__setitem__ 、__delitem__ 、__contains__ |
迭代枚举 | __iter__ 、__reversed__ 、__next__ |
可调用模拟 | __call__ |
上下文管理 | __enter__ 、__exit__ |
实例创建和销毁 | __new__ 、__init__ 、__del__ |
属性管理 | __getattr__ 、__getattribute__ 、__setattr__ 、__delattr__ 、__dir__ |
属性描述符 | __get__ 、__set__ 、__delete__ |
跟类相关的服务 | __prepare__ 、__instancecheck__ 、__subclasscheck__ |
- 跟运算符相关的特殊方法
类别 | 方法名和对应的运算符 |
---|---|
一元运算符 | __neg__ - 、__pos__ +、__abs__ abs() |
众多比较运算符 | __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >= |
算术运算符 | __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmod()、__pow__ ** 或pow()、__round__ round() |
反向算术运算符 | __radd__ 、__rsub__ 、__rmul__ 、__rtruediv__ 、__rfloordiv__ 、__rmod__ 、__rdivmod__ 、__rpow__ |
增量赋值算术运算符 | __iadd__ 、__isub__ 、__imul__ 、__itruediv__ 、__ifloordiv__ 、__imod__ 、__ipow__ |
位运算符 | __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |
反向位运算符 | __rlshift__ 、__rrshift__ 、__rand__ 、__rxor__ 、__ror__ |
增量赋值位运算符 | __ilshift__ 、__irshift__ 、__iand__ 、__ixor__ 、__ior__ |
1.4、为什么len不是普通方法
len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门,abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”
第 2 章、序列构成的列表和元组(数组)
2.1 内置序列类型概览
Python 标准库用 C 实现了丰富的序列类型;主要分为 容器序列 和 扁平序列,二者举例和区别如下:
分类 | 举例 | 区别 |
---|---|---|
容器序列 | list、tuple 、collections.deque | 能存放不同类型的数据、存放的是它们所包含的任意类型的对象的引用 |
扁平序列 | str、bytes、bytearray、memoryview 、array.array | 只能容纳一种类型、存放的是值而不是引用,占用一段连续的内存空间。 |
还可以根据数据是否修改分成 可变序列 和 不可变序列:
分类 | 举例 |
---|---|
可变序列 | list、bytearray、array.array、collections.deque 和 memoryview |
不可变序列 | tuple、str 和 bytes |
2.2、推导式和生成器
首先需要搞明白何为推导式、生成器表达式,别中了科技以造词为主的圈套。本节顺序:什么是推导式、推导式的分类、什么是生成器、推导式与生成器的异同。
2.2.1、什么是推导式
推导式是Python语言特有的一种语法,相当于语法糖的存在,可以帮助我们快速生成具有某种规律的列表、字典或集合。下面以列表为例子说明推导式公式:
lis = [out_result for out_exp in input_list if out_exp == 0]
out_result: 列表生成元素表达式,可以是有返回值的函数。
for out_exp in input_list: 迭代input_list将out_exp传入out_result表达式中。
if out_exp == 0: 条件过滤,可有可无
2.2.2推导式的分类
推导式主要分三种:列表推导式、字典推导式、集合推导式。举例如下:
# 列表推导式
lis = [i for i in range(5)]
print(type(lis), lis)
<class 'list'> [0, 1, 2, 3, 4]
# 字典推导式
dic = {i: 2 * i for i in range(5)}
print(type(dic), dic)
<class 'dict'> {0: 0, 1: 2, 2: 4, 3: 6, 4: 8}
# 集合推导式
se = {3 * i for i in range(5)}
print(type(se), se)
<class 'set'> {0, 3, 6, 9, 12}
相信很多人会想,python基础数据类型不是还有元组呢嘛?没有元组推导式嘛?于是我们试着写了元组推导式 :
# '元组推导式'
tupl = (3 * i for i in range(5))
print(type(tupl), tupl)
<class 'generator'> <generator object <genexpr> at 0x000001BECD1C1570>
结果发现 元组推导式 的数据类型不是 tuple
而是generator
难道没有元组推导式?我个人认为可以说有也可以说没有,有点道家思想了。。。。。。个人理解:
- 说没有元组推导式是因为上面那个假的元组推导式其实就是我们后面要说的生成器表达式(简称生成器),为什么只有元组如此特殊?这是由元组元素不可变的特性决定的;
- 说有元组推导式是因为元组推导式的正常写法被生成器霸占了,那么只能稍微复杂一点来构造元组推导式了。代码如下:
# 真的元组推导式
tupl = tuple(i for i in range(5))
print(type(tupl), tupl)
<class 'tuple'> (0, 1, 2, 3, 4)
2.2.3、什么是生成器
我们目前已经知道生成器的写法了(就是正常的元组推导式),那么什么是生成器呢?生成器表达式则可以用来创建其他任何类型的序列,这就不同于推导式只能生成python基础类型了,使该语言有一次变得很强大。总结如下:
(1)生成器推导式的结果是一个生成器对象,而不是列表,也不是元组。
(2)使用生成器对象的元素时,可以根据需要将其转化为列表或元组。
(3)可以使用__next__()
或者内置函数访问生成器对象,但不管使用何种方法访问其元素,当所有元素访问结束以后,如果需要重新访问其中的元素,必须重新创建该生成器对象。即生成器的对象好比一份蛋糕,第一个人吃完了就没了,必须重新制作另外一个蛋糕。
(4)生成器对象创建与列表推导式不同的地方就是,生成器推导式是用圆括号创建。
2.2.4、推导式与生成器的异同
- 推导式是将所有的值一次性加载到内存中,而生成器不会将所有的值一次性加载到内存中,延迟计算,一次返回一个结果,它不会一次生成所有的结果,这对大数据量处理,非常有用;
- 列表推导式可以遍历任意次,而生成器只能遍历一次 ,例子如下;
# 演示1:
generator = (i for i in range(5))
print(type(generator))
for i in generator:
print(i)
# 此处 generator 生成器已经为空
for i in generator:
print(i)
# 结果
<class 'generator'>
0
1
2
3
4
# 演示2:
generator = (i for i in range(5))
print(type(generator))
print(list(generator))
# 此处 generator 生成器已经为空
print(list(generator))
# 结果
<class 'generator'>
[0, 1, 2, 3, 4]
[]
2.2.5、生成器转化成推导式
可以将生成器转换成推导式(列表:list(generator)
、集合:set(generator)
)
# eg:生成器转化成推导式
generator = (i for i in range(5))
print(type(generator))
print(list(generator))
generator = (i for i in range(5))
print(type(generator))
print(set(generator))
# 结果
<class 'generator'>
[0, 1, 2, 3, 4]
<class 'generator'>
{0, 1, 2, 3, 4}
2.2.6、笛卡尔积
关于什么是笛卡尔积的问题这里不再展开细说,度娘在此 笛卡尔积
- 推导式子表示笛卡尔积
内部执行顺序为从左到右,例:
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
lis = [(i, j) for i in colors for j in sizes]
print(lis)
# 结果
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
- 生成器表示笛卡尔积
生成器使用迭代器原理,所以必须使用for循环来输出笛卡尔积,例:
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for gen in ((i, j) for i in colors for j in sizes):
print(gen)
# 结果
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
2.3、元组不仅仅是不可变的列表
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。可以让元组可以自动定位;例:
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
lis = [(i, j) for i in colors for j in sizes]
for a, b in lis:
print(a,b)
# 结果
black S
black M
black L
white S
white M
white L
2.3.1、元组拆包
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是:被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用 * 来表示忽略多余的元素;例如:
- eg1:
# eg 1:
tup = (2, 3)
a, b = tup
print(a, b)
# 结果
2 3
- eg2:不使用中间变量交换两个变量的值
# eg 2: 不使用中间变量交换两个变量的值
a , b = 1, 2
b, a = a, b
print(a, b)
# 结果
2 1
- eg3:用 * 运算符把一个可迭代对象拆开作为函数的参数:
# eg 3: 用 * 运算符把一个可迭代对象拆开作为函数的参数
tup = (10, 3)
a, b = divmod(*tup) # 做除法
print(a, b)
# 结果
3 1
- eg4:用*来处理剩下的元素(python3加入)在平行赋值中,
*
前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置:
# eg 4: 用*来处理剩下的元素(python3加入)
a, b, *rest = range(5)
print(a, b, rest)
a, b, *rest = range(3)
print(a, b, rest)
a, b, *rest = range(2)
print(a, b, rest)
a, *body, c, d = range(5)
print(a, body, c, d)
*head, b, c, d = range(5)
print(head, b, c, d)
# 结果
0 1 [2, 3, 4]
0 1 [2]
0 1 []
0 [1, 2] 3 4
[0, 1] 2 3 4
- eg5:嵌套元组拆包:接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应:
tupl = (1, 2, (3, 4))
a, b, c = tupl
print(a, b, c)
# 结果
1 2 (3, 4)
在进行拆包的时候,我们不总是对元组里所有的数据都感兴趣,_
占位符能帮助处理这种情况
2.3.2、具名元组
根据字面意思可知,其是指:带有名字的元组,也就是说元组内的每个元素都具有一个名字,使用 python 标准库 collections 中的工厂函数实现。它接受两个参数:
-
第一个参数表示类的名称;
-
第二个参数是类的字段名。后者可以是可迭代对象,也可以是空格隔开的字符串。然后,我们通过一串参数的形式将参数传递到构造函数中。这样,我们既可以通过字段名访问元素,也可以用索引访问元素。例:
此外具名元组实例对象具有
_fields
属性,其是一个包含这个类所有字段名称的元组例子如下:
# 使用工厂函数声明一个具名元组类
import collections
Student = collections.namedtuple('Student', 'name sex age score')
stu = Student('xiaoming', 'man', 23, (100, 99))
print(stu.name, stu.sex, stu.age, stu.score)
print(stu[0], stu[1], stu[2],stu[3])
print(stu._fields)
# 结果
xiaoming man 23 (100, 99)
xiaoming man 23 (100, 99)
('name', 'sex', 'age', 'score')
2.4、对对象切片
2.4.1、为什么切片和区间会忽略最后一个元素
和Java、C语言一样,切片的区间会忽略最后一个元素;这样做主要有以下几点优点:
- 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:
range(3)
和my_list[:3]
都返回 3 个元素; - 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标
(stop - start)
即可; - 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成
my_list[:x]
和my_list[x:]
就可以了。
2.4.2、切片对象
众所周知,我们可以用 s[a:b:c]
的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值,但实际代码运行是怎样的过程呢?
实际上 a:b:c
这种用法对 seq[start:stop:step]
进行求值的时候,Python 会调用seq.__getitem__(slice(start, stop, step)
,其中 slice()
就是切片对象,例如:
a = '1234567890'
b = slice(0, 10, 2)
print(a[b])
print(a.__getitem__(b))
# 结果
13579
13579
2.4.3、切片赋值
如果把切片放在赋值语句的左边,或把它作为 del
操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作:
l = list(range(10))
print(l)
l[2:5] = [20, 30]
print(l)
del l[5:7]
print(l)
l[3::2] = [11, 22]
print(l)
l[2:5] = 100
print(l)
l[2:5] = [100]
print(l)
# 结果
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 8, 9]
[0, 1, 20, 11, 5, 22, 9]
Traceback (most recent call last):
File "C:/Users/Administrator/Desktop/NeuqAmazon/test.py", line 88, in <module>
l[2:5] = 100
TypeError: can only assign an iterable
[0, 1, 100, 22, 9]
2.5、对序列使用+和*
Python 程序员会默认序列是支持 + 和 * 操作的。通常 + 号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果。
2.5.1、建立由列表组成的列表
下面是我在使用时曾经踩过的坑:我们会不以为然的认为 board = [['_'] * 3 for i in range(3)]
和
weird_board = [['_'] * 3] * 3
的生成嵌套列表是一样的,但事实证明它们不一样,来看例子:
board = [['_'] * 3 for i in range(3)]
print(board)
weird_board = [['_'] * 3] * 3
print(weird_board)
# 结果
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
上述程序输出看起来没什么问题,但是如果这里加以修改呢:
board = [['_'] * 3 for i in range(3)]
board[1][2] = 'O'
print(board)
weird_board = [['_'] * 3] * 3
weird_board[1][2] = 'O'
print(weird_board)
# 结果
[['_', '_', '_'], ['_', '_', 'O'], ['_', '_', '_']]
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
为什么结果不同呢?是因为使用 *
符号得到的新列表其实是原来列表的多次引用,内存地址其实是同一个地址,这样对列表进行修改时会发现新列表中多点都被修改了,使用以下例子可以证明:
board = [['_'] * 3 for i in range(3)]
print(id(board[0]), id(board[1]), id(board[2]))
weird_board = [['_'] * 3] * 3
print(id(weird_board[0]), id(weird_board[1]), id(weird_board[2]))
# 结果
2131644532488 2131645210248 2131645434056
2131645433992 2131645433992 2131645433992
可以发现后者(使用 *
符号)的id相同,故使同一个列表的引用,在我之前写程序时曾被它深深的坑过,吃一堑长一智。
2.5.2、对元组使用 *
由于元组是不可修改的,那么有人会认为试试把上面的 []
都换成 ()
就好了?我们试试:
board = tuple(('_',) * 3 for i in range(3))
print(board)
print(id(board[0]), id(board[1]), id(board[2]))
weird_board = (('_',) * 3,) * 3
print(weird_board)
print(id(weird_board[0]), id(weird_board[1]), id(weird_board[2]))
# 结果
(('_', '_', '_'), ('_', '_', '_'), ('_', '_', '_'))
2167152808728 2167152808728 2167152808728
(('_', '_', '_'), ('_', '_', '_'), ('_', '_', '_'))
2167152808872 2167152808872 2167152808872
经过实验,事实证明当使用元组时,两种方法都是对单一元组的引用。
2.6、序列的增量赋值
增量赋值运算符 +=
和 *=
的表现取决于它们的第一个操作对象。简单起见,我们把讨论集中在增量加法(+=)
上,但是这些概念对 *=
和其他增量运算符来说都是一样的。+=
背后的特殊方法是 __iadd__
(用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__
;例如 a += b
:如果 a 实现了 __iadd__
方法,就会调用这个方法。同时对可变序列(例如 list、bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b)
一样。但是如果 a 没有实现 __iadd__
的话,a += b
这个表达式的效果就变得跟 a = a + b
一样了:首先计算 a + b
,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现 __iadd__
这个方法。
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
t = (1, 2, [30, 40])
t[2].extend([50, 60])
print(t)
# 结果
(1, 2, [30, 40, 50, 60])
由上面式子发现,元组元素居然被改了,因为元组本身存放了可变列表,为了在写程序时不遇到这类奇怪的情况,建议:
- 不要把可变对象放在元组里面。
- 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
- 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。
2.7、list.sort方法和内置函数sorted
list.sort
方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表。在这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。例如,random.shuffle
函数也遵守了这个惯例;
但用返回 None
来表示就地改动这个惯例有个弊端,那就是调用者无法将其串联起来。而返回一个新对象的方法(比如说 str
里的所有方法)则正好相反,它们可以串联起来调用,从而形成连贯接口。
- 与 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表;
- 总结就是
list.sort
是在原列表地址上进行排序,而sorted
是新建列表进行排序,不改变原列表。
不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数:
-
reverse:
如果被设定为 True,被排序的序列里的元素会以降序输出,默认值是 False; -
key:
一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些字符串排序时,可以用key=str.lower 来实现忽略大小写的排序,或者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒等函数(identity function),也就是默认用元素自己的值来排序。
2.8、除列表外的数组,双向列表和numpy等
2.8.1、数组
Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型,例如 ‘b’ 表示整数,‘d’ 表示浮点数。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes
和 .tofile
。
import array
float = array.array('d', (i for i in range(6)))
print(float)
fp = open('floats.bin', 'wb')
float.tofile(fp)
fp.close()
fp = open('floats.bin', 'rb')
floats2 = array.array('d')
floats2.fromfile(fp, 6)
print(floats2)
fp.close()
# 结果
array('d', [0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
array('d', [0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
由于现在Numpy大行其道,python的array数组使用的情况很小,所以这里不展开讨论。
2.8.2、内存视图 memoryview
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片,memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你,同样使用领域很小,不做过多介绍。
2.8.3、Numpy
凭借着 NumPy 和 SciPy 提供的高阶数组和矩阵操作,Python 成为科学计算应用的主流语言。NumPy 实现了多维同质数组(homogeneous array)和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。通过 NumPy,用户能对这些数据结构里的元素进行高效的操作;此库过于强大,建议单独学习。
2.8.4、双向列表
利用 .append
和 .pop
方法,我们可以把列表当作栈或者队列来用(比如,把 .append
和 .pop(0)
合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。例子如下:
import collections
dq = collections.deque(range(10), maxlen=10)
print(dq)
# 队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n 个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被移动到右边。
dq.rotate(3)
print(dq)
dq.rotate(-4)
print(dq)
# 在左侧添加-1
dq.appendleft(-1)
print(dq)
# 在尾部添加列表
dq.extend([11, 22, 33])
print(dq)
# extendleft(iter) 方法会把迭代器里的元素逐个添加到双向队列的左边,因此迭代器里的元素会逆序出现在队列里。
dq.extendleft([10, 20, 30, 40])
print(dq)
# 结果
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
第 3 章、字典和集合
3.1、散列表 —— 字典和集合的靠山
字典不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影,所以字典的概念至关重要,其中字典和集合性能出众的根本原因是都使用散列表。所以在说字典之前需要补充一个知识点:散列表和可散列特性
3.1.1、散列表定义
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。在 dict 的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。
Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。
3.1.2、散列表算法
如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。下面从散列值和如何计算两方面说明:
-
散列值和相等性
内置的 hash() 方法可以用于所有的内置类型对象。如果是自定义对象调用
hash()
的话,实际上运行的是自定义的__hash__
。如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。例如,如果 1 == 1.0 为真,那么hash(1) == hash(1.0)
也必须为真,但其实这两个数字(整型和浮点)的内部结构是完全不一样的 -
散列表算法
为了获取
my_dict[search_key]
背后的值,Python 首先会调用hash(search_key)
来计算search_key
的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看当前散列表的大小)。若找到的表元是空的,则抛出KeyError 异常。若不是空的,则表元里会有一对found_key:found_value
。这时候 Python 会检验search_key == found_key
是否为真,如果它们相等的话,就会返回found_value
。如果 search_key 和 found_key 不匹配的话,这种情况称为散列冲突。发生这种情况是因为,散列表所做的其实是把随机的元素映射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字的一部分。为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元。若这次找到的表元是空的,则同样抛出KeyError
;若非空,或者键匹配,则返回这个值;或者又发现了散列冲突,则重复以上的步骤,具体过程如下图:
3.1.3、字典的优缺点
字典由于使用散列表进行存储数据,导致其具有内存占用大,搜索时间短的特点,可以说成字典是用空间换时间的典型数据类型。
3.2、字典构造方法 —— 字典推导等
3.2.1、对键的要求
标准库里的所有映射类型都是利用 dict 来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键(只有键有这个要求,值并不需要是可散列的数据类型);原子不可变数据类型(字符串(str)、单字符(bytes) 和数值类型)都是可散列类型,frozenset
也是可散列的,因为根据其定义,frozenset
里只能容纳可散列类型。元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。
3.2.2、字典的构造
- 可以使用如下几种方法构造字典:
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})
print(a == b == c == d == e)
# 结果
True
- 此外还有字典推导方式构造字典:
lis = [('a', 1), ('b', 2), ('c', 3)]
dic = {key: value for key, value in lis}
print(dic)
# 结果
{'a': 1, 'b': 2, 'c': 3}
3.3、字典的弹性键查询
有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候不会报错,而是能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict
这个类型而不是普通的 dict,另一个是给自己定义一个 dict 的子类,然后在子类中实现__missing__
方法。下面将介绍这两种方法。
3.3.1、defaultdict
defaultdict方法在 collections库中,在用户创建 defaultdict 对象的时候,就需要给它配置一个为找不到的键创造默认值的方法。具体而言,在实例化一个 defaultdict 的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在 __getitem__
碰到找不到的键的时候被调用,让 __getitem__
返回某种默认值。
比如,我们新建了这样一个字典:ddict = defaultdict(list)
,如果键 ‘new-key’ 在 ddict 中还不存在的话,表达式 ddict [‘new-key’] 会按照以下的步骤来行事:
(1) 调用 list() 来建立一个新列表。
(2) 把这个新列表作为值,‘new-key’ 作为它的键,放到 dd 中。
(3) 返回这个列表的引用。
举例如下:
from collections import defaultdict
dict1 = defaultdict(int)
dict2 = defaultdict(set)
dict3 = defaultdict(str)
dict4 = defaultdict(list)
print(dict1[1])
print(dict2[1])
print(dict3[1])
print(dict4[1])
# 结果
0
set()
[]
3.3.2、特殊方法__missing__
所有的映射类型在处理找不到的键的时候,都会牵扯到 missing 方法。这也是这个方法称作“missing”的原因。虽然基类 dict 并没有定义这个方法,但是 dict 是知道有这么个东西存在的。也就是说,如果有一个类继承了 dict,然后这个继承类提供了__missing__
方法,那么在 __getitem__
碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。
3.4、字典的变种(其他库函数)
这一节总结了标准库里 collections 模块中,除了 defaultdict 之外的不同映射类型。
3.4.1、collections.OrderedDict
这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False)
这样调用它,那么它删除并返回第一个被添加进去的元素,例:
from collections import OrderedDict
order_dict = OrderedDict({'a': 1, 'b': 2, 'c': 3})
print(order_dict)
order_dict.popitem()
print(order_dict)
order_dict.popitem(last=False)
print(order_dict)
# 结果
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
OrderedDict([('a', 1), ('b', 2)])
OrderedDict([('b', 2)])
3.4.2、collections.Counter
这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。例子如下:
from collections import Counter
count_dict = Counter('aaabbc')
print(count_dict)
# 结果
Counter({'a': 3, 'b': 2, 'c': 1})
3.4.3、colllections.UserDict
与上面的库函数不同,UserDict 是让用户继承写子类的。就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。原因是:dict()有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict 就不会带来这些问题。
3.5、集合(set)—— 无序不重复
“集”这个概念在 Python 中算是比较年轻的,同时它的使用率也比较低。set 和它的不可变的姊妹类型 frozenset 直到 Python 2.3 才首次以模块的形式出现,然后在 Python 2.6 中它们升级成为内置类型。
集合的本质是许多唯一对象的聚集。因此,集合的重要特点是无序不重复;集合中的元素必须是可散列的,set 类型本身是不可散列的,但是 frozenset 可以。因此可以创建一个包含不同 frozenset 的 set。
除了保证唯一性,集合还实现了很多基础的中缀运算符。给定两个集合 a 和 b,a | b 返回的是它们的合集,a & b 得到的是交集,而 a - b 得到的是差集。合理地利用这些操作,不仅能够让代码的行数变少,还能减少 Python 程序的运行时间。这样做同时也是为了让代码更易读,从而更容易判断程序的正确性,因为利用这些运算符可以省去不必要的循环和逻辑操作
3.5.1、集合的创建
除空集之外,集合的字面量——{1}、{1, 2},等等——看起来跟它的数学形式一模一样。如果是空集,那么必须写成 set() 的形式,否则创建的是空字典。
结合也可以使用集合推导式进行创建。
3.5.2、集合运算
集合的中缀运算符需要两侧的被操作对象都是集合类型,但是其他的所有方法则只要求所传入的参数是可迭代对象。例如,想求 4 个聚合类型 a、b、c 和 d的合集,可以用 a.union(b, c, d),这里 a 必须是个 set,但是 b、c 和 d 则可以是任何类型的可迭代对象。
- 集合的数学运算:
- 集合的比较运算符,返回值是布尔类型:
- 集合类型的其他方法:
3.5.3、集合的优缺点
集合由于使用和字典一样的散列表原理存储,所以优缺点和字典相似,总结如下:
- 集合里的元素必须是可散列的;
- 集合很消耗内存;
- 可以很高效地判断元素是否存在于某个集合;
- 元素的次序取决于被添加到集合里的次序;
- 往集合里添加元素,可能会改变集合里已有元素的次。
第 4 章、文本(字符串)和字节序列
人类使用文本,也就是字符串来表达信息,而计算机却使用字节序列来表示存储数据;我们人类所用的文本信息(字符)在计算机底层都是使用数字来进行表示的,所以需要一种编码方案来解决这一问题,由此,ASCII、Unicode等编码方案应运而生,但 ASCII 只能对英文字符和标点进行编码,而Unicode 可以对世界上所有语言和符号编码,所以后者成为世界通用的编码方案,也叫万国码、统一码等。其中Unicode 编码共有三种具体实现,分别为utf-8,utf-16,utf-32,其中utf-8占用一到四个字节,utf-16占用二或四个字节,utf-32占用四个字节
4.1、字符序列与字节序列
别看只有一字之差,字符表示的是文本和字符串,它使用 Unicode字符 来表示,而字节序列确是我们看不懂的符号,其实是十六进制位的数字。
把字节序列变成人类可读的文本字符串就是解码(decode),而把字符串变成用于存储或传输的字节序列就是编码(encode)。下面是编解码实验,其中要注意的是如果使用 utf-8进行编码时会发现编码结果依然是 abc
, 这是因为我的编辑器就是使用 utf-8 进行编写的,所以编码就是自身,采用 utf-16编码就好了。
s = 'abc'
es = s.encode('utf16')
print(es)
ds = es.decode('utf16')
print(ds)
# 结果
b'\xff\xfea\x00b\x00c\x00'
abc
关于此章的后续内容都是在讨论字节码的问题,虽然很重要,但可能在实际使用过程中占得比重不大,所以先跳过学习,之后再来更新这一部分。
第 5 章、一等函数
Python 有一句名言,叫做:一切皆对象;包括字符、数字、列表、元组、集合、字典、函数、类、模块、某种操作、甚至Python本身,等等都是对象。
在 Python 中,函数是一等对象。编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
-
在运行时创建
-
能赋值给变量或数据结构中的元素
-
能作为参数传给函数
-
能作为函数的返回结果
有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数
5.1、高阶函数
高阶函数(higher-order function)定义:接受函数为参数,或者把函数作为结果返回的函数。map 函数就是一例,此外,内置函数 sorted 也是:可选的key 参数用于提供一个函数,它会应用到各个元素上进行排序,例子如下:
def reverse(word):
return word[::-1]
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=reverse))
# 结果
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
在函数式编程范式中,最为人熟知的高阶函数有 map、filter、reduce,但其有可替代品。那就是我们之前见过的推导式和生成器,使用方法略。
5.2、匿名函数
匿名函数根据字面意思可以看出就是没有名字的函数,使用 lambda
关键字声明;Python 简单的句法限制了 lambda
函数的定义体只能使用纯表达式。换句话说,lambda
函数的定义体中不能赋值,也不能使用 while
和 try
等 Python 语句。
lambda语句中,冒号前是参数,可以有多个,用逗号隔开,冒号右边的返回值。lambda语句构建的其实是一个函数对象,见证一下:
a = [1, 2, 3]
g = lambda x: x * 3
print(g(a))
# 结果
[1, 2, 3, 1, 2, 3, 1, 2, 3]
除了作为参数传给高阶函数之外,Python 很少使用匿名函数。由于句法上的限制,非平凡的 lambda 表达式要么难以阅读,要么无法写出。
5.3、可调用对象
可调用对象是指可以在对象后面加入 ()
来计算的对象和方法等。 除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable()
函数;可调用返回 True 否则返回 False,
- 可调用对象:
- 函数
- 内置方法
- 类
- 不可调用对象:
- 常量
- 类实例对象
举例如下:
class A:
def __init__(self):
pass
a = A()
print(callable(abs), callable(len), callable(1), callable(A), callable(a))
# 结果
True True False True False
根据上面的结果我们发现,类是可调用的,类的实例对象是不可调用的(也就是说不可以使用 a()
),如何可以让类的实例也可以被调用呢?python给类创建了__call__
的特别方法,该方法允许程序员创建可调用的对象(实例)。默认情况下,__call__()
是没有实现的,即大多数的类的实例是不可调用的。而如果类中实现了这个方法,那么这个类的实例就成了可调用的了。例子如下:
class A:
def __init__(self):
pass
def __call__(self, arg):
print(arg + '实现了call可调用方法')
a = A()
a('对象a')
print(callable(A), callable(a))
# 结果
对象a实现了call可调用方法
True True
5.4、函数自省(对象或函数的内置属性)
Python中每个对象都有一定的内置属性,使用两个下划线定义,我们可以使用 dir()
或 __dir__()
进行查看。例子:
class A:
def __init__(self):
pass
a = A()
print(dir(a))
print(a.__dir__())
print(set(dir(a)) - set(a.__dir__()))
# 结果
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
['__module__', '__init__', '__dict__', '__weakref__', '__doc__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']
set()
我们会发现 dir()
和 __dir__()
返回的列表虽然顺序不一致,但变量是一致的。
5.5、函数的关键字参数
如果我们有一些具有许多参数的函数,而你又希望只对其中的一些进行指定,那么你可以通过命名它们来给这些参数赋值——这就是Python关键字参数(Keyword Arguments)——我们使用命名(关键字)而非位置(一直以来我们所使用的方式)来指定函数中的参数。
这样做有两大优点——其一,我们不再需要考虑参数的顺序,函数的使用将更加容易。其二,我们可以只对那些我们希望赋予的参数以赋值,只要其它的参数都具有默认参数值。例:
def func(a=0, b=1):
print(a, b)
func(b=3)
# 结果
0 3