对 Python 中的序列类型进行操作是我们的必要需求。尤其是切片,以及从列表中建立一个新的列表等操作尤其需求的多。阅读这一部分,我收获很多。PS: 这篇博客有点长,一下看不完就请收藏吧。。。
切片
list, tuple, str 以及 Python 中的所有序列类型都支持切片操作,但是他们实际能做的事情比我们想象的多很多
为什么切片和 range 函数都不包括最后一个元素
Pythonic 的惯例是不包含切片中的最后一个元素的,这和 Python, C 语言中的用 0 作为位置索引的第一位是相吻合的。这些惯例包括:
- 当只给了切片上限的时候,可以很容易的看出切片和 range 函数的长度。无论是函数
range(3)
还是list[:3]
所得到的内容长度均为 3 - 当切片上下限都给了的时候,内容长度也很容易得到 stop - start ,即上限减去下限
- 把序列按照索引
x
分开而不发生重叠的方法很简单,list[:x]
和list[x:]
:例如
l = [10, 20, 30, 40, 50, 60]
l[:2] # [10, 20]
l[2:] # [30, 40, 50, 60]复制代码
切片对象
s[a:b:c]
可以用来指明切片的步长 c ,使得目标切片跳过相应的元素。切片的步长也可以为负,这将导致切片的方向为负方向。
s = 'bicycle'
s[::3] # 'bye'
s[::-1] # 'elcycib'
s[::-2] # 'eccb'复制代码
a:b:c
只有位于 []
内才生效,用来产生一个切片对象。当执行 seq[start:stop:step]
的时候, Python 会调用 seq.__getitem__(slice(start, stop, step))
。即使你没有在你自定义的序列类型中实现这个方法,知道切片对象是怎么产生作用,也是很有用的。
如果需要处理偏平的数据 (flat-like data),如例子2-11所示的那样,为了处理的更加明确,可以直接将对应的切片命名。这对于可读性来说,是很有帮助的。
# 例子2-11,处理偏平数据
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, 60)
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]复制代码
将对应的切片命名,可以便于我们阅读,找到我们需要提取的信息。
多维切片和省略号
在第三方包 NumPy 中,可以使用 []
操作符操作用逗号分隔的多个索引值或者切片来获取元素。最简单的操作便是 a[i, j]
,其中 a 的类型为 numpy.ndarray 。二维的切片操作为 a[m:n, k:l]
。__getitem__
和 __setitem__
特殊方法就是用来处理 []
的。为了执行 a[i, j]
,实际的 Python 调用为 a.__getitem__((i, j))
。里面的多维索引被封装成了元组传给了对应的特殊函数。但是 Python 内置的序列类型只是一维的,并不支持多维操作,sad。
Python 使用省略号来作为多维切片的简写,例如 x[i, ...]
等同于 x[i, :, :, :, ]
。此处 x 为四维数组。
对切片赋值
可变的序列类型可以用切片进行直接操作。
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]复制代码
对切片进行赋值时,右侧的对象必须得是可遍历对象,即使只有一个元素。
使用 +
和 *
来操作序列
程序员都期待序列类型支持 +
和 *
。当使用 +
操作的时候,两边的对象必须是相同的序列类型,而且两个对象都不能被改变,但是会产生一个相同类型的对象作为结果。
当把一个序列对象乘以一个整数的时候,一个新的序列会被创建,原序列不变。
l = [1, 2, 3]
l * 5 # [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
5 * 'abc' # 'abcdabcdabcdabcdabcd'复制代码
+
和 *
都不会改变其操作数,但是会产生一个新的对象。
小建议:
当进行
a * n
的操作时,而 a 又是一个包含可变元素的序列时,结果可能会很有趣。
a = [[1]] b = a * 3 # [[1], [1], [1]] a[0][0] = 2 a # [[2]] b # [[2], [2], [2]]复制代码
这里面的传递是引用传递,所以进行修改时,会影响到很多。这方面切记!
建立一个列表的列表
有时我们需要建立一个包含嵌套列表的列表,实现这个的最好办法就是列表解析。
# 例子2-12,包含三个长度为3列表的列表,可以用来表示一个一字棋
border = [['_'] * 3 for i in range(3)] # 注释1
print border # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
border[1][2] = 'X' # 注释2
print border # [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]复制代码
- 创建一个包含3个元素的列表,每个元素包含三个元素。注意查看其结构
- 在第一行第二列出放一个标记,查看结果
对比看来,例子2-13是一个错误的例子
# 例子2-13,同样的例子,但是是错的
weird_board = [['_'] * 3] * 3 # 注释1
print weird_board
weird_board[1][2] = '0' # 注释2
print weird_board复制代码
- 最外层列表有三个指向相同引用的列表组成。不改变的时候,看起来没毛病
- 在第一行第二列放置一个标记,可以发现每一行都指向了相同的元素
例子2-13其实相当于
row = ['_'] * 3
board = []
for i in range(3):
board.append(row) # 注释1复制代码
- 被添加到 board 里面的列表都是同一个
而对应的,例子2-12和以下代码是等同的。
board = []
for i in range(3):
row = ['_'] * 3 # 注释1
board.append(row)
print board # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[2][0] = 'X'
print board # 注释2
# [['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]复制代码
- 每次迭代都新建立了一个列表,并添加到了 board 后面
- 只有对应的第二列改变了,如我们所期待的那样
增广的序列赋值
+=
和 *=
可以和正常的运算非常不同。此处只讨论 +=
,其思想概念完全适用于 *=
。让其工作的特殊函数为 __iadd__
。如果这个特殊函数没有被实现, Python 会调用 __add__
作为备用。
a += b复制代码
如果 __iadd__
被实现了就会被调用。对于可变序列(如 list, bytearray, array.array),a 会在原地被改变。而如果 __iadd__
没有被实现,那么表达式 a += b
就和 a = a + b
完全一样了。同样的,*=
对应特殊函数 __imul__
,一下是一些简单例子。
l = [1, 2, 3]
id(l) # 4311953800 注释1
l *= 2
l # [1, 2, 3, 1, 2, 3]
id(l) # 4311953800 注释2
t = (1, 2, 3)
id(t) # 4312681568 注释3
t *= 2
id(t) # 4301348296 注释4复制代码
- 初始列表的id
- 做了运算之后,l 还是其本身,只是在最后面添加上了新的元素
- 初始元组的id
- 做了运算之后,元组被改变
重复进行不可变序列的连结是效率很低的,因为除了将需要的元素添加到目标元素的后面之外,解释器还得把得到的整个序列复制到新的内存中。
一个有趣的 +=
赋值难题
# 例子2-14,一个谜题
t = (1, 2, [30, 40])
t[2] += [50, 60]复制代码
会发生什么呢?t 是一个不可变的元组,是不能做修改的,而 t[2] 又是一个可变的列表。结果可能会让人吃惊
# 例子2-15,例子2-14的输出
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'tuple' object does not support item assignment
print t # (1, 2, [30, 40, 50, 60])复制代码
也就是说 Python 一边报错,一边输出了正确的结果。(傲娇的 Python )
这里安利一下一个网站 Online Python Tutor ,在这里可以看出程序运行期间具体都发生了什么,有兴趣就自己去看运行时吧。具体不介绍,但是谁用谁知道。
小建议:
值得指出的是,使用
t[2].extend([50, 60])
就不会出错。这里只是指出+=
的怪异行为。
总结上面的例子:
- 把可变元素放在元组之中不是什么好主意
- 增广赋值操作并不是原子操作
list.sort 和内置的 sorted 函数
list.sort
函数会对 list 进行原地排序,函数的返回值为 None
来告知我们 list 本身已经被修改了,同时也不会产生新的 list 。这是 Python Api 的一个重要惯例:函数或者方法如果原地修改了一个对象,应该返回一个 None ,来让人清楚的知道对象本身被改变了,以及没有新的对象被创建。
与之形成对比的,Python 的内置函数 sorted
创建了一个新的 list 并将其返回。sorted
函数接受任何可遍历对象作为输入参数,包括不可变序列和生成器。无论输入参数是什么类型,sorted
函数总会创建一个新的 list ,并将其返回。
list.sort
和 sorted
函数接收两个可选的,只接受关键词的参数
- reverse 如果为 True ,元素会以逆序的形式返回,默认为 False
- key 决定每个元素被排序时的排序依据。例如
key=str.lower
会进行大小写无关的排序,key=len
会把字符串按照长度排序。
具体的例子省略,但是没有任何难度。
通过 bisect
函数来操作排序的序列
bisect(haystack, needle)
会对 haystack 做二分查找来搜寻 needle,这自然必须要求 haystack 是一个排序后的序列。
用 bisect
来搜索
# 例子2-17,bisect 找到元素的插入位置
import bisect
import sys
HAYSTACK = [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:2} @ {1:2} {2}{0:<2}'
def demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle) # 注释1
offset = position * ' |' # 注释2
print ROW_FMT.format(needle, position, offset) # 注释3
if __name__ == '__main__':
if sys.argv[-1] == 'left': # 注释4
bisect_fn = bisect.bisect_left
else:
bisect_fn = bisect.bisect
print('DEMO:', bisect_fn.__name__) # 注释5
print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)复制代码
- 使用
bisect
函数来获得插入点 - 根据偏移量来画分割线
- 格式化输出
- 通过命令行参数来选择对应的
bisect
函数 - 在表头打印函数名称
('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复制代码
这张图展示了 bisect
函数的具体工作过程。和函数 bisect_left
的比较就是,当遇到相同元素时,bisect
(也就是 bisect_right
),把元素插入了右边,而 bisect_left
插到了左边。例子2-18是一个简单应用。
# 例子2-18,给定分数,输入对应的成绩分区
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
i = bisect.bisect(breakpoints, score)
return grades[i]
print [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]复制代码
用 bisect.insort
来插入
对序列排序是很耗时的操作,所以一旦一个序列已经排好序,我们希望后续的操作依旧可以保持排序状态。
# 例子2-19,Insort函数保持排序状态
import bisect
import random
SIZE = 7
random.seed(1729)
my_list = []
for i in range(SIZE):
new_item = random.randrange(SIZE*2)
bisect.insort(my_list, new_item)
print('%2d ->' % new_item, my_list)复制代码
输出结果为:
('13 ->', [13])
('12 ->', [12, 13])
(' 5 ->', [5, 12, 13])
(' 6 ->', [5, 6, 12, 13])
(' 9 ->', [5, 6, 9, 12, 13])
(' 2 ->', [2, 5, 6, 9, 12, 13])
(' 4 ->', [2, 4, 5, 6, 9, 12, 13])复制代码
也许有时 List 不是最好的选择
list 是那么的好用,以至于我们选择使用 list 几乎不需要做多余的思考。但有时,例如我们需要存储一个一千万的浮点数列表,使用 array 是更高效的选择,因为 array 并不是把 float 对象完全的存贮下来,而只是存下来了打包的字节。如果我们需要不断的添加或者删除元素,这是 deque
是更好的选择。如果你需要做大量的 in
操作,那么 set
是更好的选择。
Arrays
如果序列中只有数, array.array
是一个更高效的选择。和 list 一样,它也支持所有的可变序列的操作,包括 pop, insert, extend。它还支持快速加载和保存的 frombytes
和 tofile
方法。
和 C 语言中的 array 一样,创建 array 需要指明存储类型。array('b')
表明每个元素都是一个字节,被解释为从 -128 到 127 的整数。对于体积很大的序列来说,这个很节省空间。同时 array 会检查你的输入,不会允许你把类型不对的元素放进去。
# 例子2-20,创建,保存,加载一个大的数组
from array import array # 注释1
from random import random
floats = array('d', (random() for i in range(10**7))) # 注释2
print floats[-1] # 注释3
fp = open('floats.bin', 'wb')
floats.tofile(fp) # 注释4
fp.close()
floats2 = array('d') # 注释5
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7) # 注释6
fp.close()
print floats2[-1] # 注释7
print floats2 == floats # 注释8复制代码
- 创建一个数组类型
- 从一个可遍历对象中,创建一个双精度浮点数的数组(关键字为 d)。此处的可遍历对象为生成器
- 查看一下最后一个元素
- 把 array 存储为二进制文件
- 创建一个双精度浮点数的空数组
- 从二进制文件中读取一千万个浮点数
- 检查数组中的最后一个数
- 比较两个数组
重点是,这些操作相当迅速。我自己曾经试着用 list 来操作数据,写文件然后读文件,相当慢。对于数组来说 array.fromfile
只需要 0.1 秒的时间从二进制文件中来加载一千万个双精度浮点数的数组,这个几乎比从文本文件中读取要快 60 倍。同样的,array.tofile
也比一个个往每一行中写浮点数要快差不多 7 倍。更重要的是,存储一千万个浮点数的二进制文件只有差不多 80 M,而写文本文件则差不多要 180 M。
小建议:
在 Python 3.4 中,array 并没有像
list.sort()
那样的原地排序操作,如果需要的话,可以进行如下操作
a = array.array('d', sorted(a))复制代码
源码的github地址
源代码
以上源代码均来自于 fluent python 一书,仅为方便阅读之用。如侵删。