第2章 序列构成的数组

171 阅读9分钟

序列构成的数组

内置序列类型

  • 数据类型

    • 容器序列

      listtuplecollections.deque

    • 扁平序列

      strbytesbytearraymemoryviewarray.array

    • 容器序列存放的是他们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。

  • 可修改性

    • 可变序列(MutableSequence)

      listbytearrayarray.arraycollections.dequememoryview

    • 不可变序列(Sequence)

      tuplestrbytes

    • 继承关系

      20220711155057.png

列表推导和生成器表达式

  • 列表推导

    • 略:就基本的列表推导,不再复述

    • map/filter组合的效率不一定高于列表推导

  • 生成器表达式

    虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存。生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。

元组

元组不仅仅是不可变的列表,它还可以用于没有字段名的记录。

  • 拆包

    • 略:就基本的拆包,不再复述

    • * 运算符把一个可迭代对象拆开作为函数的参数,在 Python 中,函数用 *args 来获取不确定数量的参数算是一种经典写 法了。在平行赋值中,* 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置:

      >>> divmod(20, 8)
      (2, 4)
      >>> t = (20, 8)
      >>> divmod(*t)
      (2, 4)
      >>> quotient, remainder = divmod(*t)
      >>> quotient, remainder
      (2, 4)
      
  • 具名元组

    • 具名元组的属性和方法

      from collections import namedtuple
      City = namedtuple('City', 'name country population coordinates')
      tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
      
      >>> City._fields #_fields 属性是一个包含这个类所有字段名称的元组。
      ('name', 'country', 'population', 'coordinates')
      
      >>> LatLong = namedtuple('LatLong', 'lat long')
      >>> delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
      >>> delhi = City._make(delhi_data) # 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟 City(*delhi_data) 是一样的
      
      >>> delhi._asdict() #_asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。
      OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population',
      21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))])
      
      >>> for key, value in delhi._asdict().items():
      print(key + ':', value)
      name: Delhi NCR
      country: IN
      population: 21.935
      coordinates: LatLong(lat=28.613889, long=77.208889)
      

切片

alist[start:stop:step]

  • 示例初始化:alist = [0,1,2,3,4]

  • 参数

    • start:起始位置
    • stop:终止位置
    • step:步长
  • 负值用法

    • alist[-1]:表示取alist的最后一个值,输出为4
    • alist[-3:]:表示从alist的倒数第三个值开始,取list剩下的元素,输出为[2, 3, 4]
    • alist[:-1]:表示从alist的开始取元素,直到下标为倒数第一,输出为[0, 1, 2, 3]
  • step参数为负值

    step为负值时候表示将列表逆向,最简单的例子为:alist[::-1]。即从倒数第一个元素开始,每一步加上步长,因为这里步长为负数,所以是减法,直到alist的开头。

  • 内部调用

    seq[start:stop:step] 进行求值的时候,Python 会调用seq.__getitem__(slice(start, stop, step))

为什么切片和区间会忽略最后一个元素

  • 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)my_list[:3] 都返回 3 个元素。
  • 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
  • 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x]my_list[x:] 就可以了。

对序列使用 + 和 *

  • 对可变序列使用*

    如使用my_list = [[]] * 3来初始化一个由列表组成的列表,我们得到的列表里包含的三个元素实际上是三个引用,并且这三个引用指向同一个列表。

  • 建立由列表组成的列表

    >>> board = [['_'] * 3 for i in range(3)] 
    >>> board
    [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
    >>> board[1][2] = 'X' 
    >>> board
    [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
    

序列的增量赋值

  • += 背后的特殊方法是 __iadd__ (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__
  • 对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
  • 两个教训:
    • 不要把可变对象放在元组里面。
    • 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。

排序

  • list.sort():就地排序,不会把原列表复制一份,所以这个方法的返回值为None
  • sorted():会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管sorted接受的是怎样的参数,它最后都会返回一个列表。
    • 如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象,例如random.shuffle
  • 参数
    • reverse :如果被设定为True,被排序的序列里的元素会以降序输出,默认值为False
    • key:一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些字符串排序时,可以用 key=str.lower 来实现忽略大小写的排序,或者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒等函数 (identity function),也就是默认用元素自己的值来排序。可选参数 key 还可以在内置函数 min()max() 中起作用。另外,还有些标准库里的函数也接受这个参数,像 itertools.groupby()heapq.nlargest() 等。

bisect来管理已排序的序列

bisect 模块包含两个主要函数,bisectinsort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。

  • bisect.bisect_left(a, x, lo=0, hi=len(a))

    a 中找到 x 合适的插入点以维持有序。参数 lohi 可以被用于确定需要考虑的子集;默认情况下整个列表都会被使用。如果 x 已经在 a 里存在,那么插入点会在已存在元素之前(也就是左边)。如果 a 是列表(list)的话,返回值是可以被放在 list.insert() 的第一个参数的。

  • bisect.bisect_right(a, x, lo=0, hi=len(a))

  • bisect.bisect(a, x, lo=0, hi=len(a))

    bisect_right()等同于bisect(),类似于 bisect_left(),但是返回的插入点是 a 中已存在元素 x 的右侧。

    In [64]: haystack
    Out[64]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
    
    In [65]: bisect.bisect_left(haystack, 0)
    Out[65]: 0
    
    In [66]: bisect.bisect_right(haystack, 0)
    Out[66]: 1
    
    In [67]: bisect.bisect(haystack, 0)
    Out[67]: 1
    
  • bisect.insort_left(a, x, lo=0, hi=len(a))

  • bisect.insort_right(a, x, lo=0, hi=len(a))

  • bisect.insort(a, x, lo=0, hi=len(a))

数组

如果我们需要一个只包含数字的列表,那么 array.arraylist 更高效。数组支持所有跟可变序列有关的操作。

类型码C 类型Python 类型以字节表示的最小尺寸
'b'signed charint1
'B'unsigned charint1
'u'wchar_tUnicode 字符2
'h'signed shortint2
'H'unsigned shortint2
'i'signed intint2
'I'unsigned intint2
'l'signed longint4
'L'unsigned longint4
'q'signed long longint8
'Q'unsigned long longint8
'f'floatfloat4
'd'doublefloat8

内存视图

memoryview是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview.cast的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。

>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) 
>>> len(memv)
5
>>> memv[0] 
-2
>>> memv_oct = memv.cast('B') 
>>> memv_oct.tolist() 
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) 

Numpy

基本操作:

In [14]: import numpy

In [15]: a = numpy.arange(12)

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

In [17]: type(a)
Out[17]: numpy.ndarray

In [18]: a.shape
Out[18]: (12,)

In [19]: a.shape = (3,4)   #a.shape = 3, 4 也可

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

In [21]: a[2]
Out[21]: array([ 8,  9, 10, 11])

In [22]: a[2, 1]
Out[22]: 9

In [23]: a[:, 1]
Out[23]: array([1, 5, 9])

In [24]: a.transpose()
Out[24]: 
array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])

双向队列和其他形式的队列

​ 利用 .append.pop 方法,我们可以把列表当作栈或者队列来用(比如,把 .append.pop(0) 合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。

collections.deque类(双向队列是一个线程安全、可以快速从两端添加或者删除元素的数据类型)。

>>> from collections import deque
>>> dq = deque(range(10), maxlen=10) #maxlen是一个可选参数,设置后不可修改
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
# #队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被移动到右边
>>> dq.rotate(3) 
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1) #当试图对一个已满(len(d) == d.maxlen)的队列做尾部添加操作的时候,它头部的元素会被删除掉。
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33]) 
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40])
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)