当前位置: 代码迷 >> 综合 >> Fluent Python - Part2 序列构成的数组
  详细解决方案

Fluent Python - Part2 序列构成的数组

热度:36   发布时间:2023-10-21 05:28:26.0

序列

Python 使用统一的风格去处理序列数据,不管是哪种数据结构,它们都共用一套丰富的操作:迭代,切片,排序和拼接。通过了解 Python 不同的序列类型以及它们的使用,能够让我们避免重复发明轮子,而且 Python 的 API 能够帮助我们把自己定义的结构设计得跟原生序列一样。

序列类型

  • 根据是否能放不同类型的数据分类

    • 容器序列:能存放不同类型的数据,存放的是对象的引用。其中包括:list,tuple,collections.deque等。
    • 扁平序列:仅能存放基础类型的对象,如字符,字节,数值。其中包括:str,bytes,bytearray,memoryview, array.array
  • 根据容器内数据是否可变分类

    • 可变序列:list, bytearray,array.array, collections.dequememoryview
    • 不可变序列:tuple, strbytes

列表推导和生成器表达式

列表推导

一个例子

symbols = 'abcdefg'
codes = [ord(symbol) for symbol in symbols]
  • 优势:简单,可读。
  • 劣势:在使用列表推导时,会建立一个完整的列表,然后再将列表传递出去,某些情况下会有额外的内存占用。
  • 如果列表推导的代码超过两行,则需要考虑是不是得用for循环重写了。

变量泄露的问题

在 python2 中,列表推导中的变量会覆盖外部的同名变量,而在 python3 中则不会。

Python 2.7.17 (default, Apr 15 2020, 17:20:14)
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'abcd efg'
>>> dummy = [x for x in 'pouyr']
>>> x
'r'
Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'abcd efg'
>>> dummy = [x for x in 'pouyr']
>>> x
'abcd efg'

生成器表达式

一个例子

Python 2.7.17 (default, Apr 15 2020, 17:20:14)
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'abcd efg'
>>> tuple(ord(a) for a in x)
(97, 98, 99, 100, 32, 101, 102, 103)
>>> import array
>>> array.array('I', (ord(a) for a in x))
array('I', [97L, 98L, 99L, 100L, 32L, 101L, 102L, 103L])
  • 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来,否则括号是必须的。
  • 生成器表达式逐个产出元素。

元组

  • 元组是不可变的列表
  • 元组可用于记录(没啥用)

元组拆包

一个例子

>>> a= (1 , 2)
>>> x, y = a
>>> x
1
>>> y
2
  • * 来处理剩下的元素, 在 python 中,函数用 *args 来获取不确定数量的参数算是一种经典的写法了。

切片

一个例子

>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

a:b:c 这种用法只能作为索引或者下标在[]中来返回一个切片对象: slice(a, b, c), 当使用 seq[start:stop:step] 时,python实际会调用seq.__getitem__(slice(start, stop, step))。虽然我们还不了解这个切片对象,但知道这个原理足以让我们做一些骚操作了。比如解析一些纯文本文件,使用有名字的切片比硬编码的数字区间要方便得多。

对序列使用 +*

>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
  • 注意:在 a * n 这个语句中,如果 a 里的元素是对其他可变对象的引用的话,那么实际上会产生 n 个对该对象的引用。

  • Good Case

>>> bord = [['_'] * 3 for i in range(3)]
>>> bord
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> bord[1][2] = 'X'
>>> bord
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
  • Bad Case
>>> bord2 = [['_'] * 3] * 3 # 外面的这个*3会导致重复生成3个['_']的引用
>>> bord2
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> bord2[1][2] = 'X'
>>> bord2
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

序列的增量赋值

  • += 背后的特殊方法是__iadd__,如果没有实现这个方法,Python会退一步使用__add__。考虑下面的这个简单的表达式:
>>> a += b
  • 如果 a 实现了 __iadd__ 方法,对可变序列来说, a 就会就地改动,就像调用了 a.extend(b) 一样。但如果a 没有实现__iadd__ 的话, a += b 这个表达式的效果就变成了a = a + b,首先计算a + b ,得到一个新的对象,然后赋值给a。也就是说,在这个表达式中,变量名会不会关联到新的对象,完全取决于这个类型有没有实现__iadd__ 这个方法。
>>> l = [1, 2, 3]
>>> id(l)
140306575506720
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
140306575506720 # 结果不变
>>>
>>> t = (1, 2, 3)
>>> id(t)
140306575504240
>>> t *= 2
>>> id(t)
140306575887840 # 结果会变

一个问题

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

运行后的结果是:

Traceback (most recent call last):File "<stdin>", 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)2 LOAD_NAME                1 (a)4 DUP_TOP_TWO6 BINARY_SUBSCR8 LOAD_NAME                2 (b)10 INPLACE_ADD12 ROT_THREE14 STORE_SUBSCR16 LOAD_CONST               0 (None)18 RETURN_VALUE

上述操作会在第14步失败,因为s是不可变的元组。这其实是个非常罕见的边界情况,在2个月的Python生涯中,我还没见过谁在这个地方吃过亏。

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

list.sort 方法和内置函数sorted

  • list.sort 就地排序,返回值是None。而内置函数sorted会新建一个列表作为返回值。
  • 两个关键字
    • reverse
      • True 为降序输出,False 为升序输出,默认值为False
      • key
        • 一个只有一个参数的函数,产生的结果将是排序算法依赖的对比关键字。

bisect来管理已排序的序列

>>> import bisect
>>> a = [1, 1, 2, 2, 3, 4, 5, 6]
>>> bisect.bisect_left(a, 3)
4
>>> bisect.bisect(a, 2)
4
  • bisect 类似 C++ 的upper_bound函数,找到第一个大于该值的下标,而bisect_left 类似 C++ 的 lower_bound函数,找到第一个大于等于该值的下标。
  • 复杂度: O(log(n)) (二分找)

bisect.insort 插入新元素

>>> import bisect
>>> a = [1, 1, 2, 2, 3, 4, 5, 6]
>>> bisect.insort(a, 4)
[1, 1, 2, 2, 3, 4, 4, 5, 6]
  • 同理它有个变体叫insort_left, 这个变体背后用的是bisect_left.
  • 复杂度: O(log(n))(二分找) + O(n)(往里塞)

其他序列

数组

  • 如果我们需要一个只包含数字的列表,那么array.arraylist 更高效。数组支持所有跟可变序列有关的操作。另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes.tofile
  • Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码。比如b 类型码代表的是有符号的字符(signed char), 因此 array('b') 创建出的数组就只能存放一个字节大小的整数,范围从-128到127。这样在序列很大的时候,我们能节省很多空间,而且Python不会允许你在数组里存放除指定类型之外的数据。
  • 优势:快。
  • 劣势:只能处理数字。
>>> from array import array
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))
>>> floats[-1]
0.36472462317072263
>>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp)
>>> fp.close()
>>> floats2 = array('d')
>>> fp = open('floats.bin', 'rb')
>>> floats2.fromfile(fp, 10**7)
>>> fp.close()
>>> floats2[-1]
0.36472462317072263
>>> floats2 == floats
True

内存视图

memoryview 是一个内置类,它能让用户不复制内容的情况下操作一个数组的不同切片。

  • memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。memoryview.cast 会把同一块内存里的内容打包成一个全新的memoryview 对象给你。
>>> from array import array
>>> numbers = 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])
  • 位置5是下标为2的元素的高位,高位变成了4,整体的值则变成了1024

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

  • collections.deque类(双向队列)是一个线程安全的队列,可以指定队列的大小,如果队列满了,则会删除头部的元素
  • appendpopleft 都是原子操作,也就是说deque可以在多线程中安全的使用,而使用者不需要担心资源锁的问题

其他队列

queue包

queue包提供了同步(线程安全)类Queue, LifoQueuePriorityQueue,不同的线程可以利用这些数据类型来交换信息。这三个类的构造方法都有一个可选参数maxsize, 它接受正整数作为输入值,用来限定队列的大小。但是在满员的时候这些类不会扔掉旧的元素来腾出位置相反,如果队列满了它会被锁住,直到另外的线程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量。

multiprocessing

这个包实现了自己的Queue, 它跟queue.Queue 类似,是设计给进程通信用的。同时还有一个专门的multiprocessing.JoinableQueue类型,可以让任务管理变得更方便。

asyncio

Python 3.4 新提供的包,里面有Queue, LifoQueue, PriorityQueueJoinableQueue 这些类受到 queuemultiprocessing 模块的影响,但是为异步编程里的任务管理提供了专门的便利。

heapq

跟上面三个模块不同的是,heapq没有队列类,而是提供了heappushheappop方法,让用户可以把可变序列当做堆队列或者优先队列来使用。

第二章的内容真多,写了一晚上。。主要内容大概就是Python的几个序列。接下来的内容就是字典和集合啦!