序列构成的数组
内置序列类型概览
Python 标准库用 C 实现了丰富的序列类型,列举如下:
容器序列
list、tuple 和 collections.deque 这些序列能存放不同类型的数据
扁平序列
str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型
注:
容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。
序列类型还能按照能否被修改来分类:
可变序列
list、bytearray、array.array、collections.deque 和memoryview
不可变序列
tuple、str 和 bytes
列表推导和生成器表达式
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。如果你的代码里并不经常使用它们,那么很可能你错过了许多写出可读性更好且更高效的代码的机会。
举个🌰
1 ''' 2 需求:把一个字符串变成 Unicode 码位的列表 3 ''' 4 5 #普通的写法 6 symbols = '$¢£¥€¤' 7 codes = [] 8 for symbol in symbols: 9 codes.append(ord(symbol)) 10 else: 11 print('普通写法得到的codes:', codes) 12 13 #列表推倒式的写法 14 codes = [ord(symbol) for symbol in symbols] 15 print('列表推倒式写法得到的codes:', codes)
列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新建一个列表。Python 内置的 filter 和 map 函数组合起来也能达到这一效果,但是可读性上打了不小的折扣。
列表推导同filter和map的比较
filter 和 map 合起来能做的事情,列表推导也可以做,而且还不需要借助难以理解和阅读的 lambda 表达式
1 #普通列表推到式实现过滤功能 2 symbols = '$¢£¥€¤' 3 beyond_ascii = [ord(s) for s in symbols if ord(s) > 127] 4 print('普通列表推倒式过滤后的结果:', beyond_ascii) 5 6 #使用filter函数实现的效果 7 beyond_ascii_filter = list(filter(lambda s: s > 127, map(ord, symbols))) 8 print('使用filter高阶函数实现过滤的结果:', beyond_ascii_filter) 9 10 print(beyond_ascii == beyond_ascii_filter)
笛卡儿积
用列表推导可以生成两个或以上的可迭代类型的笛卡儿积。笛卡儿积是一个列表,列表里的元素是由输入的可迭代类型的元素对构成的元组,因此笛卡儿积列表的长度等于输入变量的长度的乘积,如图:
图:含有 4 种花色和 3 种牌面的列表的笛卡儿积,结果是一个包含 12 个元素的列表
如果你需要一个列表,列表里是 3 种不同尺寸的 T 恤衫,每个尺寸都有2 个颜色,用列表推导算出了这个列表,列表里有 6 种组合:
笛卡尔乘积的🌰:
1 ''' 2 需求:如果你需要一个列表,列表里是 3 种不同尺寸的 T 恤衫,每个尺寸都有2个颜色,用列表推导算出了这个列表,列表里有6种组合 3 ''' 4 5 colors = ['black', 'white'] 6 sizes = ['S', 'M', 'L'] 7 8 tshirts = [(size, color) for size in sizes for color in colors] 9 print('T恤巴拉巴拉的~:', tshirts) 10 11 #循环列表输出每种可能性 12 for shirt in tshirts: 13 print(shirt)
以上代码执行的结果为:
T恤巴拉巴拉的~: [('S', 'black'), ('S', 'white'), ('M', 'black'), ('M', 'white'), ('L', 'black'), ('L', 'white')] ('S', 'black') ('S', 'white') ('M', 'black') ('M', 'white') ('L', 'black') ('L', 'white')
生成器表达式
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存。
用生成器表达式初始化元组和数组:
1 symbols = '$¢£¥€¤' 2 r = tuple(ord(s) for s in symbols) 3 print('利用生成器初始化元祖:', r) 4 5 import array 6 7 a = array.array('I', (ord(symbol) for symbol in symbols)) 8 print(a)
以上代码执行的结果为:
利用生成器初始化元祖: (36, 162, 163, 165, 8364, 164) array('I', [36, 162, 163, 165, 8364, 164])
注意:
- 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
- array 的构造方法需要两个参数,因此括号是必需的。array 构造方法的第一个参数指定了数组中数字的存储方式。
我们提到过的 T 恤衫的 2 种颜色和 3 种尺码的所有组合。与示例 2-4不同的是,用到生成器表达式之后,内存里不会留下一个有 6 个组合的列表,因为生成器表达式会在每次 for 循环运行时才生成一个组合。如果要计算两个各有 1000 个元素的列表的笛卡儿积,生成器表达式就可以帮忙省掉运行 for 循环的开销,即一个含有 100 万个元素的列表。
使用生成器实现笛卡尔乘积:
1 colors = ['black', 'white'] 2 sizes = ['S', 'M', 'L'] 3 4 for tshirt in ('%s %s'%(color, size) for color in colors for size in sizes): 5 print(tshirt)
以上代码执行的结果为:
black S
black M
black L
white S
white M
white L
元组不仅仅是不可变的列表
有些 Python 入门教程把元组称为“不可变列表”,然而这并没有完全概括元组的特点。除了用作不可变的列表,它还可以用于没有字段名的记录。鉴于后者常常被忽略,我们先来看看元组作为记录的功用。
元组和记录
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。
举个🌰 把元组用作记录
1 lax_coordinates = (33.9425, -118.408056) 2 3 #解包,把元祖中的元素对应的索引对应成变量 4 city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014) 5 6 traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')] 7 for passport in sorted(traveler_ids): 8 print('%s/%s' % passport) 9 10 #元祖中的元素又是一个单独的元祖,可以通过解包的方式循环,不需要的数据可以通过_占位符来代替 11 for country, _ in traveler_ids: 12 print(country)
以上代码执行的结果为:
BRA/CE342567 ESP/XDA205856 USA/31195855 USA BRA ESP
元组拆包
我们把元组 ('Tokyo', 2003, 32450, 0.66, 8014) 里的元素分别赋值给变量 city、year、pop、chg 和 area,而这所有的赋值我们只用一行声明就写完了。同样,在后面一行中,一个 % 运算符就把 passport 元组里的元素对应到了 print 函数的格式字符串空档中。这两个都是对元组拆包的应用。
注意:
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用 * 来表示忽略多余的元素,在“用 * 来处理多余的元素”一节里,我会讲到它的具体用法。Python 爱好者们很喜欢用元组拆包这个说法,但是可迭代元素拆包这个表达也慢慢流行了起来,比如“PEP 3132—Extended IterableUnpacking”(https://www.python.org/dev/peps/pep-3132/)的标题就是这么用的。
拆包的写法:
1 lax_coordinates = (33.9425, -118.408056) 2 latitude, longitude = lax_coordinates #拆包 3 4 print('latitude:', latitude) 5 print('longitude:', longitude)
另外一个很优雅的写法当属不使用中间变量交换两个变量的值:
b, a = a, b
还可以用 * 运算符把一个可迭代对象拆开作为函数的参数:
>>> divmod(20, 8) (2, 4) >>> t = (20, 8) >>> divmod(*t) (2, 4) >>> quotient, remainder = divmod(*t) >>> quotient 2 >>> remainder 4
平行赋值栗子:
>>> 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, [])
在平行赋值中,* 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置:
>>> 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)
嵌套元组拆包
接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应。
1 #每个组内对应四个元素,其中最后一个元素为一对坐标点 2 metro_areas = [ 3 ('Tokyo','JP',36.933,(35.689722,139.691667)), 4 ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), 5 ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), 6 ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), 7 ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), 8 ] 9 #{}format的格式化占位符,:15是十五个宽度 ^9居中对齐九个宽度 10 print('{:15} | {:^9} | {:^9}'.format('', 'lat', 'long.')) 11 fmt = '{:15} | {:9.4f} | {:9.4f}' 12 13 for name, cc, pop, (latitude, longitude) in metro_areas: 14 #这个条件判断把输出限制在西半球的城 15 if longitude < 0: 16 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 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
举个🌰 定义和使用命名元组
1 from collections import namedtuple 2 3 ''' 4 创建一个命名元祖,需要两个参数,第一个是类名称,另外的是类各个字段的名称, 5 后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串 6 ''' 7 City = namedtuple('City', 'name country population coordinates') 8 9 #存放在对应字段里的数据要以一串参数的形式传入到构造函数中 10 tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) 11 12 #通过字段或者是索引来获取值 13 print(tokyo) 14 print(tokyo.name) 15 print(tokyo.population) 16 print(tokyo.coordinates) 17 print(tokyo[1])
除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性。示例 2-10 中就展示了几个最有用的:_fields 类属性、类方法_make(iterable) 和实例方法 _asdict()。
1 print(City._fields) 2 3 LatLong = namedtuple('LatLong', 'lat long') 4 delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889)) 5 #用_make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟 City(*delhi_data)是一样的 6 delhi = City._make(delhi_data) 7 8 #_asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来 9 print(delhi._asdict())
以上代码执行的结果为:
('name', 'country', 'population', 'coordinates') OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population', 21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))]) name: Delhi NCR country: IN population: 21.935 coordinates: LatLong(lat=28.613889, long=77.208889)
切片
在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。
为什么切片和区间会忽略最后一个元素
在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语言里以 0 作为起始下标的传统。这样做带来的好处如下:
-
当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3] 都返回 3 个元素
-
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可
-
这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了,如下所示
1 l = [10, 20 , 30 ,40, 50, 60] 2 3 #取l列表中切两个元素 4 print('l列表中的前两个元素:', l[:2]) 5 6 #从第二个索引开始取值 7 print('l列表从第二个索引开始往后取值:',l[2:])
以上代码直接的结果为:
l列表中的前两个元素: [10, 20]
l列表从第二个索引开始往后取值: [30, 40, 50, 60]
对对象进行切片
一个众所周知的秘密是,我们还可以用 s[a:b:c] 的形式对 s 在 a 和 b之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值。下面的 3 个例子更直观些:
1 s = 'bicycle' 2 3 print('s隔三个取值:', s[::3]) 4 5 print('s反向输出:', s[::-1]) 6 7 print('s反向输出,隔两个取值:', s[::-2])
以上代码直接的结果为:
s隔三个取值: bye
s反向输出: elcycib
s反向输出,隔两个取值: eccb
a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)。在 10.4.1 节中会讲到,对seq[start:stop:step] 进行求值的时候,Python 会调用seq.__getitem__(slice(start, stop, step))。就算你还不会自定义序列类型,了解一下切片对象也是有好处的。例如你可以给切片命名,就像电子表格软件里给单元格区域取名字一样。
举个🌰 纯文本文件形式的收据以一行字符串的形式被解析
1 invoice = """ 2 0.....6................................40........52...55........ 3 1909 Pimoroni PiBrella $17.50 3 $52.50 4 1489 6mm Tactile Switch x20 $4.95 2 $9.90 5 1510 Panavise Jr. - PV-201 $28.00 1 $28.00 6 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95 7 """ 8 9 SKU = slice(0, 10) 10 DESCRIPTION = slice(10, 40) 11 UNIT_PRICE = slice(40, 52) 12 QUANTITY = slice(52, 55) 13 ITEM_TOTAL = slice(55, None) 14 line_items = invoice.split('\n')[2:] 15 16 for item in line_items: 17 print(item[UNIT_PRICE], item[DESCRIPTION])
以上代码直接的结果为:
$17.50 Pimoroni PiBrella $4.95 6mm Tactile Switch x20 $28.00 Panavise Jr. - PV-201 $34.95 PiTFT Mini Kit 320x240
给切片赋值
举个🌰
1 l = list(range(10)) 2 print('l列表的原有元素:', l) 3 4 #利用切片赋值 5 l[2:5] = [20, 30] 6 print('利用l[2:5]以后的结果:', l) 7 8 #删除列表中指定索引范围的值 9 del l[5:7] 10 print('删除l列表第5-7个索引值以后的l列表:', l) 11 12 #从第三个索引开始,间隔两个索引赋值 13 l[3::2] = [11, 22] 14 print('l列表从第三个索引开始,隔两个元素赋值以后的l列表:', l)
以上代码直接的结果为:
l列表的原有元素: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 利用l[2:5]以后的结果: [0, 1, 20, 30, 5, 6, 7, 8, 9] 删除l列表第5-7个索引值以后的l列表: [0, 1, 20, 30, 5, 8, 9] l列表从第三个索引开始,隔两个元素赋值以后的l列表: [0, 1, 20, 11, 5, 22, 9]
注意:
如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。
序列的增量赋值
增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象。简单起见,我们把讨论集中在增量加法(+=)上,但是这些概念对 *= 和其他增量运算符来说都是一样的。
+= 背后的特殊方法是 __iadd__ (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__ 。考虑下面这个简单的表达式:
a += b
如果 a 实现了 __iadd__ 方法,就会调用这个方法。同时对可变序列(例如 list、bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b) 一样。但是如果 a 没有实现 __iadd__ 的话,a+= b 这个表达式的效果就变得跟 a = a + b 一样了:首先计算 a +b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现__iadd__ 这个方法。
举个🌰 展示的是 *= 在可变和不可变序列上的作用:
>>> l = [1, 2, 3] >>> id(l) 4316848904 >>> l *= 3 >>> l [1, 2, 3, 1, 2, 3, 1, 2, 3] >>> id(l) 4316848904 >>> t = (1, 2, 3) >>> id(t) 4316864656 >>> t *= 2 >>> t (1, 2, 3, 1, 2, 3) >>> id(t) 4316467112
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
list.sort方法和内置函数sorted
list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 None 的原因,提醒你本方法不会新建一个列表。在这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。例如,random.shuffle 函数也遵守了这个惯例。
与 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。
不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数。
reverse
如果被设定为 True,被排序的序列里的元素会以降序输出(也就是说把最大值当作最小值来排序)。这个参数的默认值是 False。
key
一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些字符串排序时,可以用 key=str.lower 来实现忽略大小写的排序,或者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒等函数(identity function),也就是默认用元素自己的值来排序。
举个🌰
>>> fruits = ['grape', 'raspberry', 'apple', 'banana'] >>> sorted(fruits) ['apple', 'banana', 'grape', 'raspberry'] >>> sorted(fruits, reverse=True) ['raspberry', 'grape', 'banana', 'apple'] >>> sorted(fruits,key=len, reverse=True) ['raspberry', 'banana', 'grape', 'apple'] >>> fruits ['grape', 'raspberry', 'apple', 'banana'] >>> fruits.sort() >>> fruits ['apple', 'banana', 'grape', 'raspberry']
用bisect来管理已排序的序列
bisect 模块包含两个主要函数,bisect 和 insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
用bisect来搜索
bisect(haystack, needle) 在 haystack(干草垛)里搜索needle(针)的位置,该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。也就是在说这个函数返回的位置前面的值,都小于或等于 needle 的值。其中 haystack 必须是一个有序的序列。你可以先用 bisect(haystack, needle) 查找位置 index,再用 haystack.insert(index, needle) 来插入新值。但你也可用insort 来一步到位,并且后者的速度更快一些。
举个🌰 在有序序列中用 bisect 查找某个元素的插入位置
1 import bisect 2 import sys 3 4 HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30] 5 NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31] 6 7 ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}' #格式化输出:左边的是占位符的位置0 1 2... :右边的是对齐宽度 8 9 10 def demo(bisect_fn): 11 for needle in reversed(NEEDLES): 12 position = bisect_fn(HAYSTACK, needle) 13 offset = position * ' |' 14 print(ROW_FMT.format(needle, position, offset)) 15 16 if __name__ == "__main__": 17 18 if sys.argv[-1] == 'left': 19 bisect_fn = bisect.bisect_left 20 else: 21 bisect_fn = bisect.bisect 22 23 print('DEMO:', bisect_fn.__name__) 24 print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK)) 25 demo(bisect_fn)
以上代码直接的结果为:
DEMO: bisect haystack -> 1 4 5 6 8 12 15 20 21 23 23 26 29 30 31 @ 14 | | | | | | | | | | | | | |31 30 @ 14 | | | | | | | | | | | | | |30 29 @ 13 | | | | | | | | | | | | |29 23 @ 11 | | | | | | | | | | |23 22 @ 9 | | | | | | | | |22 10 @ 5 | | | | |10 8 @ 5 | | | | |8 5 @ 3 | | |5 2 @ 1 |2 1 @ 1 |1 0 @ 0 0
根据一个分数,找到它所对应的成绩
1 import bisect 2 3 4 def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'): 5 i = bisect.bisect(breakpoints, score) #获取breakpoints的索引位置 6 print('{}在{}中的索引位置:{}, 英文的分数为:{}'.format(score, breakpoints, i, grades[i])) 7 return grades[i] #返回对应的英文成绩 8 9 grade(80) 10 grade(33) 11 12 [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]
以上代码直接的结果为:
80在[60, 70, 80, 90]中的索引位置:3, 英文的分数为:B 33在[60, 70, 80, 90]中的索引位置:0, 英文的分数为:F 33在[60, 70, 80, 90]中的索引位置:0, 英文的分数为:F 99在[60, 70, 80, 90]中的索引位置:4, 英文的分数为:A 77在[60, 70, 80, 90]中的索引位置:2, 英文的分数为:C 70在[60, 70, 80, 90]中的索引位置:2, 英文的分数为:C 89在[60, 70, 80, 90]中的索引位置:3, 英文的分数为:B 90在[60, 70, 80, 90]中的索引位置:4, 英文的分数为:A 100在[60, 70, 80, 90]中的索引位置:4, 英文的分数为:A
用bisect.insort插入新元素
排序很耗时,因此在得到一个有序序列之后,我们最好能够保持它的有序。bisect.insort 就是为了这个而存在的。
insort(seq, item) 把变量 item 插入到序列 seq 中,并能保持 seq的升序顺序,🌰如下:
1 import bisect 2 import random 3 4 SIZE = 7 5 6 random.seed(1729) 7 8 my_list = [] 9 for i in range(SIZE): 10 new_item = random.randrange(SIZE*2) 11 bisect.insort(my_list, new_item) 12 print('%2d ->' % new_item, my_list)
以上代码直接的结果为:
10 -> [10] 0 -> [0, 10] 6 -> [0, 6, 10] 8 -> [0, 6, 8, 10] 7 -> [0, 6, 7, 8, 10] 2 -> [0, 2, 6, 7, 8, 10] 10 -> [0, 2, 6, 7, 8, 10, 10]
当列表不是首选时
虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。
数组
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。数组支持所有跟可变序列有关的操作,包括 .pop、.insert 和.extend。另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes 和 .tofile。
举个🌰 一个浮点型数组的创建、存入文件和从文件读取的过程
1 from array import array 2 from random import random 3 4 5 #利用一个可迭代对象来建立一个双精度浮点数组(类型码是 'd'),这里我们用的可迭代对象是一个生成器表达式 6 floats = array('d', (random() for i in range(10**7))) 7 print(floats[-1]) 8 9 #把数组存入一个二进制文件里 10 with open('floats.bin', 'wb') as fb: 11 floats.tofile(fb) 12 13 14 floats2 = array('d') 15 16 #把 1000 万个浮点数从二进制文件里读取出来 17 with open('floats.bin', 'rb') as fb: 18 floats2.fromfile(fb, 10 ** 7) 19 20 print(floats2[-1]) 21 22 #检查两个数组的内容是不是完全一样 23 if floats == floats2: 24 print('Yes') 25 else: 26 print('NO')
以上代码执行的结果为:
0.25921054635939245
0.25921054635939245
Yes
从上面的代码我们能得出结论,array.tofile 和 array.fromfile 用起来很简单。把这段代码跑一跑,你还会发现它的速度也很快。一个小试验告诉我,用 array.fromfile 从一个二进制文件里读出 1000 万个双精度浮点数只需要 0.1 秒,这比从文本文件里读取的速度要快 60倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。
另外,使用 array.tofile 写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快 7 倍。另外,1000 万个这样的数在二进制文件里只占用 80 000 000 个字节(每个浮点数占用 8 个字节,不需要任何额外空间),如果是文本文件的话,我们需要 181 515 739个字节。
内存视图
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。
memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。
举个🌰 通过改变数组中的一个字节来更新数组里某个元素的值
1 import array 2 3 #利用含有 5 个短整型有符号整数的数组(类型码是 'h')创建一个memoryview 4 numbers = array.array('h', [-2, -1, 0, 1, 2]) 5 6 memv = memoryview(numbers) 7 print('memv[0]:', memv[0]) 8 9 #创建一个 memv_oct,这一次是把 memv 里的内容转换成 'B' 类型,也就是无符号字符 10 memv_oct = memv.cast('B') 11 12 #转换成list 13 print('memv to list:', memv_oct.tolist()) 14 15 print('memv_oct[5]:', memv_oct[5]) 16 17 #修改memv_oct第五个索引的值为4 18 memv_oct[5] = 4 19 20 print('numbers:', numbers)
以上代码执行的结果为:
memv[0]: -2 memv to list: [254, 255, 255, 255, 0, 0, 1, 0, 2, 0] memv_oct[5]: 0 numbers: array('h', [-2, -1, 1024, 1, 2])
NumPy和SciPy
凭借着 NumPy 和 SciPy 提供的高阶数组和矩阵操作,Python 成为科学计算应用的主流语言。NumPy 实现了多维同质数组(homogeneous array)和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。通过 NumPy,用户能对这些数据结构里的元素进行高效的操作。
举个🌰 对 numpy.ndarray 的行和列进行基本操作
1 import numpy 2 3 #创建一个一维数组,取值为0~11 4 a = numpy.arange(12) 5 print(a) 6 print('-'*40) 7 8 #数组的维度 9 print(a.shape) 10 print('-'*40) 11 12 #转换数组的维度为3行四列 13 a.shape = 3, 4 14 print('转换数组维度以后的取值:\n', a) 15 print('-'*40) 16 17 print('打印第2个索引的数据:', a[2]) 18 print('-'*40) 19 20 print('打印第2个索引中第1个索引的值:', a[2][1]) 21 print('-'*40) 22 23 print('打印第一列中的数据:', a[:, 1]) 24 print('-'*40) 25 26 #把行和列交过,得到一个新的数组 27 print('行和列交互以后的数组:\n', a.transpose()) 28 print('-'*40)
以上代码执行的结果为:
[ 0 1 2 3 4 5 6 7 8 9 10 11] ---------------------------------------- (12,) ---------------------------------------- 转换数组维度以后的取值: [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] ---------------------------------------- 打印第2个索引的数据: [ 8 9 10 11] ---------------------------------------- 打印第2个索引中第1个索引的值: 9 ---------------------------------------- 打印第一列中的数据: [1 5 9] ---------------------------------------- 行和列交互以后的数组: [[ 0 4 8] [ 1 5 9] [ 2 6 10] [ 3 7 11]] ----------------------------------------
NumPy 也可以对 numpy.ndarray 中的元素进行抽象的读取、保存和其他操作:
1 import numpy 2 3 #使用numpy创建一个数组 4 floats = numpy.array([ 3016362.69195522, 535281.10514262, 4566560.44373946]) 5 print(floats) 6 7 #对数组中的每个元素修改 8 floats *= .5 9 print(floats) 10 11 #把修改后的数组存储到文件中 12 numpy.save('floats-10M', floats) 13 14 #读取保存文件的内容 15 floats2 = numpy.load('floats-10M.npy', 'r+') 16 print('floats-10M:', floats2) 17 18 #修改读取文件中的数据 19 floats2 *= 6 20 print('打印读取数据乘以6以后的结果:', floats2) 21 22 #通过索引取值 23 print('取floats2中数组第2个索引的值:', floats2[2:]) 24 25 #倒序输出结果 26 print('倒序输出floats2的结果:', floats2[::-1])
以上代码执行的结果为:
[ 3016362.69195522 535281.10514262 4566560.44373946] [ 1508181.34597761 267640.55257131 2283280.22186973] floats-10M: [ 1508181.34597761 267640.55257131 2283280.22186973] 打印读取数据乘以6以后的结果: [ 9049088.07586566 1605843.31542786 13699681.33121838] 取floats2中数组第2个索引的值: [ 13699681.33121838] 倒序输出floats2的结果: [ 13699681.33121838 1605843.31542786 9049088.07586566]
双向队列和其他形式的队列
collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。
举个🌰 使用双向队列
1 from collections import deque 2 3 4 #创建一个双向队列,队列最大长度为10 5 dq = deque(range(10), maxlen=10) 6 print('初始化的双向队列:', dq) 7 8 #旋转队列 9 ''' 10 队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n 11 个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被 12 移动到右边 13 ''' 14 dq.rotate(3) 15 print('旋转队列以后:', dq) 16 17 dq.rotate(-4) 18 print('旋转队列以后:', dq) 19 20 #当往一个已满的队列中添加数据,会把开头的数据替换掉 21 dq.appendleft('20') 22 print('往队列中放入数据:', dq) 23 24 #往队列中扩展一个列表,前面的内容会被自动删除掉 25 dq.extend([20, 30 , 40]) 26 print('扩展以后的队列:', dq) 27 28 #从最队列的最前面扩展,后面的内容会被自动删除叼 29 dq.extendleft([100, 200, 300]) 30 print('列队最前面扩展以后的结果:', dq)
以上代码执行的结果为:
初始化的双向队列: deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) 旋转队列以后: deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10) 旋转队列以后: deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10) 往队列中放入数据: deque(['20', 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) 扩展以后的队列: deque([3, 4, 5, 6, 7, 8, 9, 20, 30, 40], maxlen=10) 列队最前面扩展以后的结果: deque([300, 200, 100, 3, 4, 5, 6, 7, 8, 9], maxlen=10)