fluent python 读书笔记 2--Python的序列类型2

对 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'], ['_', '_', '_']]复制代码
  1. 创建一个包含3个元素的列表,每个元素包含三个元素。注意查看其结构
  2. 在第一行第二列出放一个标记,查看结果

对比看来,例子2-13是一个错误的例子

# 例子2-13,同样的例子,但是是错的
weird_board = [['_'] * 3] * 3 # 注释1
print weird_board
weird_board[1][2] = '0' # 注释2
print weird_board复制代码
  1. 最外层列表有三个指向相同引用的列表组成。不改变的时候,看起来没毛病
  2. 在第一行第二列放置一个标记,可以发现每一行都指向了相同的元素

例子2-13其实相当于

row = ['_'] * 3
board = []
for i in range(3):
    board.append(row) # 注释1复制代码
  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', '_', '_']]复制代码
  1. 每次迭代都新建立了一个列表,并添加到了 board 后面
  2. 只有对应的第二列改变了,如我们所期待的那样

增广的序列赋值

+=*= 可以和正常的运算非常不同。此处只讨论 += ,其思想概念完全适用于 *= 。让其工作的特殊函数为 __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复制代码
  1. 初始列表的id
  2. 做了运算之后,l 还是其本身,只是在最后面添加上了新的元素
  3. 初始元组的id
  4. 做了运算之后,元组被改变

重复进行不可变序列的连结是效率很低的,因为除了将需要的元素添加到目标元素的后面之外,解释器还得把得到的整个序列复制到新的内存中。

一个有趣的 += 赋值难题
# 例子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]) 就不会出错。这里只是指出 += 的怪异行为。

总结上面的例子:

  1. 把可变元素放在元组之中不是什么好主意
  2. 增广赋值操作并不是原子操作

list.sort 和内置的 sorted 函数

list.sort 函数会对 list 进行原地排序,函数的返回值为 None 来告知我们 list 本身已经被修改了,同时也不会产生新的 list 。这是 Python Api 的一个重要惯例:函数或者方法如果原地修改了一个对象,应该返回一个 None ,来让人清楚的知道对象本身被改变了,以及没有新的对象被创建。

与之形成对比的,Python 的内置函数 sorted 创建了一个新的 list 并将其返回。sorted 函数接受任何可遍历对象作为输入参数,包括不可变序列和生成器。无论输入参数是什么类型,sorted 函数总会创建一个新的 list ,并将其返回。

list.sortsorted 函数接收两个可选的,只接受关键词的参数

  • 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)复制代码
  1. 使用 bisect 函数来获得插入点
  2. 根据偏移量来画分割线
  3. 格式化输出
  4. 通过命令行参数来选择对应的 bisect 函数
  5. 在表头打印函数名称
('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。它还支持快速加载和保存的 frombytestofile 方法。

和 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复制代码
  1. 创建一个数组类型
  2. 从一个可遍历对象中,创建一个双精度浮点数的数组(关键字为 d)。此处的可遍历对象为生成器
  3. 查看一下最后一个元素
  4. 把 array 存储为二进制文件
  5. 创建一个双精度浮点数的空数组
  6. 从二进制文件中读取一千万个浮点数
  7. 检查数组中的最后一个数
  8. 比较两个数组

重点是,这些操作相当迅速。我自己曾经试着用 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 一书,仅为方便阅读之用。如侵删。

转载于:https://juejin.im/post/58ea3cac570c350057d8d878

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值