为了计算 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]:
In [9]:
l[2:5] = 100 #当赋值对象是切片时候,即使只有一个元素,等式右面也必须是一个可迭代元素
In [10]:
l[2:5] = [100]
l
Out[10]:
In [1]:
l = [1, 2, 3]
l * 5
Out[1]:
In [13]:
#建立一个有三个元素的列表,每个元素是一个列表,有三个项目
board = [[' '] * 3 for i in range(3)]
board
board[1][2] = 'X'
board
Out[13]:
上面很吸引人,并且是一种标准的做法,不过要注意,如果你在 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]:
上面的程序本质上相当于:
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]:
+= 和 = 的行为会根据第一个运算元不同而有很大的不同。为了简化讨论,我们主要讨论 +=,但是它的概念也可以套用到 =(乘等于,显示有问题) 上。让 += 可以工作的是 __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]:
In [2]:
l *= 2
l
Out[2]:
In [3]:
id(l)
Out[3]:
In [4]:
t = (1, 2, 3)
id(t)
Out[4]:
In [5]:
t *= 2
id(t)
Out[5]:
In [6]:
t = (1, 2, [30, 40])
t[2] += [50, 60]
In [7]:
t
Out[7]:
看到虽然报错了,但是 t 的值还是被改变了。我们看一下反汇编代码
In [12]:
import dis
t = (1, 2, [3, 4])
dis.dis('a[2] += [5, 6]')
前面是初始化变量,看 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 不可变
list.sort 会将原来的 list 排序,不会产生新的 list 副本,并返回一个 None 值告诉我们,已经改变了目标列表,没有创建一个新的列表,这是一种很重要的 Python API 惯例,当函数或方法改变目标时,返回一个 None 值。在 random.shuffle() 函数中也可以看到相同的用法。
相比之下,内建函数 sorted 会建立一个新的 list,并将它返回,事实上,它会接收所有可迭代对象,包括生成器,无论要给它排序的可迭代对象是什么,都会返回一个新的列表,不管 list.sort() 还是 sorted() 都有两个参数
Note: key 关键字也可以在 min() 和 max() 函数中使用,另外有些标准程序库中也支持这个参数(例如 itertools.groupby() 和 heapq.nlargest() 等)。
下面例子可以让我们了解排序函数的用法:
In [6]:
fruits = ['grape', 'raspberry', 'apple', 'banana']
sorted(fruits)
Out[6]:
In [7]:
fruits
Out[7]:
In [8]:
sorted(fruits, reverse=True)
Out[8]:
In [9]:
sorted(fruits, key=len)
Out[9]:
In [10]:
sorted(fruits, key=len, reverse=True)
Out[10]:
In [11]:
fruits
Out[11]:
In [12]:
fruits.sort()
fruits
Out[12]:
当我们将列表排序后,就可以非常有效率的搜索它们,幸运的是,Python 标准库中的 bisect 模块已经提供标准的二分搜索法了。我们来讨论一下它的基本功能,包括方便的 bisect.insort() 函数,我们可以用它来确保排序后的列表仍然保持已经排序的状态
bisect 主要提供两个函数 bisect() 和 insoct(),它们使用二分搜索,可以在任何有序列表中快速的查找和插入元素
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()
In [41]:
main('left')
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]:
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)
和 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]:
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]:
In [51]:
floats == floats2
Out[51]:
看到 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]:
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]:
In [74]:
memv[0]
Out[74]:
In [75]:
memv_oct = memv.cast('B') #将 memv 转成无符号字节
memv_oct.tolist()
Out[75]:
In [76]:
memv_oct[5] = 4 #小端模式,所以 memv_oct[5] 代表 0 的高位
numbers
Out[76]:
In [78]:
import numpy as np
a = np.arange(12)
a
Out[78]:
In [79]:
type(a)
Out[79]:
In [80]:
a.shape
Out[80]:
In [81]:
a.shape = 3, 4
In [82]:
a
Out[82]:
In [83]:
a[:1]
Out[83]:
In [84]:
a.T
Out[84]:
In [95]:
floats = np.array([random() for i in range(10 ** 7)])
floats.shape
Out[95]:
In [96]:
floats[-3:]
Out[96]:
In [97]:
floats *= .5
floats[-3:]
Out[97]:
In [99]:
from time import perf_counter as pc #引入高效计时器(Python3.3 开始提供)
t0 = pc(); floats /= 3; pc() - t0 #看到除以 3 这个操作只用了 20 毫秒的时间
Out[99]:
In [2]:
from collections import deque
dq = deque(range(10), maxlen=10)
dq
Out[2]:
In [3]:
dq.rotate(3) #使用 n > 0 来旋转,从右面取出元素,放到最左面,n < 0 是从左面取出元素放到最右面
dq
Out[3]:
In [4]:
dq.rotate(-4)
dq
Out[4]:
In [5]:
dq.appendleft(-1) #最左面插入
dq
Out[5]:
In [6]:
dq.extend([11, 22, 33]) #在最右面加入 3 个元素,最左面的 3 个元素将被丢弃
dq
Out[6]:
In [7]:
dq.extendleft([10, 20, 30, 40])
dq #注意 dq.extendleft(iter) 工作方式,它会将迭代器的个元素逐个加到队列左边,所以这些元素位置最后是反的
Out[7]:
双向队列实现了大部分列表所拥有的方法,也加了一些额外符合自身设计的方法,例如 popleft 和 rotate,但是为了实现这些方法,双向队列也付出了一些代价,从队列中间删除元素操作会慢一些,因为它只对头尾操作进行了优化。
其它的队列: