第二章 序列构成的数组

第二章 序列构成的数组

Python 用统一的风格去处理序列数据。不管是哪种数据结构,字符串、列表、字节序列、数组、XML 元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。

内置序列类型概览

Python 标准库用 C 实现了丰富的序列类型,列举如下。

  • 容器序列
    list、tuple 和 collections.deque 这些序列能存放不同类型的数据。

  • 扁平序列
    str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。

    容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是
    引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧
    凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。

序列类型还能按照能否被修改来分类。

  • 可变序列
    list、bytearray、array.array、collections.deque 和 memoryview。
  • 不可变序列
    tuple、str 和 bytes。

下图显示了可变序列(MutableSequence)和不可变序列(Sequence)的差异,同时也
能看出前者从后者那里继承了一些方法。

第二章 序列构成的数组_第1张图片

列表推导式与生成器表达式

列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新建一个列表。

列表推导式:

l = [function(i) for i in iterable]

生成器推导式:

t = (function(i) for i in iterable)

# 用生成器表达式初始化元组和数组
>>> symbols = '$¢£¥€¤' 
>>> tuple(ord(symbol) for symbol in symbols)
(36, 162, 163, 165, 8364, 164) 
>>> import array 
>>> array.array('I', (ord(symbol) for symbol in symbols))
array('I', [36, 162, 163, 165, 8364, 164])

列表推导不会再有变量泄漏的问题

列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在 Python 3 中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。

生成笛卡尔积

第二章 序列构成的数组_第2张图片

第二章 序列构成的数组_第3张图片

元组不仅仅是不可变的列表

元组和记录

元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个
字段的位置。正是这个位置信息给数据赋予了意义。
如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位
置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息
就变得非常重要了。

第二章 序列构成的数组_第4张图片

元组拆包

两种常见形式:

  • 平行赋值 a,b,_ = (1,2,'years')
  • 变量互换 b, a = a, b

⭐运算符*

运算符*把一个可迭代对象拆开作为函数的参数:

>>> divmod(20, 8) 
(2, 4) 
>>> t = (20, 8) 
>>> divmod(*t) 
(2, 4) 
>>> quotient, remainder = divmod(*t) 
>>> quotient, remainder 
(2, 4)

用*来处理剩下的元素,在 Python 中,函数用 *args来获取不确定数量的参数算是一种经典写法了:

>>> a, b, *rest = range(5) 
>>> a, b, rest 
(0, 1, [2, 3, 4]) 
>>> a, b, *rest = range(3) 
>>> a, b, rest 
(0, 1, [2]) 
>>> a, b, *rest = range(2) 
>>> a, b, rest 
(0, 1, [])

在平行赋值中,* 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置

具名元组

collections.namedtuple是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
namedtuple构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为Python 不会用__dict__来存放这些实例的属性。

>>> from collections import namedtuple 
>>> City = namedtuple('City', 'name country population coordinates')   
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))   
>>> tokyo 
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 
139.691667)) 
>>> tokyo.population   
36.933 
>>> tokyo.coordinates 
(35.689722, 139.691667) 
>>> tokyo[1] 
'JP'


>>> City._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)    
>>> delhi._asdict()    
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)

除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性,几个最有用的:_fields 类属性、类方法_make(iterable) 和实例方法 _asdict()

切片

[start:stop:step]

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

在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。

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

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

给切片赋值

如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独
一个值,也要把它转换成可迭代的序列。

对序列使用+ 和 *

Python 程序员会默认序列是支持 + 和 * 操作的。通常 + 号两侧的序列由相同类型的数据所
构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类
型数据的序列来作为拼接的结果。

+* 都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列。适用于完成简单的复制和拼接操作。

⭐注意:如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用my_list = [[]] * 3来初始化一个由列表组成的列表,但是你得到的列表里包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。

有时我们会需要初始化一个嵌套着几个列表的列表,最好的选择是使用列表推导。

# 列表推导式
>>> board = [['_'] * 3 for i in range(3)]   
>>> board 
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] 
>>> board[1][2] = 'X'  
>>> board 
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
# *
>>> weird_board = [['_'] * 3] * 3  
>>> weird_board 
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] 
>>> weird_board[1][2] = 'O'   
>>> weird_board 
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

序列的增量赋值

增量赋值运算符+=*= 的表现取决于它们的第一个操作对象。

+=背后的特殊方法是 __iadd__(用于“就地加法”)。但是如果一个类没有实现这个方法的
话,Python 会退一步调用 __add__

如 果 a 实 现 了 __iadd__方 法, 就 会 调 用 这 个 方 法。 同 时 对 可 变 序 列( 例 如 list、
bytearrayarray.array)来说,a 会就地改动,就像调用了 a.extend(b) 一样。但是如
果 a 没有实现 __iadd__的话,a += b这个表达式的效果就变得跟a = a + b 一样了:首先
计算 a + b,得到一个新的对象,然后赋值给a。也就是说,在这个表达式中,变量名会不
会被关联到新的对象,完全取决于这个类型有没有实现 __iadd__ 这个方法。

>>> l = [1, 2, 3] 
>>> id(l) 
4311953800  
>>> l *= 2 
>>> l 
[1, 2, 3, 1, 2, 3] 
>>> id(l) 
4311953800   
>>> t = (1, 2, 3) 
>>> id(t) 
4312681568   
>>> t *= 2 
>>> id(t) 
4301348296  

对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。

一个关于+=的谜题

一个谜题

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

到底会发生下面 4 种情况中的哪一种?

  • a. t 变成 (1, 2, [30, 40, 50, 60])
  • b. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
  • c. 以上两个都不是。
  • d. a 和 b 都是对的。

难以想象,答案是d!

>>> t = (1, 2, [30, 40]) 
>>> t[2] += [50, 60] 
Traceback (most recent call last): 
  File "", line 1, in <module> 
TypeError: 'tuple' object does not support item assignment 
>>> t 
(1, 2, [30, 40, 50, 60])

查看字节码

>>> dis.dis('s[a] += b') 
  1           0 LOAD_NAME                  0(s) 
              3 LOAD_NAME                  1(a) 
              6 DUP_TOP_TWO 
              7 BINARY_SUBSCR                       
              8 LOAD_NAME                  2(b) 
             11 INPLACE_ADD                          
             12 ROT_THREE 
             13 STORE_SUBSCR                         
             14 LOAD_CONST                 0(None) 
             17 RETURN_VALUE

3 个教训:
• 不要把可变对象放在元组里面。
• 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
• 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。

list.sort方法和内置函数sorted

Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。

list.sort方法会就地排序列表,也就是说不会把原列表复制一份,因此返回值为None。与 list.sort 相反的是内置函数sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。而不管sorted 接受的是怎样的参数,它最后都会返回一个列表。

不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数:

  • key:一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果 将是排序算法依赖的对比关键字。比如说,在对一些字符串排序时,可以用 key=str. lower 来实现忽略大小写的排序,或者是用 key=len 进行基于字符串长度的排序。
  • reverse:如果被设定为 True,被排序的序列里的元素会以降序输出(也就是说把最大值当作最小值来排序)。这个参数的默认值是 False。

用bisect来管理已排序的序列

用bisect来搜索

bisect(haystack, needle) 在 haystack(干草垛)里搜索 needle(针)的位置,该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。也就是在说这个函数返回的位置前面的值,都小于或等于 needle 的值。其中 haystack 必须是一个有序的序列。你可以先用 bisect(haystack, needle)查找位置 index,再用 haystack.insert(index, needle)来插入新值。但你也可用insort 来一步到位,并且后者的速度更快一些。

# 在有序序列中用 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: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))   
 
if __name__ == '__main__': 
 
    if sys.argv[-1] == '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)

第二章 序列构成的数组_第5张图片

当列表不是首选时

虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。

数组

如果我们需要一个只包含数字的列表,那么array.arraylist 更高效。数组支持所有跟可变序列有关的操作,包括 .pop.insert.extend。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes.tofile

内存视图

memoryview是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview.cast的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast会把同一块内存里的内容打包成一个全新的memoryview对象给你。

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

利用 .append.pop方法,我们可以把列表当作栈或者队列来用(比如,把 .append.pop(0)合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这差不多。memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。

你可能感兴趣的:(python,python)