流畅的Python -- 第二章 序列构成的数组
[toc]
序列构成的数组
Python的作者以前参与过一个叫ABC的语言的迭代,这个语言是专为初学者准备的,所以Python身上也有很多它的特征,其中之一就是序列的泛型操作,也就是字符串、列表、数组、字节序列、xml元素以及数据库查询结果等都共用一套操作:迭代、切片、排序和拼接
比如说java中string要进行切割的话需要调用方法比如split(当然py中同样有这个方法)或者substring来指定保留的内容,而数组需要通过copyOfRange方法或者subList方法,但py中都可以通过[]来完成
Python内置序列(集合)
容器序列
list、tuple 和 collections.deque 这些序列能存放不同类型的数据。
扁平序列
str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。
容器序列中存储的是对象引用,扁平序列中存储的是值(基础数据类型),这里说扁平序列都是连续的内存空间更加紧凑
可变不可变序列
按照是否可变进行划分
- 可变:list、bytearray、array.array、collections.deque 和memoryview
- 不可变:tuple、str 和 bytes
列表推导和生成器表达式
列表推导(list comprehension)是构建列表的快捷方式,生成器表达式(generator expression)则可以用于创建其他任何类型的序列
列表推导
[card for card in cards]这个就叫做列表推导,能提高代码的可阅读性,但不要滥用它,通常的原则是只用来创建列表,且如果代码超过了两行就要考虑是否用for循环的方式写了,这个自行把握尺度
- 另外Python会忽略[]、{}和()中的换行
- Python2中列表推到会出现变量泄露,也就是说上面的card如果之前有定义,在列表推到后会被赋值为最后一次迭代的元素,Python3中不会再有这个问题,列表推导有自己的作用域
还有嵌套的列表推导[(color, size) for color in colors for size in sizes],即是一个嵌套循环colors是外层循环
生成器表达式
tuple(ord(symbol) for symbol in symbols)
对比
这里说一下,列表推导就是用来生成列表的,如果一定要用来做元组、数组之类的初始化会有一个内存浪费的问题
1 | colors = ['black', 'white'] |
在上面的案例中用列表推导的方式会先生成一个长度为6的列表,然后再进行遍历打印,而生成器表达式遵循迭代器协议,一次循环只生成一个元素进行打印;想象一下如果colors和sizes都是1000,那么会多出一个长度为100w列表的内存占用
元组不仅仅是不可变列表
在很多的入门教程中会说元组是不可变的列表,这个是从构造的角度说的,而从作用上来看就不仅如此
元组和记录
1 | lax_coordinates = (33.9425, -118.408056) |
在上面的案例中,前两个示例是元组,而第三个是由元组组成的列表,那么我们来看看元组为什么要定义为不可变的
1 | lax_coordinates = (33.9425, -118.408056) |
案例中lax_coordinates存储了洛杉矶机场的经纬度,元组可以通过[]角标来访问也可以迭代访问(都说了是不可变的列表嘛)
但是这里准备将经度修改为1,这就不行了,而且如果想将第一个元素和第二个元素进行对调也是不行的,很明显元组虽然也是列表,但元组里面的每个元素都有自己的含义比如这里的经纬度,所以本质是一条记录,而不单纯是一堆数据的容器,每个位子都有他特定的含义
“除了用作不可变的列表,它还可以用于没有字段名的记录”
元组拆包
for country, _ in traveler_ids 通过迭代分布提取元组中的元素叫做拆包,这里我们认为第二个元素没有意义,所以用_占位符给忽略掉print('%s, %s' % lax_coordinates)元组拆包方式二city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)元组拆包方式三
1 | divmod(20, 8) # (2, 4) |
拆包方式四a, b, *rest = range(5) 这里会将[2, 3, 4]都放到reset中(接收的时候要加上*),且reset可以放到任意位置比如a, *rest, b = range(5)a, b = b, a 优雅的元素置换方式,==具体实现有待探究==
嵌套拆包
name, cc, pop, (latitude, longitude) = ('Tokyo','JP',36.933,(35.689722,139.691667))
具名元组
元组已经设计得很好用了,但作为记录来用的话,还是少了一个功能:我们时常会需要给记录中的字段命名。namedtuple 函数的出现帮我们解决了这个问题。
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
1 | City = namedtuple('City', 'name country population coordinates') |
作为不可变的列表
具体来说和列表的区别就是,无法追加、删除元素等修改操作,可以做是否包含的判定
切片
切片是一个强大的功能,这里先介绍怎么用,后续会说底层原理
为什么切片和区间会忽略最后一个元素
就是解释一下为什么要和c一样从0开始计算
- range(3)即返回三个元素,直观
- range(1, 5)返回5-1个元素,直观
- my_list[:x] 和 my_list[x:]可以从第几个元素那分割序列,直观
对对象进行切片
1 | s = 'bicycle' |
多维切片和省略
a[m:n, k:l] 大概是这样,这里没看懂且不是很常用
切片赋值
1 | l = list(range(10)) |
对序列使用+和*
使用+来拼接两个序列的时候,会创建一个同样类型数据的序列来作为拼接的结果
如果想要将一个序列复制几份然后再拼接起来,更快捷的是吧序列乘以一个整数
[[]] * 3这个时候得到的列表里面的三个元素都是同一个引用二维列表的创建方式
1
2[['_'] * 3 for i in range(3)]
[['_'] * 3] * 3
序列的增量赋值
对于a += b 如果a实现了__iadd__方法(就地加法)就会调用这个方法,如果没有实现则会调用__add__方法;即是说对于可变序列来说a指向的对象没有变,对对象进行了扩展,如果是__add__方法的话,就会变成a + b
可变序列一般都实现了__iadd__方法,不可变序列不支持这个操作,str是一个例外,由于字符串的+=操作太常见了所以做了特殊的优化(==初始化的时候预留出内存,这还叫不可变?==)
1 | t = (1, 2, [30, 40]) |
上面的案例会发生什么?
- 报错:’tuple’ object does not support item assignment
- t的值会被修改:(1, 2, [30, 40, 50, 60]),原因是t[2]指向的是一个可变对象,所以可变对象的+=操作执行成功了,在将执行结果赋值给元组的时候报错
list.sort方法和内置函数sorted
list.sort方法会就地排序列表,不会发生复制,所以返回值为None,这种情况下返回None是Python的一个惯例:如果一个函数或者方法对对象进行的是就地改动那它就应该返回None
让调用者知道传入的参数发生了变动且未产生新的对象,其弊端是无法将其串联起来调用,而str的所有方法则正好相反
儿sorted方法则相反,它会新建一个列表作为返回值,这个方法可以接受任何形式的可迭代对象作为参数,包括不可变序列或者生成器,但最终的返回结果都会是一个列表
- reverse参数,用来控制升序降序
- key这个参数会被用在序列中的每个元素上,
key=str.lower表示忽略大小写进行排序,key=len表示基于字符串长度的排序,默认值是恒等函数(identity function)表示用元素自己的值来排序
用bisect来管理已排序的序列
bisect 模块包含两个主要函数,bisect 和 insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
用bisect来搜索
bisect(haystack, needle)在haystack中找出needle的位置,即将needle插入返回的指针位置后序列仍然有序
haystack.insert(index, needle)来进行元素的插入,也可以用insort一步到位,且速度更快一些
用bisect.insort插入新元素
insort(seq, item) 不同于上面两个方法,insort不要求序列本身有序,可以完成序列的排序和元素的插入,且让序列保持有序
当列表不是首选
数组
当我们需要一个只包含数字的列表,那么array.array效率更高,且包含了效率更高的文件读写方法
内存视图
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。
不太好理解,这里给出的案例是,将一个有符号的数组,通过memoryview转化为一个无符号列表,然后去操作列表中的元素,这个时候原来的数组的内容也被修改了
NumPy 和 SciPy
是两个很厉害的第三方库,主要提供高阶的多维数组和矩阵的操作
双向队列和其他队列
首先如果我们使用list的append和pop方法可以把列表当做栈或者队列来用,但实际上向列表的首端添加数据的成本很大
collections.deque 类是一个双向队列,是一个线程安全的、可以快速从两端读写元素的数据类型;且deque可以设置队列长度,如果队列满了,还可以从反向端删除元素
queue提供线程安全的Queue、lifoQueue和PriorityQueue,不同的线程可以利用这些数据类型来交换信息,也支持设置最大值,不过区别是到了最大值之后是锁定入队操作而不是淘汰现有元素
multiprocessing与上面的queue比较类似,不过是设计用于进程间通信的。同时还有一个专门的multiprocessing.JoinableQueue 类型,可以让任务管理变得更方便。
asyncio基本涵盖了queue和multiprocessing,是专门为异步编程设计的
heapq跟上面三个模块不同的是,heapq 没有队列类,而是提供了heappush 和 heappop 方法,让用户可以把可变序列当作堆队列或者优先队列来使用。
题外话
Python里面sorted方法的内置排序算法是timsort,这个算法的是Tim Peter在02年加入的Python,这哥们也是Python核心开发者,他贡献了太多的代码,以至于大家说他是人工智能Timbot,同时他也是==python之蝉==的作者,这本书也可以抽空看一看
