流畅的Python(2)

2、序列构成的数组

2.1、内置序列类型

容器序列:list、tuple 和 collections.deque可以存放不同类型的数据
扁平序列:str、bytes、array.array、bytearray只能容纳一种类型

区别:容器序列中存放的是任意类型对象的引用,扁平序列里存放的是值而不是引用(就是连续的内存空间)。

可变序列:list、bytearray、array.array、deque 和 dict
不可变序列:tuple、str 和 bytes


2.2、列表(list)

列表推导和生成器表达式
列表推导:是构建列表的快捷方式,只能生成列表。
生成器表达式:用来创建其他任何类型的序列,生成列表以外的序列类型。

注意: 在列表推导式中有局部作用域,和函数类似,表达式内部的变量和赋值只会在局部起到作用。

列表推导式同filter和map的比较

>>> demo = 'abcde'
>>> result = [ord(i) for i in demo if ord(i) > 100]
>>> result
[101]
>>> result = list(filter(lambda i : i > 100, map(ord,demo)))
>>> result
[101]

生成器表达式

>>> demo = 'abcde'
>>> tuple(ord(i) for i in demo)
(97, 98, 99, 100, 101)
>>> import array
>>> array.array('I', (ord(i) for i in demo))
array('I', [97, 98, 99, 100, 101])

2.3、元组(tuple)

元组除了用作不可变列表,还可以用于没有字段名的记录。

namedtuple: collections.namedtuple是一个工厂函数,用来构建一个 带字段名的元组和一个有名字的类

注意: namedtuple构建的类的实例消耗的内存跟元组一样,因为字段名都存在对应的类里。这个实例跟普通对象实例也要小,因为Python不会用__dict__来存放这些实例的属性。

>>> from collections import namedtuple
>>> people = namedtuple('man', 'name sex age')
>>> demo = people('mac', 'joku', 21)
>>> demo
man(name='mac', sex='joku', age=21)
>>> demo.name
'mac'
>>> demo[2]
21

namedtuple:第一个参数是类名,第二个参数是类字段的名字,可以由字符串组成的可迭代对象或空格分割开的字符串。

>>> man_1 = ('max', 'laka', 23)
>>> demo1 = people(*man_1)
>>> demo1
man(name='max', sex='laka', age=23)
>>> demo1._fields
('name', 'sex', 'age')

_fields:属性是包含这个类所有字段名称的元组。

2.4、元组和列表的区别

元组相对列表缺失的方法:append、clear、copy、extend、insert、reverse、sort、remove一般对元素进行增、删、查、改在元组中都不存在。

2.5、切片

支持切片:支持切片操作的序列类型有list、tuple、str
slice()函数实现切片对象,主要用在切片操作函数的参数传递。

class slice(stop)
class slice(start, stop[, step])
>>> myslice = slice(5)
>>> myslice
slice(None, 5, None)
>>> arr = range(10)
>>> arr[myslice]
range(0, 5)
>>> arr[0:5]
range(0, 5)

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

>>> 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
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]
>>> 

注意:赋值左边对象是切片,那么右侧就必须是可迭代对象。


2.6、序列的 + 和 *

这两种方法,在拼接过程中序列都不会被修改,都是新建一个同样类型的序列作为结果。
我们通常会用[[]] * 3来初始化一个由列表组成的列表,但是列表包含的三个元素是引用,这三个引用指向的是同一个列表。

>>> mylist = [['_'] * 3 for i in range(3)]
>>> mylist
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> youlist = [['_'] * 3] * 3
>>> youlist
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]

mylist创建的二维列表在每次迭代中都新建一个列表
youlist创建的二维列表实际指向同一个列表的引用。


2.7、序列的增量赋值

增量赋值运算符*= 和 +=取决于第一个操作对象。
+=:对应的特殊方法是__iadd__,如果一个类没有实现改方法就会调用__add__
可变序列一般都实现了__iadd__方法,不可变序列就不支持这个__iadd__方法。

>>> l = [1, 2, 3]
>>> id(l)
4492677064
>>> l *= 2
>>> id(l)
4492677064
>>> t = (1, 2, 3)
>>> id(t)
4492681504
>>> t *= 2
>>> id(t)
4492551944

不可变序列进行重复操作,效率会很低,每次都有生成新对象,解释器需要将原来的对象中的元素复制到新对象中,再追加新元素。


关于+=的谜题

>>> t = (1, 2, [3, 4])
>>> t[2] += [5,6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [3, 4, 5, 6])

上述代码对元组进行添加数据,虽然抛出了异常但是还是添加成功了,可以将+=替换为extend()。

  • 不要把可变对象放在元组里。
  • 增量赋值不是一个原子操作。

2.8、list.sort方法和内置函数sorted

list.sort:就地进行排序,不会复制原列表,返回值是None,未产生新的对象。
sorted:这是一个内置函数,可以接受任何形式的可迭代对象作为参数(可变序列或生成器),最终返回一个列表

共同点:这两个参数都有两个可选关键字参数,reverse 和 key,改排序算法是稳定的。
reverse:默认值为False升序排列。
key:一个只有一个参数的函数,改函数会作用在序列的每个元素上。

>>> demo = ['a', 'r', 'g', 'b', 'd']
>>> sorted(demo)    # 进行升序排列
['a', 'b', 'd', 'g', 'r']
>>> demo
['a', 'r', 'g', 'b', 'd']   # 本身没有修改
>>> sorted(demo, key=len, reverse=True)
['a', 'r', 'g', 'b', 'd']   # 可见是稳定的
>>> sorted(demo, reverse=True)
['r', 'g', 'd', 'b', 'a']   # 降序排序

2.9、bisect管理已排序的序列

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

2.9.1、用bisect搜索元素

bisect(haystack, needle)在haystack里搜索needle的位置,使用二分搜索,必须保证haystack是有序序列。

>>> KEYS = [1,2,3,4,5,6,7,8,9,10]
>>> import bisect
>>> bisect.bisect(KEYS,10)
10
>>> bisect.bisect_right(KEYS,10)
10
>>> bisect.bisect_left(KEYS,10)
9

bisect:有两个可选参数lo和hi,lo默认是0,hi默认是序列长度。
bisect是bisect_right函数的别名,返回查找元素的右边位置。

2.9.2、用bisect插入新元素

insort(seq, item)把变量item插入到序列seq中,并保持seq的升序顺序。

>>> KEYS = [1,2,3,4,5,6,7,8,9,10]
>>> import bisect
>>> bisect.insort(KEYS,9.1)
>>> KEYS
[1, 2, 3, 4, 5, 6, 7, 8, 9, 9.1, 10]
>>> bisect.insort(KEYS,11,lo=0,hi=5)
>>> KEYS
[1, 2, 3, 4, 5, 11, 6, 7, 8, 9, 9.1, 10]
>>> 

insort:有两个可选参数lo和hi,来控制查找范围。
insort是insort_right函数的别名,将元素插入在右边(比如在1==1.0值相同,但是类型不同的时候又用)。

上面的两种方法对所有序列类型都适用,如果处理数字列表数组是更好选择


2.10、数据结构的选择

数组:当存放1000万个浮点数的话,数组(array)的效率要高,因为数组背后存的不是float对象,是数字的机器翻译,也就是字节表述
双端队列:当需要频繁对序列做先进先出的操作,deque(双端队列)的速度会更快。
集合:将需要包含操作(检查元素是否存在集合中)频率高的时候,用set(集合)会更合适,set专为检查元素是否存在做过优化,但是它不是序列,set是无序。


2.10.1、数组

需要只包含数字的列表array.array比list更高效。数组支持所有跟可变序列有关的操作,包括pop、insert、extend,数组还提供从文件读取和存入文件的更快方法,如frombytes和tofile

创建数组需要一个类型码,表示在底层C语言应该存放什么样的数据类型。

>>> from array import array     # 导入array类型
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))   # 生成双精度浮点数组(类型码'd'),用的可迭代对象是生成器表达式。
>>> floats[-1]
0.4019972618001888
>>> fp = open('/Users/hubo/floats.bin', 'wb')
>>> floats.tofile(fp)   # 将数组存入二进制文件里
>>> fp.close()
>>> floats1 = array('d')    # 新建一个双精度浮点数空数组
>>> fp = open('/Users/hubo/floats.bin', 'rb')
>>> floats1.fromfile(fp, 10**7)     # 将存入的浮点数从二进制文件读取
>>> fp.close()
>>> floats1[-1]
0.4019972618001888
>>> floats == floats1   # 判断数组内容是否一样
True

array.fromfile:从二进制文件里读取1000万个双精度浮点数只要0.1秒,比从文本里读取快60倍,因为后者使用内置的float方法把每行文字转换成浮点数。
array.tofile:写入到二进制文件,比每行一个浮点数方式写入文本快7本。1000万个这样的数存在二进制文件只占用80 000 000个字节,但是文本文件的话需要181 515 739个字节。

快速序列化数字类型
使用pickle模块,pickle.dump处理浮点数组的速度几乎跟array.tofile一样快,但是使用pickle可以处理几乎所有内置数组类型,包含复数、嵌套集合、自定义的类。

数组的排序,从Python3.4后不支持list.sord()操作,需要使用sorted函数新建一个数组。
a = array.array(a.typecode, sorted(a))


2.10.2、内存视图

如果经常和数组打交道,要学会使用memoryview(内置类),让用户在不复制内容的情况下操作同一个数组的不同切片。

>>> number = array('h', [-2, -1, 0, 1, 2])  # 生成短整形有符号的数组
>>> memv = memoryview(number)   # 创建一个内存视图
>>> len(memv)
5
>>> memv[0]
-2
>>> memv_oct = memv.cast('B')   # 将memv里的内容转换为'b'类型,无符号字符
>>> memv_oct.tolist()   # 用列表的形式去查看
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5]=4   # 位置为5的字节赋值为4
>>> number  # 占两个字节的整数的高位字节改为4,所以有符号的整数值变为1024
array('h', [-2, -1, 1024, 1, 2])
>>> memv_oct.tolist()
[254, 255, 255, 255, 0, 4, 1, 0, 2, 0]

2.10.3、双向队列和其他形式

利用append和pop方法,可以把列表当作栈或队列来用,但是删除列表的第一个元素(在第一个元素后添加元素)之类的操作都是很耗时的,因为会牵扯移动列表的所有元素。

colloections.deque类双向队列是一个线程安全,可以快速从两端添加或删除元素的数据类型。

>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)    # maxlen是一个可选参数,代表队列容纳的元素数量,一旦设定不能修改。
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3)    # 队列旋转操作,当参数n>0,队列右边的n个元素移动到左边,反之。
>>> 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)   # 当对已满(len(d)==d.maxlen)队列尾部添加操作,他的头元素会被删除掉。
>>> 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([44,55,66,77])    # 会把迭代器的元素添加到队列左边,元素逆序出现在队列里。
>>> dq
deque([77, 66, 55, 44, 3, 4, 5, 6, 7, 8], maxlen=10)

双向队列实现大部分列表的方法,也有自己的方法popleft和rotate,为了实现这些方法,双向队列付出了一些代价,队列中间删除元素操作会慢,因为它只在头尾进行了优化。
append和popleft都是原子操作,deque可以在多线程中安全的当作栈使用,而不用担心资源锁的问题。

其他Python标准库中对队列的实现,queue、multiprocessing、asyncio、heapq


2.11、总结

  • Python常见分类是可变和不可变序列,另一种分类就是扁平序列和容器序列。前者体积小、速度更快用起来简单,但是只能保存原子性的数据,如数字、字符和字节。容器序列比较灵活,但是当容器序列遇到可变对象时就要格外小心。
  • 除了列表推导要常用生成器表达式来提供灵活构建和初始化序列的方式。
  • 元组可以用作无名字段的记录,又可以看作不变的列表。
  • 对切片赋值是一个修改可变序列的捷径。
  • 增量赋值会区别对待可变和不可变序列。
  • 序列的sort方法和内置的sorted函数都比较灵活,都能接受一个函数作为可选参数指定排序算法比较大小。如果在插入新元素的时候还要保持有序序列的顺序,需要用到bisect.insort和bisect.bisect来快速查找。
  • 除了列表和元组,标准库里还有array.array。
  • 介绍了collections.deque这个类型。
  • 在处理纯数字列表,推荐使用数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值