[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
2
3
4
5
6
7
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in ['{} {}'.format(c, s) for c in colors for s in sizes]:
print(tshirt)

for tshirt in ('{} {}'.format(c, s) for c in colors for s in sizes):
print(tshirt)

在上面的案例中用列表推导的方式会先生成一个长度为6的列表,然后再进行遍历打印,而生成器表达式遵循迭代器协议,一次循环只生成一个元素进行打印;想象一下如果colors和sizes都是1000,那么会多出一个长度为100w列表的内存占用

元组不仅仅是不可变列表

在很多的入门教程中会说元组是不可变的列表,这个是从构造的角度说的,而从作用上来看就不仅如此

元组和记录

1
2
3
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')]

在上面的案例中,前两个示例是元组,而第三个是由元组组成的列表,那么我们来看看元组为什么要定义为不可变的

1
2
3
lax_coordinates = (33.9425, -118.408056)
print(lax_coordinates[0]) # 33.9425
lax_coordinates[0] = 1 # TypeError: 'tuple' object does not support item assignment

案例中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
2
3
4
5
divmod(20, 8)   # (2, 4)
t = (20, 8)
divmod(*t) # (2, 4)
quotient, remainder = divmod(*t)
quotient, remainder # (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
2
3
4
5
6
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
tokyo # City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
City._fields # ('name', 'country', 'population', 'coordinates')
tokyo._asdict() # OrderedDict([('name', 'Tokyo'), ('country', 'JP'), ('population', 36.933), ('coordinates', (35.689722, 139.691667))])
tokyo._asdict().items() # 可进行迭代

作为不可变的列表

具体来说和列表的区别就是,无法追加、删除元素等修改操作,可以做是否包含的判定

切片

切片是一个强大的功能,这里先介绍怎么用,后续会说底层原理

为什么切片和区间会忽略最后一个元素

就是解释一下为什么要和c一样从0开始计算

  • range(3)即返回三个元素,直观
  • range(1, 5)返回5-1个元素,直观
  • my_list[:x] 和 my_list[x:]可以从第几个元素那分割序列,直观

对对象进行切片

1
2
3
4
s = 'bicycle'
s[::3] # 'bye'
s[::-1] # 'elcycib'
s[::-2] # 'eccb'

多维切片和省略

a[m:n, k:l] 大概是这样,这里没看懂且不是很常用

切片赋值

1
2
3
4
5
l = list(range(10))
l[2:5] = [20, 30] # [0, 1, 20, 30, 5, 6, 7, 8, 9]
del l[5:7] # [0, 1, 20, 30, 5, 8, 9]
l[3::2] = [11, 22] # [0, 1, 20, 11, 5, 22, 9]
l[2:5] = [100] # [0, 1, 100, 22, 9]

对序列使用+和*

  • 使用+来拼接两个序列的时候,会创建一个同样类型数据的序列来作为拼接的结果

  • 如果想要将一个序列复制几份然后再拼接起来,更快捷的是吧序列乘以一个整数

  • [[]] * 3这个时候得到的列表里面的三个元素都是同一个引用

  • 二维列表的创建方式

    1
    2
    [['_'] * 3 for i in range(3)]
    [['_'] * 3] * 3

序列的增量赋值

对于a += b 如果a实现了__iadd__方法(就地加法)就会调用这个方法,如果没有实现则会调用__add__方法;即是说对于可变序列来说a指向的对象没有变,对对象进行了扩展,如果是__add__方法的话,就会变成a + b

可变序列一般都实现了__iadd__方法,不可变序列不支持这个操作,str是一个例外,由于字符串的+=操作太常见了所以做了特殊的优化(==初始化的时候预留出内存,这还叫不可变?==)

1
2
t = (1, 2, [30, 40])
t[2] += [50, 60]

上面的案例会发生什么?

  • 报错:’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之蝉==的作者,这本书也可以抽空看一看