fluent python学习笔记——python序列文章

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 可以在多线程程序
中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容