4-22日读书笔记——第二章(2)
2.42.42.4 切片
这个在上一章也提到了,切片操作可能很方便地从一个对象中按顺序的取用元素。
在对seq[start,stop,step]进行求值的同时,python会调用seq.__ getitem__(slice(start,stop,step))的方法。而对于3个参数我们也可以进行省略。python中,无论是切片还是区间,都会忽略最后一个元素,由于是以0为起始下标。
这样操作的优点就在于,我们在取用时,只用专注于我们所要取的信息,比如我们想要取3个元素,就可以用[:3]或者range(3)的方式。同时,该区间或切片的长度很明显就可以用stop-start的值。而且[:3]与[3:]的所取的区间正好是不重叠的两部分。
切片的第三个参数还可以定义取值的方式是正向还是反向。
切片还有两个功能:多维切片与省略。
python中有一个numpy的外部库,在做数据分析的时候用的比较多。numpy.ndarray就可以使用切片的方式来获取多维度中的元素。
实例如下:
>>> a = np.array([[1,2], [3,4]])
>>> a
array([[1, 2],
[3, 4]])
>>> a[0,1]
2
>>> a = np.array([[1,2,3,4], [5,6,7,8]])
>>> a[0:4, 1:2]
array([[2],
[6]])
>>> a[0:4, 0:2]
array([[1, 2],
[5, 6]])
注意python的内置的类型都是一维的,所以它们并不支持这种多索引的情况。
至于省略,其实这是一个很有意思的对象叫做ellipsis,它的表现形式就是3个点。有点类似与True/False,是ellipsis的单一实例。
例子如下:
>>> a = np.array([[1,2,3,4], [5,6,7,8]])
>>> a[...]
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
>>> class Test(object):
... def __getitem__(self, key):
... print(key)
...
>>> a = Test()
>>> a[1]
1
>>> a[1,2,...]
(1, 2, Ellipsis)
>>> a[1, 2:3, :4:, ::]
(1, slice(2, 3, None), slice(None, 4, None), slice(None, None, None))
在切片中,如果x是一个四维数组,x[a,…]就是x[a,:,:,:]的缩写。
使用切片,让赋值也变得很简单。可以对序列进行切除,修改等操作。
实例如下:
>>> l = list(range(6))
>>> l
[0, 1, 2, 3, 4, 5]
>>> l[2:5] = [22,33]
>>> l
[0, 1, 22, 33, 5]
>>> del l[:1]
>>> l
[1, 22, 33, 5]
对序列使用*或+操作
如果想要将一个序列中的元素复制,就可以使用* 快捷方式。
实例如下:
>>> x *3
[1, 2, 1, 2, 1, 2]
>>> x = [1,2]
>>> xx = [3,6]
>>> x + xx
[1, 2, 3, 6]
这些操作不会改变原先的x,而是又构建了一个新的序列。
用* 初始化一个由列表组成的列表
可能有时候我们会需要初始化一个多个列表嵌套的列表,那么最好使用列表推导的方式。
通过以下的例子就可以看出。
>>> x = [['1'] * 3 for i in range(3)]
>>> x
[['1', '1', '1'], ['1', '1', '1'], ['1', '1', '1']]
>>> x[1][2] = '0'
>>> x
[['1', '1', '1'], ['1', '1', '0'], ['1', '1', '1']]
>>> y = [['1'] * 3] * 3
>>> y
[['1', '1', '1'], ['1', '1', '1'], ['1', '1', '1']]
>>> y[1][2] = '0'
>>> y
[['1', '1', '0'], ['1', '1', '0'], ['1', '1', '0']]
这是因为用*操作的时候,3个指向的是同一个列表的引用。所以我们改变了其中一个的值,3个同时都改变了。
增量运算符
增量赋值运算符+=,*=的表现取决于它们的第一个操作对象。 +=其实是运用了__iadd__方法
>>> a = range(3)
>>> b = a
>>> b += [3]
>>> a
[0, 1, 2, 3]
>>> b
[0, 1, 2, 3]
b += [3]相当于 b.__ iadd__([3]),在原对象的基础上进行更新,返回值为None。
如果没有__ iadd__()方法则会调用__add__()方法。a+=b就相当于a=a+b一样,将a和b的和赋值给a。
*=也是一样,只不过调用的是__imul__()方法。
有个很有趣的小例子:
>>> t = (1, 2, [30, 40])
>>> t[2] += [40, 50]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t[2]
[30, 40, 40, 50]
>>> t
(1, 2, [30, 40, 40, 50])
可以看到,当t[2] += [40,50]时,第三个元素[30,40]改变成了[30,40,40,50]。而将这个值再赋值给元组时报了错,由于它不是一个原子操作,所以t的值改变了,程序却报错了。
2.72.72.7 list.sort方法和内置函数sorted
list类型中的sort方法,使用时就该列表进行排序,而不会新建一个list,所以返回值为None,表示原参数发生了变动,并没有新对象产生。
而python中的内置函数sorted就正好相反,他会返回一个新的列表(无论传进来的参数是怎样的)
这两个方法都有两个可选的关键字参数。
reverse和key
第一个关键字参数,可想而知,是反向降序的意思,默认值为false。
第二个关键字可以将它理解成排序的方式,默认用元素本身的值来排序。
实例如下:
>>> name = ['zhang', 'li', 'zhao', 'sun']
>>> sorted(name)
['li', 'sun', 'zhang', 'zhao']
>>> sorted(name, key=len)
['li', 'sun', 'zhao', 'zhang']
>>> sorted(name, key=len, reverse=True)
['zhang', 'zhao', 'sun', 'li']
>>> name
['zhang', 'li', 'zhao', 'sun']
最后可以看到,原列表并没有发生变化。
然后我们用sort试一下:
>>> name.sort()
>>> print(name.sort())
None
>>> name
['li', 'sun', 'zhang', 'zhao']
这个时候,name本身已经被排序了。
2.82.82.8 用bisect来管理已排序的序列
排序在平常的编程中还是很好用的,对序列排序后,我们就可以使用一些方便的查找方法。
例如:
标准库中有一个bisect模块,该模块实现了一个算法用于插入元素到有序列表。
>>> data = [1,3,9,11]
>>> data.sort()
>>> data
[1, 3, 9, 11]
>>> import bisect
>>> bisect.insort(data,7)
>>> data
[1, 3, 7, 9, 11]
>>> bisect.bisect(data, 2)
1
>>> data
[1, 3, 7, 9, 11]
实际上,bisect是运用二分查找,主要用于对有序列表进行操作。他有两个可选参数——lo和hi(low和high)。lo默认为0,hi默认值为序列的长度。这里使用二分法来排序,将一个元素插入到一个有序列表的合适位置,就不需要每次都调用sort的方式来处理有序列表。
该bisect函数是bisect_right的函数的别名,还有一个叫bisect_left。返回的插入位置是原序列中被插入的元素的位置。
刚才上例中的insort方法用起来就很方便。他的两个参数为seq和item。将变量item插入到seq中,并且保持着原序列的顺序。它其实就相当于用bisect函数先找到对应的位置,再调用insert()方法进行插入操作。但显然,前者便捷的多。
同样,它也有一个相近的方法叫insort_left()。
总结了那么多的方法,感觉在平时的代码中,list是被用的最多的。list的优势是灵活多变,但其实在不同的场景中,根据不同的需要,选择最适合代码的类型才是最重要哒!
例如:当我们需要存放1000万个float或是int类型的数字时,如果以list存储,那么每个元素都作为一个对象存储,就会浪费很多的内存。可是如果我们使用数组,数字都将以字节的方式的存储。就会节省的多。