python序列类型
容器序列
容器序列能够存放不同类型的数据,像list、tuple和collections.deque。
扁平序列
扁平序列只能存在一种类型的数据,像str、bytes、bytearray、memoryview、array.array。
注意:容器序列存放是任意类型的对象的引用,而扁平序列存在是值而不是引用,是一段连续的内存空间。
按照是否能被修改来区分:
可变序列
list、bytearray、array.array、collections.deque 和
memoryview
不可变序列
tuple、str 和 bytes
list——列表推倒
利用列表推导创建列表具有很好的可读性
比较下面两段代码
symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
codes.append(ord(symbol))
codes
Out[5]:
[36, 162, 163, 165, 8364, 164]
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
codes
Out[12]:
[36, 162, 163, 165, 8364, 164]
利用列表推导的代码显得相当简洁。列表推导还能生成多重for循环的列表,如下代码所示:
a = ['A','B','C','D']
b = [1,2,3,4]
c = [(i, j) for i in a for j in b]
c
Out[5]:
[('A', 1),
('A', 2),
('A', 3),
('A', 4),
('B', 1),
('B', 2),
('B', 3),
('B', 4),
('C', 1),
('C', 2),
('C', 3),
('C', 4),
('D', 1),
('D', 2),
('D', 3),
('D', 4)]
d = [(i, j) for j in b for i in a]
d
Out[9]:
[('A', 1),
('B', 1),
('C', 1),
('D', 1),
('A', 2),
('B', 2),
('C', 2),
('D', 2),
('A', 3),
('B', 3),
('C', 3),
('D', 3),
('A', 4),
('B', 4),
('C', 4),
('D', 4)]
用列表推导还能替代filter和map,避免使用难以理解的lambda表达式,
symbols = '$¢£¥€¤'
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
beyond_ascii
Out[12]:
[162, 163, 165, 8364, 164]
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
beyond_ascii
Out[14]:
[162, 163, 165, 8364, 164]
元组
元组的意义不仅仅在于是不可变序列,更在于它可以与记录关联起来,赋予它特殊的意义。
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)
BRA,CE342567
ESP,XDA205856
USA,31195855
这里每个元组里的每一项都有了特定的含义。下面看看元组的拆包。
简单拆包
需要说明的是拆包可以运用到任何可迭代对象上。
先看最简单的拆包,平行赋值
lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates
latitude
Out[10]:
33.9425
longitude
Out[11]:
-118.408056
利用平行赋值简便的交换两个变量的值
a=2
b=3
a,b=b,a
a
Out[15]:
3
b
Out[16]:
2
*可以用俩处理剩下的元素
a,b,*rest = range(6)
a
Out[19]:
0
rest
Out[20]:
[2, 3, 4, 5]
b
Out[21]:
1
a,*rest,b = range(6)
a
Out[23]:
0
b
Out[24]:
5
rest
Out[25]:
[1, 2, 3, 4]
嵌套拆包
拆包还可以嵌套进行,这个时候需要注意表达式的嵌套结构要和元组的嵌套结构一致
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),]
print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas:
if longitude <= 0:
print(fmt.format(name, latitude, longitude))
上述代码输出如下
| lat. | long.
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
Sao Paulo | -23.5478 | -46.6358
具名元组
作为记录使用,我们很多时候希望要有一个字段名,虽然元组本身无法满足要求,但是可以利用库中的collections.namedtuple来实现。collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类。
>>> 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
('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)
>>> delhi._asdict()
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)
_fields属性是一个包含这个类所有字段名称的元组,_make()方法能够接受一个可迭代对象来生成这个类的一个实例,_asdict() 把具名元组以collections.OrderedDict 的形式返回。
切片
切片的功能学过python的都了解,这里主要涉及一些切片需要注意的地方。
切片会忽略最后一个元素
这复合python和C都以0作为起始是相符的
>>> s = [1,2,3,4,5,6]
>>> s[1:3]
[2, 3]
对对象进行切片
还可以用s[a:b:c]的形式对s在a和b之间以c为间隔取值,c的值可以为负,这时为反向取值
>>> s = [1,2,3,4,5,6,7,8,9,10]
>>> s[1:9:2]
[2, 4, 6, 8]
>>> s[9:1:-2]
[10, 8, 6, 4]
>>> s[::-1]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
a:b:c这种用法的本质是在调用slice(start,stop,step),对seq[start:stop:step] 进行求值的时候,Python 会调用
seq._getitem_(slice(start, stop, step))。
给切片赋值
切片还有一个强大的功能就是可以直接给切片赋值以修改原序列。
>>> s = list(range(10))
>>> s
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> s[3:6] = [10,20]
>>> s
[0, 1, 2, 10, 20, 6, 7, 8, 9]
>>> s[1:3] = [30,40,50,60]
>>> s
[0, 30, 40, 50, 60, 10, 20, 6, 7, 8, 9]
>>> s[1:5] = 20
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
s[1:5] = 20
TypeError: can only assign an iterable
>>> s[1:5] = [20]
>>> s
[0, 20, 10, 20, 6, 7, 8, 9]
从上面代码可以看出,给切片复值的值必须也是序列,哪怕只有一个值,也要用序列的形式。
对序列使用+和*
+和*都是对序列的拼接操作,+和*操作不会修改原序列,而是生成一个新的序列。
>>> s1 = [1,2,3]
>>> s2 = [4,5,6]
>>> s1 + s2
[1, 2, 3, 4, 5, 6]
>>> s1
[1, 2, 3]
>>> s1 * 4
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> s1
[1, 2, 3]
>>> 3*s2
[4, 5, 6, 4, 5, 6, 4, 5, 6]
>>> s2
[4, 5, 6]
>>>
关于需要注意的一点:如果序列内的元素是其他可变对象的引用的话,就需要格外小心了。看下面这个由列表构成列表的例子*:
创建一个由列表组成的列表,我们可能会写出下面这样的代码
weird_board = [['_'] * 3] * 3
weird_board[1][2] = 'O'
这样的结果会是什么,我们看下每步的结果:
>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2] = 'O'
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
完全不是我们想要的结果,为什么会出现这种现象?这里第一步中的[['_'] * 3]
会形成序列[['_', '_', '_']]
,而[['_', '_', '_']]*3
形成的三个序列都是这个序列的引用,所以就会出现上面的情况。
正确的做法应该像下面这样:
>>> board = [['_'] * 3 for i in range(3)]
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
序列的增量赋值
增量赋值原理上都是一致的,这里主要集中在+=上。
+=操作背后实际是调用了_iadd方法,如果一个类没有实现这个方法的话,Python 会退一步调用 _add 方法。同时,可变序列就地改动,不可变序列会生成一个新的对象。
>>> s=[1,2,3]
>>> id(s)
48702528
>>> s+=[4,5,6]
>>> s
[1, 2, 3, 4, 5, 6]
>>> id(s)
48702528
>>> s*=3
>>> s
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
>>> id(s)
48702528
>>> t=(1,2,3)
>>> id(t)
48765168
>>> t+=(4,5,6)
>>> t
(1, 2, 3, 4, 5, 6)
>>> id(t)
48202224
一个关于+=的陷阱
下面这段代码执行后会发生什么情况
>>> t = (1,2,[3,4])
>>> t[2] += [5,6]
相信很多人都知道会抛出异常,因为元组是不可变序列。我们来看看执行结果,的确如此
>>> t = (1,2,[3,4])
>>> t[2] += [5,6]
Traceback (most recent call last):
File "<pyshell#56>", line 1, in <module>
t[2] += [5,6]
TypeError: 'tuple' object does not support item assignment
再看看现在t的值呢?
>>> t
(1, 2, [3, 4, 5, 6])
t的值也改变了,这是怎么回事呢?我们利用Python Tutor(http://www.pythontutor.com)这个可视化分析工具来看看运行情况。(这里强烈推荐这个工具,能够动态展示每句代码执行后各个变量的变化情况)。
再来看看相应的背后的字节码
>>> import dis
>>> dis.dis('s[a]+=b')
1 0 LOAD_NAME 0 (s)
2 LOAD_NAME 1 (a)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_NAME 2 (b)
10 INPLACE_ADD
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
首先将 s[a] 的值存入 TOS(栈的顶端),然后计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对象,最后s[a] = TOS 赋值。这一步失败,是因为 s 是不可变的元组。
排序和搜索
排序
list.sort会对列表进行就地排序,而python的内置函数sorted则是重新创建一个对象返回,它接受任何可迭代对象作为参数。
>>> s=[3,5,2,1,6,7,9,8]
>>> s_new = sorted(s)
>>> s
[3, 5, 2, 1, 6, 7, 9, 8]
>>> s_new
[1, 2, 3, 5, 6, 7, 8, 9]
>>> s.sort()
>>> s
[1, 2, 3, 5, 6, 7, 8, 9]
搜索
排过序的序列可以进行二分查找,python标准库的内置模块 bisect 提供了二分查找算法。bisect(haystack, needle) 在 haystack里搜索needle的位置,该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。
>>> bisect.bisect(s,3)
3
>>> bisect.bisect(s,8)
7
>>> bisect.bisect(s,8.5)
7
用bisect.insort还可以在一个有序的序列里插入新元素从而保持序列仍然有序。
>>> bisect.insort(s,8.5)
>>> s
[1, 2, 3, 5, 6, 7, 8, 8.5, 9]
列表之外的选择
列表虽然灵活而且简单,但是有些时候面对各类需求的时候,我们可能还有更好的选择。当数据量较大时,比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。
数组
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。同时,数组不仅包括 .pop、.insert 和.extend,还提供从文件读取和存入文件的更快的方法,如.frombytes 和 .tofile。
创建一个array时,需要指定其类型,这个类型的符号与相应的类型的对应关系在标准库中能够查到。
下面的例子展示了从创建一个有 1000 万个随机浮点数的数组开始,到如何把这个数组存放到文件里,再到如何从文件读取这个数组。
>>> from array import array
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))
>>> floats[-1]
0.4628972061976403
>>> fp = open('floats.bin','wb')
>>> floats.tofile(fp)
>>> fp.close()
>>> floats2 = array('d')
>>> fp = open('floats.bin','rb')
>>> floats2.fromfile(fp, 10**7)
>>> fp.close()
>>> floats2[-1]
0.4628972061976403
>>> floats2 == floats
True
内存视图
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。这与C语言中的类型转换差不多。
>>> numbers = array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)
>>> len(memv)
5
>>> memv[0]
-2
>>> memv_oct = memv.cast('B')
>>> memv_oct
<memory at 0x02E71E68>
>>> memv_oct.tolist()
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4
>>> numbers
array('h', [-2, -1, 1024, 1, 2])
>>>
这里利用含有 5 个短整型有符号整数的数组(类型码是 'h')创建一个memoryview,然后创建一个 memv_oct,这一次是把 memv 里的内容转换成 'B' 类型,也就是无符号字符。把位于位置 5 的字节赋值成 4,因为我们把占 2 个字节的整数的高位字节改成了 4,所以这个有符号整数的值就变成了 1024。
双端队列
collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33])
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40])
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
>>>
maxlen 是一个可选参数,设定之后就不能修改,当队列中的元素超出这个值之后,就会自动删除多出的元素,如果从左端加入,就删除最右端的,如果从最右端加入,则从最左端删除。extendleft()方法是迭代插入的,所以插入后的顺序与原来的是相反的。
此外,append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序
中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。