引言

Pthon使用统一风格去处理序列数据。不管是哪种数据结构,字符串、列表、数组、XML,或者是数据库查询结果,它们都公用一套丰富的操作:迭代、切片、排序还有拼接。

内置序列类型概览

标准库提供了C语言实现的丰富的序列类型选择:

容器序列
list, tuple,collections.deque这种能保存不同类型(包括内嵌容器)的数据。
扁平序列(Flat sequences)
str, bytes, bytearray, memoryview, array.array这种只能保存同一类型数据。

容器序列存储的是对象的引用,可以是任何类型;而扁平序列存放的是值而不是引用。如图2-1所示:


上图左边是容器序列,右边是扁平序列。
带有...的灰色格子表示每个对象的内存地址头。
在左边,比如其中元组有其内容的引用数组。每个元素都是单独的Python对象,可能会是其他Python对象的引用。
而右边,Python中的array是一个单独的对象,持有3个C语言double类型的数组。

因此,扁平类型更加紧凑,但是局限于存储原始类型,如byte,integer,float等。

另一种分组序列类型的方式是基于它们的可变性:

可变序列
list, bytearray, array.array, collections.deque, memoryview
不可变序列
tuple, str, bytes

图2-2展示了可变序列从不可变序列中继承了所有方法,同时自己实现了几个额外方法。

虽然内置的序列类型并不是直接从SequenceMutableSequence这两个抽象基类(Abstract Base Class,ABC)继承而来的,但是他们是注册到这些ABC上的虚子类(virtual subclass)。做为虚子类,tuplelist能通过下面的测试:

from collections import abc
print(issubclass(tuple,abc.Sequence)) # True
print(issubclass(list,abc.MutableSequence)) # True

最基础、最常用的序列类型是list,你应该了解了它的基本使用。下面直接介绍列表推导式。

列表推导和生成器表达式

构建列表的快速方法是使用列表推导或生成器表达式(可以创建任何类型的序列)。

列表推导式比传统的for循环可读性更好,并且速度更快。

列表推导和可读性

例1:把字符串变成Unicode码位

symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:codes.append(ord(symbol))codes # [36, 162, 163, 165, 8364, 164]

例2:把字符串变成Unicode码位的另一种写法

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

相当于列表推导把3行代码精简为1行。如果懂了列表推导,就会发现它的写法更可读。

但是写列表推导时要确保代码的精简,如果它多于两行,可能需要改写成for循环的形式了。

列表推导不会再有变量泄露的问题

列表推导和生成器表达式有它们自己的局部作用域,和函数一样。表达式内部的变量和赋值只在局部起作用,而表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。

x = 'CBA'
codes = [ord(x) for x in x]
print(x) # 'CBA'
print(codes) # [67, 66, 65]
  • 第3行打印说明,x还是引用到’CBA’
  • 列表推导也产生了期望的列表

但不建议这样使用同名变量,很容易让人困惑,不需要这么省。

列表推导从序列或任何其他可迭代类型中构建列表,同时可以在构建时过滤和转换元素。filtermap合起来能做同样的事情,但是可读性就没那么好了。如下所示。

列表推导 vs filter和map

列表推导能做filtermap能做的所有事情,而且不会受到lambda表达式功能改变所带来的限制。

symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
print(beyond_ascii) # [162, 163, 165, 8364, 164]
beyond_ascii = list(filter(lambda c: c > 127, map(ord , symbols)))
print(beyond_ascii) # [162, 163, 165, 8364, 164]

下面我们来看下如何使用列表推导来计算笛卡尔积:两个或以上的列表中的元素对构成元组,这些元组构成的列表就是笛卡尔积。

笛卡尔积

用列表推导可以生成两个或以上的可迭代类型的笛卡尔积。笛卡尔积是一个列表,列表里的元素是由输入的可迭代类型的元素对构成的元组,因此笛卡尔积的长度等于输入变量的长度的乘积,如图2-3所示:


举个例子,假设你需要一个列表,列表里是3中不同尺寸的T-shirt,每个尺寸都有2个颜色,下面的代码展示了如何通过列表推导产生这样的列表,返回结果是2×3=62 \times 3=62×3=6个元素:

colors = ['black', 'white']
sizes = ['S', 'M', 'L']
# 生成元组的列表,首先根据color,然后根据size
tshirts = [(color, size) for color in colors for size in sizes]
tshirts


再来看一下同等的for循环写法:

for color in colors:for size in sizes:print((color, size))

还可以使用列表推导首先根据size,然后根据color生成:

tshirts = [(color, size) for size in sizes for color in colors]
tshirts


列表推导式的作用只有一个:生成列表。如果想生成其他序列类型,生成器表达就派上了用场。

生成器表达式

虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产生元素,显然能节省内存。

生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
下面展示了如何用生成器表达式构建元组和数组。

symbols = '$¢£¥€¤'
print( tuple(ord(symbol) for symbol in symbols) ) # (36, 162, 163, 165, 8364, 164)
import array
print( array.array('I',(ord(symbol) for symbol in symbols)) )  # array('I', [36, 162, 163, 165, 8364, 164])


下面的代码则展示了使用生成器表达式实现一个笛卡尔积,用来打印上文中T-shirts的两种颜色和三种尺码的所有组合。与上文代码不同的是,用到生成器表达式之后,内存里不会留下一个由6个组合的列表,因为声词器表达式会在每次for循环运行时才生成一个组合。

colors = ['black', 'wihte']
size = ['S', 'M', 'L']
# 生成器表达式逐个产生元素,不会一次性产出一个含有6个tshrit样式的列表
for tshirt in ('%s %s' % (c ,s) for c in colors for s in sizes) :print(tshirt)

输出:

black S
black M
black L
wihte S
wihte M
wihte L

下面我们来看下另一个重要的序列类型:元组

元组不仅是不可变列表

元组有两个主要作用:可以当做不可变列表,还可以用于没有字段名的记录。

元组和记录

元组保存记录:元组中的每个元素都存放了记录中一个字段的数据,以及这个字段的位置。正是这个位置信息给数据赋予了意义。

如果把元组当做一些字段的集合,那么元组元素数量和位置信息就变得非常重要了。

下面的代码展示了元组被当成记录使用。如果我们在元组内对元素排序,那么这些元素所携带的信息就会丢失,因为这些信息是跟它们的位置有关的。

# Los Angeles 的坐标
lax_coordinates = (33.9425, -118.408056)
# 东京的相关信息:名称,年份,人口(千),人口增长比,面积(平方千米)
city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)
# 元组列表,格式:(城市编码,护照编码)
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),  ('ESP', 'XDA205856')]
# passport 代表每个列表元素:元组
for passport in sorted(traveler_ids):print( '%s/%s' % passport)
# 这种写法,直接抽取元组中的元素,叫做拆包,这里我们不关心第二个元素,所以用_代替
for country, _ in traveler_ids:print(country)


从上面的例子可以看到,位置信息是很重要的。比如经纬度元组中的格式为:(经度,纬度)。

拆包让元组可以完美地被当做记录来使用。下面来看一下。

拆包

在上例中,我们把元组('Tokyo', 2003, 32_450, 0.66, 8014)里的元素分别赋值给变量city,year,pop,chg,area,这所有的赋值,我们只用了一行。

同样,在后面一行中,一个%运算符就把passport元组里的元素对应到了print函数的格式字符串slot中。这就是两个拆包的例子。

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

最好辨认的元组拆包形式就是平行赋值,即,从一个可迭代对象赋值到元组变量,如下所示:

lax_coordinates = (33.9425, -118.408056)
latitude,longitude = lax_coordinates # 拆包
print(latitude,longitude) # 33.9425 -118.408056

使用元组的一种优雅的应用是在替换变量上:

b ,a = a , b

还可以用 *运算符把一个可迭代对象拆开作为函数的参数:

t = (20, 8)
divmod(*t)
quotient, remainder = divmod(*t)
quotient, remainder


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

import os
_, filename = os.path.split('~/.ssh/id_rsa.pub')
filename # 'id_rsa.pub'

除此之外,在元组拆包中使用 * 也可以帮助我们把注意力集中在元组的部分元素上。

使用*来捕获剩下的元素

函数用 *args 来获取不确定数量的参数是一种经典的Python特性。
在Python 3 里,这个概念被扩展到了平行赋值中:

在平行赋值中,*前缀只能用在一个变量名前面,但是该变量名可以出现在任何位置:

最后,一个元组拆包强大的特性是它能应用于嵌套结构。

嵌套元组拆包

接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。只要对应嵌套结构相匹配,Python 就可以作出正确的选择。下面的代码就是对嵌套元组拆包的应用:

# 每个元组都有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)),
]
def main():print(f'{"":15} | {"lat.":^9} | {"long.":^9}')# 通过将最后一个字段赋值给一个元组来拆包for name, cc, pop, (latitude, longitude) in metro_areas:  if longitude <= 0:print(f'{name:15} | {latitude:9.4f} | {longitude:9.4f}')main()


元组已经设计得很好用了,但作为记录来用的话,还是少了一个功能:给记录中的字段命名。namedtuple 工厂的出现帮我们
解决了这个问题。我们会在第5章探讨这个知识点。

作为不可变列表的元组

Python将元组用作不可变列表,你也应该要这样。这带来两个好处:

  1. 清晰:当你看到元组,你就知道它的长度是不会变的。
  2. 性能:长度相同时,元组用的内存比列表少,同时元组可以让Python做一些优化。

然而,需要明确的是,元组的不可变现仅仅应用于它存储的引用。这些引用不能被替换或删除。但是引用指向的对象可能是可变的。下面的代码片段通过创建两个初始化相等的元组来说明这一点。当元组b的最后一个元素——列表,发生改变后,这两个元组也不相等了。

a = (10, 'alpha', [1, 2])
b = (10, 'alpha', [1, 2])
print(a == b)
b[-1].append(99)
print(a==b)
print(b)


初始时,元组的内存示意图如下:

如果你想确保元组保持不变,你可以计算它的哈希。只有值不可变的对象才是可哈希的(hashable)。因此,可以这样验证一个元组是否真的是不可变的:

def fixed(o):try:hash(o)except TypeError:return Falsereturn Truetf = (10, 'alpha', (1, 2))
tm = (10, 'alpha', [1, 2])
print(fixed(tf)) # True
print(fixed(tm)) # False

除了这点要注意之外,元组被广泛的用作不可变列表。

元组VS列表方法

当把元组当成不可变列表时,知道它们API的相似性是必要的。如表2-1所示,元组支持所有不涉及增加或删除元素的列表方法,除了没有__reversed__方法之外。然后,这只是为了优化,我们可以用reversed(my_tuple)实现这一点。

list tuple
s.__add__(s2) ⭕️ ⭕️ s + s2,拼接
s.iadd(s2) ⭕️ s += s2,原地拼接
s.append(e) ⭕️ 在尾部添加一个元素
s.clear() ⭕️ 删除所有元素
s.__contains__(e) ⭕️ ⭕️ e in s
s.copy() ⭕️ 列表浅拷贝
s.count(e) ⭕️ ⭕️ es中出现的次数
s.__delitem__(p) ⭕️ 把位于p的元素删除
s.extend(it) ⭕️ 把可迭代对象it追加给s
s.__getitem__(p) ⭕️ ⭕️ 获取位置p的元素
s.__getnewargs__() ⭕️ pickle中支持更优化的序列化
s.index(e) ⭕️ ⭕️ s中找到元素e第一次出现的位置
s.insert(p, e) ⭕️ 在位置p之前插入元素e
s.__iter__() ⭕️ ⭕️ 获取迭代器
s.__len__() ⭕️ ⭕️ len(s),元素的数量
s.__mul__(n) ⭕️ ⭕️ s * nns的重复拼接
s.__imul__(n) ⭕️ s *= n,原地重复拼接
s.__rmul__(n) ⭕️ ⭕️ 反向拼接(第16章探讨)
s.pop([p]) ⭕️ 删除并返回最后或可选的p位置的元素
s.remove(e) ⭕️ 删除s中第一次出现的e
s.reverse() ⭕️ 原地逆序
s.__reversed__() ⭕️ 返回s的逆序迭代器
s.__setitem__(p, e) ⭕️ s[p]=e,将元素e放在位置p,替换之前的元素
s.sort([key], [reverse]) ⭕️ 原地对s进行排序,可选的参数有keyreverse

切片

列表、元组和字符串这类序列类型都支持切片操作,但实际上切片比人们想象的要强大的多。

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

这种做法符合以0作为初始下标的传统。这样带来的好处如下:

  • 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)my_list[:3] 都返回 3 个元素。
  • 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
  • 可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x]my_list[x:] 就可以了。

如下所示:

接下来进一步看看 Python 是如何解释切片操作的。

对象切片

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


a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)。对seq[start:stop:step] 进行求值的时候,Python 会调用seq.__getitem__(slice(start, stop, step))
你还可以给切片命名,就像电子表格软件里给单元格区域取名字一样。

比如,要解析下面代码中所示的纯文本文件,这时使用有名字的切片比用硬编码的数字区间要方便得多,注意示例里的 for 循环的可读性有多强。

invoice = """
... 0.....6.................................40........52...55........
... 1909  Pimoroni PiBrella                     $17.50    3    $52.50
... 1489  6mm Tactile Switch x20                 $4.95    2     $9.90
... 1510  Panavise Jr. - PV-201                 $28.00    1    $28.00
... 1601  PiTFT Mini Kit 320x240                $34.95    1    $34.95
... """
SKU = slice(0,6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY =  slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split('\n')[2:]
for item in line_items:print(item[UNIT_PRICE], item[DESCRIPTION])


invoice.split('\n')[2:]是去掉不相关的头两行。

如果从Python用户的角度来看,切片还包含像多维切片和省略表示法(...)这两个额外的特性。

多维切片和省略

[] 运算符里还可以使用以逗号分开的多个索引或切片,要正确处理这种 [] 运算符的话,对象的特殊方法 __getitem____setitem__ 需要以元组的形式来接收a[i, j] 中的索引。也就是说,如果要得到 a[i, j] 的值,Python 会调用 a.__getitem__((i, j))

这种做法在Numpy中就用到了,二维的 numpy.ndarray 就可以用 a[i,j] 这种形式来获取,或者是用 a[m:n, k:l] 的方式来得到二维切片。

Python内建的序列类型都是一维的,除了memoryview,所以它们只支持单一索引或切片,而不是元组的形式。

省略(ellipsis)的正确书写方法是三个英语句号(...)。省略在 Python 解析器眼里是一个符号,而实际上它是 Ellipsis 对象的别名,而 Ellipsis 对象又是 ellipsis 类的单一实例。 它可以当作切片规范的一部分,也可以用在函数的参数中,比如 f(a, ..., z),或a[i:...]。在NumPy 中,... 用作多维数组切片的快捷方式。如果 x 是四维数组,那么 x[i, ...] 就是 x[i, :, :, :] 的缩写。

切片不仅用于从序列中抽取内容,还可以原地修改可变序列。

赋值给切片

如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对序列进行移植(grafted)、切除或原地修改操作。通过下面这几个例子,你应该就能体会到这些操作的强大功能:

对序列使用+*

Python程序员会默认序列是支持+*操作的。通常+号两侧的序列由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python会新建一个包含同类型数据的序列来作为拼接的结果。

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

l = [1, 2, 3]
l * 5
5 * 'abcd'

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

下节展示如何用*实例化列表组成的列表,其中包含了一些可能的陷阱。

建立由列表组成的列表

有时我们想用一些内嵌的列表来初始化一个列表,最好的方式是使用列表推导。如下:

# 建立一个包含3个列表的列表,嵌套的3个列表各自由3个元素来代表井字游戏的一行方块
board = [['_'] * 3 for i in range(3)]
board
board[1][2] = 'X' # 把第1行第2列的元素标记为X
board


上面是正确的做法,下面展示了另一种方法,一种吸引人但错误的做法:

# 外面的列表其实包含3个指向同一个列表的引用。
weird_board = [['_'] * 3] * 3
weird_board
# 一旦修改,就会暴露了列表内的3个引用指向同一个对象的事实
weird_board[1][2] = 'O'
weird_board


上例中犯的错误本质上跟下面的代码犯的错误一样:

row=['_'] * 3
board = []
for i in range(3):board.append(row)

相反,正确的做法等同于这样做:

board = []
for i in range(3):# 每次迭代都新建了一个列表,作为新的一行追加到boardrow = ['_'] * 3 board.append(row)
board
board[2][0] = 'X'
board # 只有第2行的元素被修改

序列的增量赋值

增量赋值操作+=*=的表现取决于它们的第一个操作对象。我们集中讨论增量加法,这些概念对增量乘法和其他增量运算符来说都是一样的。

+=后面的魔法方法是__iadd__。然而,如果__iadd__没有实现,Python会退一步调用__add__。考虑下面简单的表达式:

a += b

如果a实现了__iadd__,就会调用它。在可变序列(比如,listbytearrayarray.array)的情况下,a会原地修改。然而,当a没有实现__iadd__,那么表达式a += b就等同于a = a + b:首先会计算a+b,返回一个新对象,然后赋值给a。换言之,变量名会不会关联到新的对象,完全取决于是否实现__iadd__

上面所说的这些关于 += 的概念也适用于 *=,不同的是,后者相对应的是 __imul__
接下来有个小例子,展示的是 *= 在可变和不可变序列上的效果:

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

一个关于+=的谜题

读完下面的代码,然后回答这个问题:示例中的两个表达式到底会产生什么结果?

t = (1, 2, [30, 40])
t[2] += [50, 60]

到底会发生下面 4 种情况中的哪一种?
a. t 变成 (1, 2, [30, 40, 50, 60])
b. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
c. 以上两个都不是。
d. a 和 b 都是对的。

博主以为答案是a,实际上答案是d。下面是这段代码运行的结果,用的Python版本是3.8.8:

Python Tutor是一个对 Python 运行原理进行可视化分析的工具。图 2-3 里是两张截图,分别代表示例中 t的初始和最终状态。


下面来看看s[a] += b生成的字节码,可能这个现象背后的原因会变得清晰起来。

import dis
dis.dis('s[a] += b')

输出:

1            0 LOAD_NAME                0 (s)2 LOAD_NAME                1 (a)4 DUP_TOP_TWO6 BINARY_SUBSCR     # 将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端)8 LOAD_NAME                2 (b)10 INPLACE_ADD        # 计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对
象(示例中的列表)12 ROT_THREE14 STORE_SUBSCR      # [a] = TOS 赋值。这一步失败,是因为 s 是不可变的元组(示例中的元组)16 LOAD_CONST               0 (None)18 RETURN_VALUE

至此作者得到了 3 个教训。

  • 不要把可变对象放在元组里面。
  • 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
  • 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。

list.sort方法和内置函数sorted

list.sort方法原地对列表list进行排序,返回为None是为了提醒我们它没有创建一个新列表。这是一个重要的Python API约定:函数或方法如果原地修改对象应该返回None,好让调用者知道传入的参数发生了变动,而并未产生新的对象。例如,random.shuffle函数也遵守这个约定。

list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。

不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数:

  • reverse

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

下面是一些小例子来说明这些函数和关键字参数。这些例子也展示了Timsort是稳定的:

已排序的序列可以用来进行快速搜索,而标准库的 bisect模块给我们提供了二分查找算法。下一节会详细讲这个函数,顺便还会看看bisect.insort 如何让已排序的序列保持有序。

bisect来管理已排序的序列

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

bisect来搜索

bisect(haystack, needle)haystack(必须是一个有序序列)中搜索needle,来找到一个needle能插入的位置,并且还能保持haystack的增序。即,这个函数返回的位置前面的值,都小于或等于 needle 的值。其中 haystack 必须是一个有序的序列。你可以先用 bisect(haystack, needle) 查找位置 index,再用 haystack.insert(index, needle) 来插入新值。
但你也可用insort 来一步到位,并且后者的速度更快一些。

下面用几个精心挑选的needle来展示bisect返回的不同插入位置的值。

import bisect
import sysHAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
ROW_FMT = '{0:2d} @ {1:2d}    {2}{0:<2d}'def demo(bisect_fn):for needle in reversed(NEEDLES): # 逆序position = bisect_fn(HAYSTACK, needle)  #得到插入位置offset = position * '  |' print(ROW_FMT.format(needle, position, offset)) argv = ''
if argv == 'left':bisect_fn = bisect.bisect_left
else:bisect_fn = bisect.bisectprint('DEMO:', bisect_fn.__name__)
print('haystack ->', ' '.join(f'{n:2}' for n in HAYSTACK))
demo(bisect_fn)


每一行以 needle@position(元素及其应该插入的位置)开始,然后展示了该元素在原序列中的物理位置。

bisect的行为可以以两个方面微调。

首先可以用它的两个可选参数——lo(low) 和 hi(hight)——来缩小搜寻的范围。lo的默认值是 0,hi 的默认值是序列的长度,即 len() ,作用于该序列的返回值。

其次,bisect 函数其实是 bisect_right 函数的别名,后者还有个姊妹函数叫 bisect_left。它们的区别在于,bisect_left 返回的插入
位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面,而 bisect_right 返回的则是跟它相等的元素之后的位置。这个细微的差别可能对于整数序列来讲没什么用,但是对于那些值相等但是形式不同的数据类型来讲,结果就不一样了。比如说虽然 1 == 1.0 的返回值是 True,11.0 其实是两个不同的元
素。下图显示的是用 bisect_left 来运行上述示例的结果:

跟前面的图对比可以返现,值 1、8、23、29 和 30 的插入位置变成了原序列中这些值的前面。

bisect.insort插入新元素

排序很耗时,因此在得到一个有序序列之后,我们最好能够保持它的有序。bisect.insort 就是为了这个而存在的。
insort(seq, item) 把变量 item 插入到序列 seq 中,并能保持 seq的升序顺序。

import bisect
import randomSIZE = 7random.seed(1729)my_list = []
for i in range(SIZE):new_item = random.randrange(SIZE * 2)bisect.insort(my_list, new_item)print(f'{new_item:2d} -> {my_list}')

当列表不是首选时

虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。
比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻
译,也就是字节表述。
再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。

数组

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

Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char),因此 array('b') 创建出的数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存放除指定类型之外的数据。

下面展示了从创建一个1000万个随机浮点数的数组开始,到如何把这个数组存放到文件里,再到如何从文件读取这个数组。

from array import array
from random import random
# d表示双精度浮点类型
floats = array('d', (random() for i in range(10**7)))
print(floats[-1])
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()
print(floats2[-1])
print(floats2 == floats) # 检查两个数组的内容是不是完全一样

输出:

0.5963321947530882
0.5963321947530882
True

这段代码很简单,并且tofilefromfile也很快。

下表对数组和列表的功能做了一些总结。

list array
s.__add__(s2) ⭕️ ⭕️ s + s2—拼接
s.__iadd__(s2) ⭕️ ⭕️ s += s2—原地拼接
s.append(e) ⭕️ ⭕️ 在尾部添加一个元素
s.byteswap() ⭕️ 交换数组中元素的字节顺序
s.clear() ⭕️ 删除所有元素
s.__contains__(e) ⭕️ ⭕️ e in s
s.copy() ⭕️ 列表浅拷贝
s.__copy__() ⭕️ 支持copy.copy
s.count(e) ⭕️ ⭕️ es中出现的次数
s.__deepcopy__() ⭕️ 支持 copy.deepcopy
s.__delitem__(p) ⭕️ ⭕️ 把位于p的元素删除
s.extend(it) ⭕️ ⭕️ 把可迭代对象it追加给s
s.frombytes(b) ⭕️ 将字节序列b解读为机器值的数组,并添加
s.fromfile(f, n) ⭕️ f中读取 n 项(解读为机器值)并将它们添加到数组末尾。
s.fromlist(l) ⭕️ 添加来自列表l的项
s.__getitem__(p) ⭕️ ⭕️ 获取位置p的元素
s.index(e) ⭕️ ⭕️ s中找到元素e第一次出现的位置
s.insert(p, e) ⭕️ ⭕️ 在位置p之前插入元素e
s.itemsize ⭕️ 在内部表示中一个数组项的字节长度
s.__iter__() ⭕️ ⭕️ 获取迭代器
s.__len__() ⭕️ ⭕️ len(s),元素的数量
s.__mul__(n) ⭕️ ⭕️ s * nns的重复拼接
s.__imul__(n) ⭕️ ⭕️ s *= n,原地重复拼接
s.__rmul__(n) ⭕️ ⭕️ 反向拼接(第16章探讨)
s.pop([p]) ⭕️ ⭕️ 删除并返回最后或可选的p位置的元素
s.remove(e) ⭕️ ⭕️ 删除s中第一次出现的e
s.reverse() ⭕️ ⭕️ 原地逆序
s.__reversed__() ⭕️ 返回s的逆序迭代器
s.__setitem__(p, e) ⭕️ ⭕️ s[p]=e,将元素e放在位置p,替换之前的元素
s.sort([key], [reverse]) ⭕️ 就地排序序列,可选参数有keyreverse
s.tobytes() ⭕️ 将数组转换为一个机器值数组并返回其字节表示
s.tofile(f) ⭕️ 将所有项(作为机器值)写入到文件对象f
s.tolist() ⭕️ 将数组转换为包含相同项的普通列表
s.typecode ⭕️ 包含所有可用类型码的字符串。

memoryview

memoryview是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。

memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。

在下面的示例里,我们利用 memoryview 精准地修改了一个数组的某个字节,这个数组的元素是 16 位二进制整数。

import arraynumbers = array.array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)# 利用短整型有符号整数数组创建memoryview
print(len(memv)) # 5
print(memv[0]) # -2
memv_oct = memv.cast('B') # 把memv里的内容转换成'B',即无符号字符
# 打印为列表
print(memv_oct.tolist()) # [254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
memv_oct[5] = 4
print(numbers) # array('h', [-2, -1, 1024, 1, 2])


因为我们把占2个字节的整数的高位字节改成了4,所以这个有符号整数的值就变成了1024。

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

利用.append.pop方法,我们可以把列表当做栈或者队列来用。但是删除列表的第一个元素之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。

collections.deque类是一个线程安全、可以快速从两端添加或删除元素的数据类型。下面展示双向队列的操作:

下表总结了列表和双向队列这两个类型的用法。appendpopleft都是原子操作,即可以在多线程程序中安全地使用。

list deque
s.__add__(s2) ⭕️ s + s2—拼接
s.__iadd__(s2) ⭕️ ⭕️ s += s2—原地拼接
s.append(e) ⭕️ ⭕️ 在尾部添加一个元素
s.appendleft(e) ⭕️ 在头部添加一个元素
s.clear() ⭕️ ⭕️ 删除所有元素
s.__contains__(e) ⭕️ e in s
s.copy() ⭕️ 列表浅拷贝
s.__copy__() ⭕️ 支持copy.copy
s.count(e) ⭕️ ⭕️ es中出现的次数
s.__delitem__(p) ⭕️ ⭕️ 把位于p的元素删除
s.extend(i) ⭕️ ⭕️ 把可迭代对象i追加给s
s.extendleft(i) ⭕️ 把可迭代对象i从头部插入到s
s.__getitem__(p) ⭕️ ⭕️ 获取位置p的元素
s.index(e) ⭕️ s中找到元素e第一次出现的位置
s.insert(p, e) ⭕️ 在位置p之前插入元素e
s.__iter__() ⭕️ ⭕️ 获取迭代器
s.__len__() ⭕️ ⭕️ len(s),元素的数量
s.__mul__(n) ⭕️ s * nns的重复拼接
s.__imul__(n) ⭕️ s *= n,原地重复拼接
s.__rmul__(n) ⭕️ 反向拼接(第16章探讨)
s.pop() ⭕️ ⭕️ 删除并返回最后的元素
s.popleft() ⭕️ 删除并返回头部的元素
s.remove(e) ⭕️ ⭕️ 删除s中第一次出现的e
s.reverse() ⭕️ ⭕️ 原地逆序
s.__reversed__() ⭕️ ⭕️ 返回s的逆序迭代器
s.rotate(n) ⭕️ 把 n 个元素从队列的一端移到另一端
s.__setitem__(p, e) ⭕️ ⭕️ s[p]=e,将元素e放在位置p,替换之前的元素
s.sort([key], [reverse]) ⭕️ 就地排序序列,可选参数有 key 和 reverse

除了 deque 之外,还有些其他的 Python 标准库也有对队列的实现。

  • queue

    提供了同步(线程安全)类 QueueLifoQueuePriorityQueue,不同的线程可以利用这些数据类型来交换信息。
    这三个类的构造方法都有一个可选参数 maxsize,它接收正整数作为输入值,用来限定队列的大小。但是在满员的时候,这些类不会扔掉旧的元素来腾出位置。相反,如果队列满了,它就会被锁住,直到另外的线程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量。

  • multiprocessing
    这个包实现了自己的 无界的SimpleQueue和有界的Queue,它跟 queue.Queue 类似,是设计给进程间通信用的。同时还有一个专门的multiprocessing.JoinableQueue 类型,可以让任务管理变得更方便。

  • asyncio
    Python 3.4 新提供的包,里面有QueueLifoQueuePriorityQueueJoinableQueue,这些类受到 queuemultiprocessing 模块的影响,但是为异步编程里的任务管理提供了专门的便利。

  • heapq
    跟上面三个模块不同的是,heapq 没有队列类,而是提供了heappushheappop 方法,让用户可以把可变序列当作堆队列或者优先队列来使用。

总结

主要介绍了常见的序列类型。

《流畅的Python第二版》读书笔记——序列数组相关推荐

  1. 《流畅的Python第二版》读书笔记——函数作为一等对象

    引言 这是<流畅的Python第二版>抢先版的读书笔记.Python版本暂时用的是python3.10.为了使开发更简单.快捷,本文使用了JupyterLab. 函数是Python的一等( ...

  2. 《流畅的Python第二版》读书笔记——函数中的类型注解

    引言 这是<流畅的Python第二版>抢先版的读书笔记.Python版本暂时用的是python3.10.为了使开发更简单.快捷,本文使用了JupyterLab. 本章关注于Python在函 ...

  3. Python核心教程(第二版)读书笔记(三)

    第三章Python基础 2010-04-09 换行  一行过长的语句可以使用反斜杠'\'分解成几行.有两种例外情况一个语句不使用反斜线也可以跨行. 1.在使用闭合操作符时,单一语句可以跨多行.例如:在 ...

  4. 深入理解JVM(第二版读书笔记)

    一  开始前 HotSpot:http://xiaomogui.iteye.com/blog/857821 http://blog.csdn.net/u011521890/article/detail ...

  5. 《细说PHP》第二版--读书笔记

    第五章 PHP的基本语法 5.2.4 在程序中使用空白的处理 5.3 变量 5.3.1 变量的声明 在php中变量的声明必须是使用一个$符号,后面跟变量名来表示 unset()函数释放指定变量 iss ...

  6. 【我的JS第三本】JavaScript_DOM编程艺术第二版读书笔记

    经过前一段时间HTML&CSS的学习,感觉视频加读书是一个比较不错的学习方法,两者相辅相成,互相补充,所以也准备看看关于JavaScript的书. 2015年12月14日,之前使用韩顺平老师的 ...

  7. 刘鹏老师和王超老师的计算广告第二版读书笔记

    广告的定义与目的 广告的基本概念 广告的分类 在线广告的表现形式 横幅广告 文字链广告 富媒体广告 视频广告 交互式广告 社交广告 移动广告 邮件营销广告 广告的基本概念 需求方:可以是广告主.代表广 ...

  8. 《计算广告》第二版 读书笔记

    在线广告创意类型: 横幅广告 文字链广告 富媒体广告 视频广告 社交广告 移动广告 邮件定向营销广告. 广告发展历程: 合约广告->定向广告->竞价广告 (上下文广告) 术语解释: ADN ...

  9. sql注入攻击与防御第二版读书笔记二——SQL盲注利用

    寻找并确认SQL盲注 强制产生通用错误 注入带副作用的查询 如 mssql waitfor delay '0:0:5' mysql sleep() 拆分与平衡 5 -> 7-2 常见SQL盲注场 ...

  10. Effective Java 英文 第二版 读书笔记 Item 14:In public classes,use accessor methods,not public fields...

    本章主要分析 公开属性与私有属性提供公开get.set方法两种方式对比 // Degenerate classes like this should not be public! class Poin ...

最新文章

  1. 虚拟服务器系统一般用那种,虚拟主机 选什么系统
  2. OpenCASCADE:Mac OS X平台使用Code::Blocks构建OCCT
  3. Keyword-Driven Testing
  4. java byte json_关于java:当前推荐的将byte []转换为JsonNode并返回的方法
  5. 数据结构实践项目——图的基本运算及遍历操作
  6. 简单mysql主从配置
  7. Web项目:校园社团管理系统
  8. wox wpm 安装 有道插件
  9. 2012第33周国内Android应用下载动态
  10. 应用于大数据分析的工作流调度系统
  11. 常用工具类之jwt的学习使用
  12. 信息安全文章搜索引擎技术原理
  13. java读取含有合并行的excel
  14. 命令行hbase shell操作hbase
  15. Design a Facebook NewsFeed
  16. UI设计需要学会哪些软件?
  17. 局域网 --- 共享文件夹设置与访问
  18. socket事例代码
  19. ZSC 1306: 沼跃鱼早已看穿了一切 题解
  20. 关于电影主题HTM5网页设计作业成品——千与千寻在线电影(9个页面) HTML+CSS+JavaScript

热门文章

  1. php中foreach()的用法
  2. 修改注册表设置桌面和收藏夹路径
  3. JSONSerializer把类转换成JSON字符串
  4. 收到群硕的offer了
  5. SQL SERVER 查找某个字符在字符串中出现的次数
  6. latex中pdflatex与xelatex的区别
  7. Java IO输入输出流 字符数组流 ByteArrayOutputStream/ByteArrayInputStream
  8. 2015 Changchun Regional
  9. stat---文件状态信息结构体
  10. Java Memcached的使用