切片

为了计算 seq[start:stop:step],Python 会调用 seq.__getitem__(slice(start, stop, step))

多维切片

[ ] 运算符也可以接收以逗号分隔的多个索引或切片,举例来说,Numpy 中,你可以使用 a[i, j] 取得二维的 numpy.ndarray,以及使用 a[m:n, k:l] 这类的运算符获取二维的切片。处理 [ ] 运算符的 __getitem____setitem__ 特殊方法,都只会接收 tuple 格式的 a[i, j] 内的索引,换句话说,Python 调用 a.getitem((i, j)) 算出来的 a[i, j]

Python 会将省略号(三个句点)视为一种标记,在 Numpy 对多维矩阵进行切片时,会使用快捷的方式 ...,例如一个四维矩阵 a[i, ...] 相当于 a[i, :, :, :] 的简写。

对切片赋值


In [8]:
l = list(range(10))
l


Out[8]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [9]:
l[2:5] = 100 #当赋值对象是切片时候,即使只有一个元素,等式右面也必须是一个可迭代元素


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-d02ac7e234f9> in <module>()
----> 1 l[2:5] = 100 #当赋值对象是切片时候,即使只有一个元素,等式右面也必须是一个可迭代元素

TypeError: can only assign an iterable

In [10]:
l[2:5] = [100]
l


Out[10]:
[0, 1, 100, 5, 6, 7, 8, 9]

对列表使用 + 与 *

要连接多个同一列表副本,只需要将列表乘上一个整数


In [1]:
l = [1, 2, 3]
l * 5


Out[1]:
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]

用 * 构建内含多个列表的列表

如果我们想初始化列表中有一定数量的列表,最适合使用列表生成式,例如下面就可以表示井字的棋盘列表,里面有 3 个长度为 3 的列表


In [13]:
#建立一个有三个元素的列表,每个元素是一个列表,有三个项目
board = [[' '] * 3 for i in range(3)]
board
board[1][2] = 'X'
board


Out[13]:
[[' ', ' ', ' '], [' ', ' ', 'X'], [' ', ' ', ' ']]

上面很吸引人,并且是一种标准的做法,不过要注意,如果你在 an 语句中,如果 a 元素是对其它可变对象的引用的话,就要注意了,因为这个式子可能出乎你的意料,例如,你想用 my_list = [[]] 3 初始化一个由列表组成的列表,但是我们其实得到的列表中包含的三个元素是 3 个引用,这 3 个引用指向同一个列表。

下面也是创建三个元素的列表,每个元素是一个列表,有三个项目,这是一种看起来很简洁,但是是错误的做法


In [2]:
weir_board = [['_']* 3] * 3 #这是 3 个指向同一个地址的列表
weir_board


Out[2]:
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]

In [3]:
weir_board[1][2] = 'O'
weir_board


Out[3]:
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

上面的程序本质上相当于:


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


Out[4]:
[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

In [5]:
board[1][2] = '0'
board


Out[5]:
[[' ', ' ', '0'], [' ', ' ', '0'], [' ', ' ', '0']]

列表加法

+= 和 = 的行为会根据第一个运算元不同而有很大的不同。为了简化讨论,我们主要讨论 +=,但是它的概念也可以套用到 =(乘等于,显示有问题) 上。让 += 可以工作的是 __iadd__(代表 ”in-place addition“ 就地加法)。但是,如果没有实现 __iadd__ 方法的话,就会去调用 __add__ 方法,运算式 a += b 就和 a = a + b 效果一样,也就是 先算出 a + b,产生一个新的变量,将新的变量赋值给 a,换句话说,赋值给 a 的可能是一个新的内存地址的变量,变量名会不会被关联到新的对象完全取决于是否有 __iadd__ 决定。

一般来说,可变序列都实现了 __iadd__ 方法,因此 += 是就地加法,而不可变序列根本不支持这个操作,对这个方法的实现也就无从谈起。

这种 += 的概念也可以应用到 *=,它是用 __imul__重写的,关于这两个方法,第 13 章会谈到。

下面是一个例子,展现 乘等于在可变和不可变序列上的作用:


In [1]:
l = [1, 2, 3]
id(l)


Out[1]:
140464508914312

In [2]:
l *= 2
l


Out[2]:
[1, 2, 3, 1, 2, 3]

In [3]:
id(l)


Out[3]:
140464508914312

In [4]:
t = (1, 2, 3)
id(t)


Out[4]:
140464499919176

In [5]:
t *= 2
id(t)


Out[5]:
140464508777672

Note: 对不可变序列拼接效率很低,因为它要整个的把原来的内容复制到新的内存,然后拼接。

一个好玩的 += 例子


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


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-5198460d9273> in <module>()
      1 t = (1, 2, [30, 40])
----> 2 t[2] += [50, 60]

TypeError: 'tuple' object does not support item assignment

In [7]:
t


Out[7]:
(1, 2, [30, 40, 50, 60])

看到虽然报错了,但是 t 的值还是被改变了。我们看一下反汇编代码


In [12]:
import dis
t = (1, 2, [3, 4])
dis.dis('a[2] += [5, 6]')


  1           0 LOAD_NAME                0 (a)
              3 LOAD_CONST               0 (2)
              6 DUP_TOP_TWO
              7 BINARY_SUBSCR
              8 LOAD_CONST               1 (5)
             11 LOAD_CONST               2 (6)
             14 BUILD_LIST               2
             17 INPLACE_ADD
             18 ROT_THREE
             19 STORE_SUBSCR
             20 LOAD_CONST               3 (None)
             23 RETURN_VALUE

前面是初始化变量,看 17 开头的 INPLACE_ADD,执行 += 操作,这步成功了,然后 19 STORE_SUBSCR 执行 t[2] = [3, 4, 5, 6],但是由于 tuple 不可变,这步失败了,但是由于列表 执行的 += 操作是调用 __iadd__,不会改变内存地址,所以 list 内容已经改变了。

所以上面的相当于:


In [14]:
x = [3, 4]
x.__iadd__([5, 6])
t[2] = x  #这步报错,tutle 不可变


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-a863a382f716> in <module>()
      1 x = [3, 4]
      2 x.__iadd__([5, 6])
----> 3 t[2] = x  #这步报错,tutle 不可变

TypeError: 'tuple' object does not support item assignment

list.sort 与 sorted() 内部函数

list.sort 会将原来的 list 排序,不会产生新的 list 副本,并返回一个 None 值告诉我们,已经改变了目标列表,没有创建一个新的列表,这是一种很重要的 Python API 惯例,当函数或方法改变目标时,返回一个 None 值。在 random.shuffle() 函数中也可以看到相同的用法。

相比之下,内建函数 sorted 会建立一个新的 list,并将它返回,事实上,它会接收所有可迭代对象,包括生成器,无论要给它排序的可迭代对象是什么,都会返回一个新的列表,不管 list.sort() 还是 sorted() 都有两个参数

  • reverse 参数: 如果值是 True,会降序返回,默认是 False
  • key 参数: 一个函数,返回每一个元素的排序键。例如你要排序几个字符串,key = str.lower 可以执行不分大小写的排序,key = len 可以按照长度排序,默认值是恒等函数,也就是默认用元素自己的值排序

Note: key 关键字也可以在 min() 和 max() 函数中使用,另外有些标准程序库中也支持这个参数(例如 itertools.groupby() 和 heapq.nlargest() 等)。

下面例子可以让我们了解排序函数的用法:


In [6]:
fruits = ['grape', 'raspberry', 'apple', 'banana']
sorted(fruits)


Out[6]:
['apple', 'banana', 'grape', 'raspberry']

In [7]:
fruits


Out[7]:
['grape', 'raspberry', 'apple', 'banana']

In [8]:
sorted(fruits, reverse=True)


Out[8]:
['raspberry', 'grape', 'banana', 'apple']

In [9]:
sorted(fruits, key=len)


Out[9]:
['grape', 'apple', 'banana', 'raspberry']

In [10]:
sorted(fruits, key=len, reverse=True)


Out[10]:
['raspberry', 'banana', 'grape', 'apple']

In [11]:
fruits


Out[11]:
['grape', 'raspberry', 'apple', 'banana']

In [12]:
fruits.sort()
fruits


Out[12]:
['apple', 'banana', 'grape', 'raspberry']

当我们将列表排序后,就可以非常有效率的搜索它们,幸运的是,Python 标准库中的 bisect 模块已经提供标准的二分搜索法了。我们来讨论一下它的基本功能,包括方便的 bisect.insort() 函数,我们可以用它来确保排序后的列表仍然保持已经排序的状态

使用 bisect 来管理有效列表

bisect 主要提供两个函数 bisect() 和 insoct(),它们使用二分搜索,可以在任何有序列表中快速的查找和插入元素

使用 bisect 来搜索

bisect(haystack, needle) 会对 haystack(干草垛) 中(必须是已排序的列表)的 needle(针) 做二分搜索,找出可以插入 needle 的位置的索引,并保持 haystack 的顺序,也就是该位置左面的数都小于等于 needle,你可以使用 bisect(haystack, needle) 的结果来作为 haystack.insert(index, needle) 的参数,但是 insort() 可以做这两步,速度更快


In [1]:
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: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))

def main(args=None):
    if args == 'left':
        bisect_fn = bisect.bisect_left
    else:
        bisect_fn = bisect.bisect
    
    print('DEMO:', bisect_fn.__name__)
    print('haystack ->', ' '.join('%2d' % n for n in HAYSTACK))
    demo(bisect_fn)

In [40]:
main()


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 

In [41]:
main('left')


DEMO: bisect_left
haystack ->  1  4  5  6  8 12 15 20 21 23 23 26 29 30
31 @ 14      |  |  |  |  |  |  |  |  |  |  |  |  |  |31
30 @ 13      |  |  |  |  |  |  |  |  |  |  |  |  |30
29 @ 12      |  |  |  |  |  |  |  |  |  |  |  |29
23 @  9      |  |  |  |  |  |  |  |  |23
22 @  9      |  |  |  |  |  |  |  |  |22
10 @  5      |  |  |  |  |10
 8 @  4      |  |  |  |8 
 5 @  2      |  |5 
 2 @  1      |2 
 1 @  0    1 
 0 @  0    0 

bisect 函数有两种微调方式,再插入时,可以使用一对索引,lo 和 hi 来缩小索引范围,lo 的默认值是 0, hi 的默认值是列表的 len()

其次,bisect() 其实是 bisect_right() 的别名,它还有一种姐妹函数,叫做 bisect_left(),从上面看出,当列表和插入元素不相同时,看不出来差别,但是如果有相同元素时,bisect() 会在相同的最一个元素右面插入,bisect_left() 会在相同的第一个元素左面插入。

bisect 有一个有趣的用法,就是执行数值表格查询,例如,将考试的分数转成字母:


In [44]:
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'):
    i = bisect.bisect(breakpoints, score)
    return grades[i]
[grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]


Out[44]:
['F', 'A', 'C', 'C', 'B', 'A', 'A']

这段程序出自 bisect 模块文件,在搜索冗长的数字列表时,用 bisect 来取代 index 方法,可以加快查询速度

使用 bisect.insort 来插入

insort(seq, item) 会将 item 插入 seq,让 seq 保持升序排列


In [13]:
import bisect   
import random

SIZE = 7

random.seed(1729)
# 哈哈,python 版本的插入排序,很方便
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)


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]

和 bisect 一样,inosrt 可以使用 lo,hi 参数来搜索子列表。另外还有一个 insort_left 函数,使用 bisect_left 查找插入点


当列表不适用时

list 类型很好,但有时根据特定需求会有更好的选择,例如,如果你要存储一千万浮点数,那么 数组(array)会比较有效率,因为数组存的不是浮点数对象,而是像 C 语言一样保存它们的机器值。另一方面,如果你经常在列表尾端加入以及移除元素,将它当成栈或队列使用, deque(double-ended queue 双端队列) 的工作速度会较快

数组

如果列表中只存放数字,那么 array.array 会比列表更有效率,它支持所有的可变列表操作(包括 .pop, .insert 和 .extend)以及额外的方法,可以快速将内容存储到硬盘,例如 .frombytes 和 .tofile

Python 数组与 C 中的数组一样精简,当建立数组时,需要提供一个 类型码(typecode)来指定在底层存储时使用的 C 语言类型,例如,b 是 signed char 的 类型码,如果建立一个 array('b'),那么每一个元素都会被存成一个 byte,并被解释成整数,范围是 -128 到 127。对于大型数字列表来说,节省了很多内存。且 Python 不允许任何不符合数组类型的数字放进去。

下面展示了创建 1000 万浮点数数组,如何存到文件中并读取到数组中。


In [49]:
from array import array
from random import random
floats = array('d', (random() for i in range(10 ** 7))) #双精度浮点数
floats[-1]


Out[49]:
0.5963321947530882

In [53]:
fp = open('ipynb_floats.bin', 'wb')
floats.tofile(fp)
fp.close()
floats2 = array('d')  
fp = open('ipynb_floats.bin', 'rb')
floats2.fromfile(fp, 10 ** 7)
fp.close()
floats2[-1]


Out[53]:
0.5963321947530882

In [51]:
floats == floats2


Out[51]:
True

看到 array.tofile() 和 array.fromfile() 都很简单,执行速度也很快,事实证明,array.fromfile 载入这些数据只花了 0.1 秒,大约比从文本文件中读取数据快了 60 倍

Note: 另一种快速且更方便的数值存储方式是 pickle,pickle.dump 存储浮点数数组和 array.tofile() 几乎一样的快,但是 pickle 几乎可以处理所有的内置类型,包括复数等,甚至可以处理自己定义的实例(如果不是太复杂)

对于一些特殊的数字数组,用来表示二进制图像,例如光栅图像,里面涉及到的 bytes 和 bytearry 类型会在第 4 章讲到。

Note: 在 python3.5 为止,array 都没有像 list.sort() 这样的排序方法,如果排序可以使用 sorted() 函数排序建立一个新数组 a = array.array(a.typecode, sorted(a))


In [56]:
floats = array(floats.typecode, sorted(floats))
floats[0:5]


Out[56]:
array('d', [1.3204618898310372e-07, 1.438214560778306e-07, 2.6395568086812204e-07, 3.0406143480821157e-07, 3.2941991068291543e-07])

内存视图(Memoryview)

memoryview 是一个内置类,可以让你在不复制内容的情况下下操作同一个数组的不同切片,本质上,memoryview 是一个泛化和去数学化的 Numpy 数组,在不需要复制内存情况下,可以再数据库结构之间共享内存,其中数据可以任何格式,例如 PIL 图像,SQLlite 数据库,Numpy 数组等,这个功能对处理大型数据集合时候非常重要。

memoryview.cast 使用类似数组模块的标记法,可以让你使用不同的方式读取同一块内存数据,而且内容字节不会随意移动。memoryview.cast 会将同一块内从打包成一个全新 memoryview 对象返回,听起来像 C 语言中的类型转换。

更改数组中的一个 bytes,来改变某个元素的值:


In [73]:
numbers = array('h', [-2, -1, 0, 1, 2]) # h 代表 short 类型
memv = memoryview(numbers)
len(memv)


Out[73]:
5

In [74]:
memv[0]


Out[74]:
-2

In [75]:
memv_oct = memv.cast('B') #将 memv 转成无符号字节
memv_oct.tolist()


Out[75]:
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]

In [76]:
memv_oct[5] = 4 #小端模式,所以 memv_oct[5] 代表 0 的高位
numbers


Out[76]:
array('h', [-2, -1, 1024, 1, 2])

看到 memoryview 可以将数据用另一种类型读写,还是很方便的,在第 4 章会看到一个 memoryview 和 struct 操作二进制序列的例子

这时候,我们自然想到了如何处理数组中的数据,答案是使用 Numpy 和 Scipy

Numpy 和 Scipy

Numpy 和 Scipy 实现了数组和矩阵运算,使得 Python 成为了应用科学计算的主流。

Scipy 是一种基于 Numpy 的包,提供了很多科学计算方法,包括线性代数,数值计算,统计等,Scipy 即快速又可靠,是一个很好的科学计算包


In [78]:
import numpy as np
a = np.arange(12)
a


Out[78]:
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

In [79]:
type(a)


Out[79]:
numpy.ndarray

In [80]:
a.shape


Out[80]:
(12,)

In [81]:
a.shape = 3, 4

In [82]:
a


Out[82]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

In [83]:
a[:1]


Out[83]:
array([[0, 1, 2, 3]])

In [84]:
a.T


Out[84]:
array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])

In [95]:
floats = np.array([random() for i in range(10 ** 7)])
floats.shape


Out[95]:
(10000000,)

In [96]:
floats[-3:]


Out[96]:
array([ 0.24123447,  0.8827802 ,  0.62739882])

In [97]:
floats *= .5
floats[-3:]


Out[97]:
array([ 0.12061723,  0.4413901 ,  0.31369941])

In [99]:
from time import perf_counter as pc #引入高效计时器(Python3.3 开始提供)
t0 = pc(); floats /= 3; pc() - t0 #看到除以 3 这个操作只用了 20 毫秒的时间


Out[99]:
0.019770045000768732

双向队列(Deque) 和其它的队列

.append() 和 .pop() 方法可以让你的列表当队列使用,每次进行 append() 和 pop(0),就可以产生 LIFO,但是如果你在最左端插入和移除元素,就很耗费资源,因为整个列表都要移位。

collections.deque(双向队列) 是一个线程安全的双端队列,可以快速的在两端进行插入和移除,如果你需要一个 “只保留最后看到的几个元素” 的功能,这也是一个最佳选择,因为 deque 可以设置队列的大小,当它被填满,加入新元素时,它会丢弃另一端的元素,下面是几个双向队列的典型操作


In [2]:
from collections import deque
dq = deque(range(10), maxlen=10)
dq


Out[2]:
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [3]:
dq.rotate(3) #使用 n > 0 来旋转,从右面取出元素,放到最左面,n < 0 是从左面取出元素放到最右面
dq


Out[3]:
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6])

In [4]:
dq.rotate(-4)
dq


Out[4]:
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])

In [5]:
dq.appendleft(-1) #最左面插入
dq


Out[5]:
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [6]:
dq.extend([11, 22, 33]) #在最右面加入 3 个元素,最左面的 3 个元素将被丢弃
dq


Out[6]:
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33])

In [7]:
dq.extendleft([10, 20, 30, 40])
dq #注意 dq.extendleft(iter) 工作方式,它会将迭代器的个元素逐个加到队列左边,所以这些元素位置最后是反的


Out[7]:
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8])

双向队列实现了大部分列表所拥有的方法,也加了一些额外符合自身设计的方法,例如 popleft 和 rotate,但是为了实现这些方法,双向队列也付出了一些代价,从队列中间删除元素操作会慢一些,因为它只对头尾操作进行了优化。

其它的队列:

  • queue: 也是一个安全的队列,当队列满了的时候,会等待当有元素被移除后再添加新的元素,很适合用来限制线程的数量
  • multiprocessing: 类似 queue.Queue,但是设计的目的是为了程序间通信
  • asyncio: python3.4 后的新功能,适用于异步编程任务
  • heapq: 并没有队列类,但是提供 heappush 和 heappop 方法,用户可以把可变序列当做队列