不管是python的哪种数据结构,字符串、列表、字节序列、数组、XML 元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。

深入理解 Python 中的不同序列类型,不但能让我们避免重新发明轮子,它们的 API 还能帮助我们把自己定义的 API 设计得跟原生的序列一样,或者是跟未来可能出现的序列类型保持兼容。

容器序列(存放的是它们所包含的任意类型的对象的引用)

list(列表), tuple(元组), collections(collections是Python内建的一个集合模块,提供了许多有用的集合类), collections.deque(双端队列) 可以存放不同类型的数据,容器序列存放的是他们所包含的任意类型的对象的引用

扁平序列(存放的是值而不是引用,是一段连续的内存空间,更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型)

str(string), bytes(字节序列), bytearray(字节数组), memoryview(内存视窗), array.array(保存相同类型的数值的动态数组) 只能容纳一种类型,存放的是值而不是引用。是一段连续的内存空间,更加经凑

可变序列

list(列表), bytearray(字节数组), array.array(保存相同类型的数值的动态数组), collections.deque(双边队列), memoryview(内存视窗)

不可变序列

tuple(元组), str(字符串), bytes(字节序列)

要存放1000万个浮点数时,数组(array)的效率比列表(list)高的多,因为数组在背后存在的并不是float对象,而是数字的机器翻译,也就是字节表述。如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。如果在代码里包含操作(比如检查一个元素是否存在在一个集合里)的频率很高,用set(集合)会更合适,但他不是序列,是无序的。

列表推导(列表推导是一种构建列表的方法,是构建列表(list)的快捷方式)

示例  用列表推导把一个字符串变成 Unicode 码位的列表

1
2
3
4
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]

map/fillter组合来创建表单

1
2
symbols = "efaevg"
beyond_ascii = list(fillter(lambda c: c > 127, map(ord, symbols)))

列表推导

1
2
symbols = "efaevg"
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]

python会忽略代码里[],{}和()中的换行,可以省略不太好看的续行符号\

列表推导不会再有变量泄漏的问题列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在 Python 3 中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。

列表推导实现笛卡尔积
1
2
3
colors = ["white", "black", "red"]
sizes = ['S', 'M', 'L']
T_shirts = [(color, size) for color in colors for size in sizes]
列表推导的作用只有一个:生成列表

生成器表达式(生成器表达式可以用来创建其他任何类型的序列,具有生成各种类型的元素并用它们来填充序列的功能)

虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存

生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。

用生成器表达式初始化元组和数组
1
2
3
4
5
6
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) # 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) #array 的构造方法需要两个参数,因此括号是必需的。array 构造方法的第一个参数指定了数组中数字的存储方式。
array('I', [36, 162, 163, 165, 8364, 164])
使用生成器表达式计算笛卡儿积
1
2
3
4
5
6
7
8
9
10
11
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes): # 生成器表达式逐个产出元素,从来不会一次性产出一个含有 6 个 T 恤样式的列表。
... print(tshirt)
...
black S
black M
black L
white S
white M
white L
生成器表达式逐个产生元素,从来不会一次性产出一个完整的列表

元组(tuple)

元祖可以用于没有字段名的记录

元组不仅仅是不可变的列表,它还可以用于没有字段名的记录。元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。如果把元组当作一些字段的集合,那么数量和位置信息就变得非常重要了如果在任何的表达式里我们在元组内对元素排序,这些元素所携带的信息就会丢失,因为这些信息是跟它们的位置有关的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

>>> 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): #在迭代的过程中,passport 变量被绑定到每个元组上。
... print('%s/%s' % passport)
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids:
... print(country)
...
USA
BRA
ESP

for 循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因为元组中第二个元素对我们没有什么用,所以它赋值给“_”占位符。
拆包让元组可以完美地被当作记录来使用
总结:记录与元祖——位置的重要性

元组拆包

元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接收这些元素的元组的空档数一致。除非我们用*来表示忽略多余的元素

*运算符把一个可迭代对象拆开作为函数的参数可以帮助我们把注意力集中在元组的部分数据中:

1
2
3
4
5
6
7
8
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4

用*来处理剩下的元素 ,在 Python 中,函数用 *args 来获取不确定数量的参数算是一种经典写法了。

1
2
3
4
5
6
7
8
9
>>> 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, [])

在平行赋值中,*前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式中任意位置

1
2
3
4
5
6
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)

平行赋值

1
b, a = a, b

下面是另一个例子,这里元组拆包的用法则是让一个函数可以用元组的形式返回多个值,然后调用函数的代码就能轻松地接受这些返回值。比如 os.path.split() 函数就会返回以路径和最后一个文件名组成的元组 (path, last_part):

1
2
3
4
>>> import os
>>> _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
>>> filename
'idrsa.pub'

嵌套元组拆包

接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应。

具名元组

collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。

实例用具名元组来记录一个城市的信息。定义和使用具名元组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> from collections import namedtuple
>>> 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))
>>> tokyo.population #你可以通过字段名或者位置来获取一个字段的信息。
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'
>>> City._fields # _fields 属性是一个包含这个类所有字段名称的元组。
('name', 'country', 'population', 'coordinates')
>>> LatLong = namedtuple('LatLong', 'lat long')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
>>> delhi = City._make(delhi_data) # 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟City(*delhi_data) 是一样的。
>>> delhi._asdict() #_asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。
OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population',
21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))])
>>> for key, value in delhi._asdict().items():
print(key + ':', value)
name: Delhi NCR
country: IN
population: 21.935
coordinates: LatLong(lat=28.613889, long=77.208889)

列表或元组的方法和属性(那些由object类支持的方法没有列出来)

列表 元组
s.add(s2) s + s2,拼接
s.iadd(s2) s += s2,就地拼接
s.append(e) 在尾部添加一个新元素
s.clear() 删除所有元素
s.contains(e) s 是否包含 e
s.copy() 列表的浅复制
s.count(e) e 在 s 中出现的次数
s.delitem(p) 把位于 p 的元素删除
s.extend(it) 把可迭代对象 it 追加给 s
s.getitem(p) s[p],获取位置 p 的元素
s.getnewargs() 在 pickle 中支持更加优化的序列化
s.index(e) 在 s 中找到元素 e 第一次出现的位置
s.insert(p, e) 在位置 p 之前插入元素e
s.iter() 获取 s 的迭代器
s.len() len(s),元素的数量
s.mul(n) s * n,n 个 s 的重复拼接
s.imul(n) s *= n,就地重复拼接
s.rmul(n) n * s,反向拼接 *
s.pop([p]) 删除最后或者是(可选的)位于 p 的元素,并返回它的值
s.remove(e) 删除 s 中的第一次出现的 e
s.reverse() 就地把 s 的元素倒序排列
s.reversed() 返回 s 的倒序迭代器
s.setitem(p, e) s[p] = e,把元素 e 放在位置p,替代已经在那个位置的元素
s.sort([key],[reverse]) 就地对 s 中的元素进行排序,可选的参数有键(key)和是否倒序(reverse)

高级切片形式的用法

序列可以用 s[a:b] 的形式切片
在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作

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

对对象进行切片

用 s[a:b:c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值

1
2
3
4
5
6
7
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b,c)

多维切片和省略

Python 内置的序列类型都是一维的,因此它们只支持单一的索引,成对出现的索引是没有用的。

[] 运算符里还可以使用以逗号分开的多个索引或者是切片,

外部库 NumPy 里就用到了这个特性,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,抑或是用 a[m:n,k:l] 的方式来得到二维切片。

算符的话,对象的特殊方法 getitemsetitem 需要以元组的形式来接收a[i, j] 中的索引。也就是说,如果要得到 a[i, j] 的值,Python 会调用a.getitem((i, j))。

省略(ellipsis)的正确书写方法是三个英语句号(…),而不是 Unicdoe 码位 U+2026表示的半个省略号(…)

在NumPy中 … 用作多维数组切片的快捷方式 。如果 x 是四维数组,那么 x[i, …] 就是 x[i,:, :, :] 的缩写

给切片赋值

如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100 # 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。 正确的做法是l[2:5] = [100]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]

对序列使用+和*

如果想要把一个序列复制几份然后再拼接起来,更快捷的做法是把这个序列乘以一个整数。同样,这个操作会产生一个新序列.

1
2
3
4
5
>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
  • 和 * 都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列。

append是引用不是传值(第八章会说明引用和可变对象的原理)

如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用my_list = [[]] * 3 来初始化一个由列表组成的列表,但是你得到的列表里包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。这可能不是你想要的效果。

下面错误用法

1
2
3
4
5
6
7
a = ['_','_','_']#或者 a=['_']*3
board =[]
for i in range(3):
board.append(a)
board[1][2]='2'
print(board)
#board = [['_', '_', '2'], ['_', '_', '2'], ['_', '_', '2']]

下面正确操作

1
2
3
4
board = [['_']*3 for i in range(3)]
board[1][2]='2'
print(board)
#[['_', '_', '_'], ['_', '_', '2'], ['_', '_', '_']]

下面正确操作

1
2
3
4
5
6
7
board =[]
for i in range(3):
a = ['_','_','_']#或者 a=['_']*3 与上面区别,这里每次for都新建了一个列表
board.append(a)
board[1][2]='2'
print(board)
#[['_', '_', '_'], ['_', '_', '2'], ['_', '_', '_']]

 序列的增量赋值

增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象
+= 背后的特殊方法是 iadd (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 add

如果 a 实现了 iadd 方法,就会调用这个方法。同时对可变序列(例如list、bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b)一样。但是如果 a 没有实现 iadd 的话,a += b 这个表达式的效果就变得跟 a = a+ b 一样了:首先计算 a + b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现 iadd 这个方法。
总体来讲,可变序列一般都实现了 iadd 方法,因此 += 是就地加法。而不可变序列根本就不支持这个操作,对这个方法的实现也就无从谈起。
上面所说的这些关于 += 的概念也适用于 *=,不同的是,后者相对应的是 imul__。关于 __iaddimul

对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。

str 是一个例外,因为对字符串做 += 实在是太普遍了。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。

元组中的神奇问题

1
2
3
4
5
6
7
8
t=(1,2,[1,2])
t*=3#也是引用拼接
>>> t[2]+=[1]#虽然报错,但依然+=成功,可以用t[2].extend([1])来避免这个异常问题
>>> Traceback (most recent call last):
>>> File "<stdin>", line 1, in <module>
>>> TypeError: 'tuple' object does not support item assignment
>>> t
>>> (1, 2, [1, 2, 1], 1, 2, [1, 2, 1], 1, 2, [1, 2, 1])

教训:
1.不要把可变对象放在元组里面
2.增量赋值不是一个原子操作,虽然抛出异常但还是完成了
3.查看python的字节码不难但有帮助

查看字节码

1
2
3
4
import dis
dis.dis('t[2]+=[1]')
2**3 = 8
2**4 = 16

排序——list.sort方法和内置函数sorted

list.sort(reverse = False, key = len) 会就地排序列表,不会把原列表复制一份。返回 None

sorted(reverse = False, key = len) 会新建一个列表作为返回值

reverse 默认为 False, 如果被设为 True, 被排序的序列里的元素会以降序输出

key 一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。 比如在对一些字符串排序时,可以用key=str.lower 来实现忽略大小写的排序(书本36)key=len 进行基于字符串长度的排序。默认为恒值函数也就是默认用元素自己的值来排序。

用bisect来管理已排序的序列

bisect 模块包含两个主要函数,bisect 和 insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。

用bisect来搜索

用bisect.insort插入新元素

例子
haystack有序升序序列,needle需要插入的数

1
2
3
4
5
bisect.bisect(haystack, needle, lo = 0, hi = len(haystack))  #利用二分法来查找,bisect.bisect返回的位置前面的值,都小于或等于 needle 的值
bisect.insort(haystack, needle) #利用二分法来插入

bisect.bisect_left(haystack, needle, lo = 0, hi = len(haystack)) # 利用二分法来查找,返回位置值,前面的值都 <needle
bisect.bisect_right(haystack, needle, lo = 0, hi = len(haystack)) # 利用二分法来查找,返回位置值,前面的值都 ≤needle

根据一个分数找到他所对应的绩点

1
2
3
4
5
6
import bisect
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
i = bisect.bisect(breakpoints, score)
return grades[i]
print([grade(score) for score in [22, 44, 66, 77, 34, 88 , 99]])
#['F', 'F', 'D', 'C', 'F', 'B', 'A']

数组array的使用

如果只需要处理数字列表的话,数组可能是个更好的选择。
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。数组支持所有跟可变序列有关的操作,包括 .pop、.insert 和 .extend。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes 和 .tofile。

创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。
类型码网上都有
比如 b 类型码代表的是有符号的字符(signed char),因此 array(‘b’) 创建出的数组就只能存放一个字节大小的整数,范围从 -128到 127
Python 不会允许你在数组里存放除指定类型之外的数据。

一个浮点型数组的创建、存入文件和从文件读取的过程

1
2
3
4
5
6
7
8
9
10
11
12
from array import array#引入array类型
from random import random

floats = array('d', (random() for i in range(10**7)))#利用一个可迭代对象来建立一个双精度浮点数组(类型码是'd'),1000万个
fp = open('floats.bin', 'wb')
floats.tofile(fp)#把数组存入一个二进制文件里
fp.close()
floats2 = array('d')#新建一个双精度浮点空数组
fp = open('floats.bin', 'rb')
floats2.frombytes(fp, 10**7)#把1000万个浮点数从二进制文件里读取出来
fp.close()
floats2 == floats

从 Python 3.4 开始,数组类型不再支持诸如 list.sort() 这种就地排序方法。要给数组排序的话,得用 sorted 函数新建一个数组:

想要在不打乱次序的情况下为数组添加新的元素,bisect.insort 还是能派上用场

1
a = array.array(a.typecode, sorted(a))

内存视图 memoryview

内存视窗能让你在不需要复制内容的前提下在数据结构之间共享内存
memoryview.cast能用不同的方式读写同一块内存数据,而且内容字节不会随意移动
memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。

在示例里我们利用 memoryview 精准地修改了一个数组的某个字节,这个数组的元素是 16 位二进制整数。
 通过改变数组中的一个字节来更新数组里某个元素的值

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) # 利用含有 5 个短整型有符号整数的数组(类型码是 'h')创建一个 memoryview。
>>> len(memv)
5
>>> memv[0] # memv 里的 5 个元素跟数组里的没有区别。
-2
>>> memv_oct = memv.cast('B') # 创建一个 memv_oct,这一次是把 memv 里的内容转换成 'B' 类型,也就是无符号字符。
>>> memv_oct.tolist() # 以列表的形式查看 memv_oct 的内容。
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 #把位于位置 5 的字节赋值成 4。
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) # 因为我们把占 2 个字节的整数的高位字节改成了 4,所以这个有符号整数的值就变成了 1024。

双向队列和其他形式的队列

利用 .append 和 .pop 方法,我们可以把列表当作栈或者队列来用(比如,把 .append和 .pop(0) 合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。

collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。

很多人认为python中的字典是无序的,因为它是按照hash来存储的,
但是python中有个模块collections(英文,收集、集合),里面自带了一个子类
OrderedDict,实现了对字典对象中元素的排序。使用OrderedDict会根据放入元素的先后顺序进行排序
OrderedDict对象的字典对象,如果其顺序不同那么Python也会把他们当做是两个不同的对象

评论