第二部分 Data Structure

  • Chapter2 An Array of Sequences
  • Chapter3 Dictionaries and Sets
  • Chapter4 Text versus Bytes

 


 

An Array of Sequences 

本章讨所有的序列包括list,也讨论Python3特有的str和bytes。

也涉及,list, tuples, arrays, queues。

 

概览内建的序列

分类

Container swquences: 容器类型数据

  • list, tuple
  • collections.deque: 双向queue。

Flat sequences: 只存放单一类型数据

  • str,
  • bytes, bytearray, memoryview : 二进制序列类型
  • array.array:  array模块中的array类。一种数值数组。即只储存字符,整数,浮点数。

分类2:

Mutable sequences:

  • list, bytearray, array.array
  • collections.deque
  • memoryview

Immutable sequences:tuple, str, bytes

 

python dataclasses field 知乎_子类

 

 

⚠️,内置的序列类型,并非直接从Sequence和MutableSequence这两个抽象基类(Abstract Base Class ,ABC)继承的。

了解这些基类,有助于我们总结出那些完整的序列类型包括哪些功能。

 

List Comprehensions and Generator Expressions

可以简写表示:listcomps, genexps。

例子:使用list推导式。

#
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]

 

ord(c)是把字符转化为Uicode对应的数值.

 

列表推导式的好处:

  • 比直接用for语句,更方便。也同样好理解。
  • 类似函数, 会产生局部作用域,不会再有变量泄露的问题。

 

map()和filter组合

symbols = '$¢£¥€¤'
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
print(beyond_ascii)

 

map(func, iterable) -> iterator

速度上list comprehensions更快。

 

Generator Expressions

Listcomps只能产生一个list,而genexps可以产生其他类型的序列。

它遵守迭代器协议,逐个产生元素。

 

例子,利用genexps产生tuple和array.array

symbols = '$¢£¥€¤'
a = tuple(ord(symbol) for symbol in symbols)
print(a)

import array
arr = array.array("I", (ord(symbol) for symbol in symbols))
print(arr)
print(arr[0])

 

 

colors = ['black', 'white']
sizes = ['S', 'M', 'L']
a = ('%s %s' % (c, s) for c in colors for s in sizes)
print(a)
#产生一个生成器表达式<generator object <genexpr> at 0x10351b3c0>

 

 

⚠️函数生成器,要加yield关键字。

 


 

Tuples are not just Immutable lists 

tuple除了是不可变数组/列表。

另有一个功能体现: 储存从数据库提取的一条记录:record, 但这个record没有field name,只有value。

例如:

traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),
                ('ESP', 'XDA205856')]
for passport in sorted(traveler_ids):
    print('%s/%s' % passport)

for country, _ in traveler_ids:
    print(country)
#输出
BRA/CE342567
ESP/XDA205856
USA/31195855
USA
BRA
ESP

⚠️:_是一个占位符。

 

tuple unpacking

一个习惯用法产生的概念:

>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # tuple unpacking >>> latitude
33.9425
>>> longitude
-118.408056

下面的赋值代码省略了中间变量 :

>>>b,a=a,b
#等同于
x = (a, b)
b, a = x

 

这个概念也可以用到其他的类型上,如list。range(), dict。

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

⚠️使用*号抓起过多的item

 

有拆包,就有打包:

>>> o = 1
>>> n = 2
>>> o, n
(1, 2)

 

Nested Tuple Unpacking  嵌套tuple的解包

只要表达式左边的结构,符合嵌套元祖的结构,嵌套tuple也可以解包

>>> x = ('Tokyo','JP',36.933,(35.689722,139.691667))
>>> name, cc, pop, (latitude, longitude) = x
>>> name
'Tokyo'
>>> longitude
139.691667

 

 

Named Tuples --一个tuple的子类

具有tuple的方法,同时也有自己的方法和属性。

 

因为tuple具有拆包封包的特性,用起来很方便。

⚠️list拆包后,再封包,得到的是tuple类型。

>>> a = list(range(2))
>>> a
[0, 1]
>>> x,y = a
>>> x
0
>>> y
1
>>> x, y
(0, 1)
>>> z = x ,y
>>> z
(0, 1)

 

 

但是,如果把tuple类型用在一条数据库记录上:因为缺少fields字段。就很不方便,因此出现了Tuples类的子类named tuples。

使用工厂函数:collections.namedtuple(typename, field_names, *) 

  • field_names可以是['x', 'y']也可以是"x, y, z"或"x y z" 

下例子:创建一个子类City, 它的父类是tuple。即Ciyt.__bases__返回(<class 'tuple'>,)

from collections import namedtuple

Card = namedtuple("Card", ['rank', 'suit'])

City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
print(tokyo)
#City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))

 

⚠️内存一样

namedtuple和tuple的实例,占用的内存是一样的。因为namedtuple实例把字段名储存在了对应的类中。

因为结构固定,所以可以像dict一样显示key=value;

Tu = ('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
import sys
print(sys.getsizeof(tokyo))
print(sys.getsizeof(Tu))
#88
#88

 

类方法

_fields: 返回类的所有的field的名字。

>>> City._fields
('name', 'country', 'population', 'coordinates')

 

 

_asdict:返回一个对应field字段的dict:

print(tokyo._asdict())

#返回一个dict  {'name': 'Tokyo', 'country': 'JP', 'population': 36.933, 'coordinates': (35.689722, 139.691667)}

 

 

_make(): 接受一个可迭代对象来生成这个类的一个实例。等同于City(*tokyo)。

 

小结:

因为namedtuple的功能扩展,这个类就支持从数据库读取一条记录。但是因为是tuple的子类,不能对记录进行修改。

 


 

 

Slicing  --切片的高级用法

list, tuple, str这些sequences类都支持slicing。

 

Why Slices and Range Exclude the last Item?

就是习惯。真要找原因:

  •  my_list[:x] and my_list[x:]合起来就是my_list
  • a[:3], 直接清楚的表示这个切片包括3个item。
  • a[2:6], 6-2等于4。表示这个切片包括四个item。

 

Slice Objects

切片对象的用途:

一个有固定格式纯文本文件,需要对这个文件的每一行都进行相同的切片操作,那么使用Slice对象保存这种切片的起始位置和step。

之后无需反复重写切片了。

例子:

#invoice.txt
0.....6.................................40........52...55........
1909 Pimoroni PiBrella                      $17.50    3    $17.50
1489 6mm Tactile Switch x20                  $4.95    2     $9.90
1510 Panavise Jr. - PV-201                  $28.00    1    $28.00
#首先,读取文本文件的内容
with open('invoice.txt', 'r') as invoice:
    line_items = invoice.read()
#然后,按照文本内的格式,创建3个切片对象,
sku = slice(0, 6)
description = slice(6, 40)
unit_price = slice(40, 52)
#最后,逐行打印切片的结果
for item in line_items.split('\n')[1:]:
    print(item[unit_price], item[description])

 

直接使用切片对象的好处,方便代码的管理和维护。用自然语言描述要切片的字符串。

 

 

class slice(start=None, stop[, step=None])

返回一个slice对象。start,step默认为None。

slice对象有三个只读的数据属性(就是instance variable)start, stop, step

>>> a
slice(0, 6, None)
>>> type(a).__dict__.keys()
dict_keys(['__repr__', '__hash__', '__getattribute__', '__lt__', '__le__', 
'__eq__', '__ne__', '__gt__', '__ge__', '__new__', 'indices', '__reduce__', 'start', 'stop', 'step', '__doc__'])
>>> a.start
0
>>> a.stop
6

  

 

 给切片赋值

  • 替换原list中的元素。
  • 切片start等于stop,则相当于插入了。

⚠️可参考源码(c写的)或这篇文章https://www.the5fire.com/python-slice-assignment-analyse-souce-code.html

 


 

 

Pythong, Ruby都支持对序列类型进行+和*操作

需要注意*操作对嵌套list:

>>> d = [["1"]*3]*3
>>> d
[['1', '1', '1'], ['1', '1', '1'], ['1', '1', '1']]
>>> d[1][1] = 's'
>>> d
[['1', 's', '1'], ['1', 's', '1'], ['1', 's', '1']]

相当于:

>>> a = ['1']*3
>>> a
['1', '1', '1']
>>> b = [a]*3
>>> b
[['1', '1', '1'], ['1', '1', '1'], ['1', '1', '1']]
>>> b[1][1] = "x"
>>> b
[['1', 'x', '1'], ['1', 'x', '1'], ['1', 'x', '1']]

 ⬆️例子,列表b,使用了3次a。b相当于[a, a, a]。

 

 


 

 

Augmented Assignment with Sequences  序列的增量赋值操作符 *=, +=

 

+=的背后是__iadd__方法,即(in-place addition,翻译过来就是,在当场进行加法运算,简单称为就地运算。)

这是因为a.+= b中的a 的内存地址不发生变化,所以称为就地运算。

⚠️但是,是否执行就地运算,要看type(a)是否包括__iadd__这个方法了。如果没有,则只相当于a = a + b, 新的a是一个新的对象。

  • 可变序列支持__iadd__
  • 不可变序列当然不支持了。比如tuple就不支持。
  • ⚠️str作为不可变序列,支持__iadd__,这是因为在循环内对str做+=太普遍了。因此对它做了优化,str实例初始化内存时预留了足够的空间,因此不会复制原有的字符串到新内存位置,
>>> l = list(range(3))
>>> l
[0, 1, 2]
>>> id(l)
4421232256
>>> l *= 2
>>> id(l)
4421232256

 

同样 *=及其他增量赋值操作符 都是这样。

 

关于+=的 Puzzler

>>> 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模块。 code object等知识。

模块dis:https://docs.python.org/zh-cn/3/library/dis.html  字节码反汇编器

什么是字节码?

字节码就是把可读的源码通过编译转化为bytecode,字节码还需要解释器才能转换为机器代码,所以字节码是一种中间代码。

操作系统都支持字节码转化,因此字节码可以用到不同系统中,通过转化为机器码直接在硬件上运行。

而且字节码也可以逐条执行。有了解释型语言的特点。

Python源代码会被编译成bytecode, 即Cpython解释器中表示Python程序的内部代码。它会缓存在.pyc文件(在文件夹__pycache__)内,这样第二次执行同一文件时速度更快。

具体见博文:

 

再说上面的例子: dis.dis(x):反汇编x对象。

>>> dis.dis('s[a] += b')
  1           0 LOAD_NAME                0 (s)     #把s推入计算栈
              2 LOAD_NAME                1 (a)      #把a推入计算栈
              4 DUP_TOP_TWO                         # 复制顶部的两个引用,无需从栈取出item
              6 BINARY_SUBSCR                      #执行计算: TOS = TOS1[TOS], 即把s[a]的值推入计算栈(TOS代表栈的顶部), 现在栈里有3个item.
              8 LOAD_NAME                2 (b)     #把b推入栈
             10 INPLACE_ADD                         #Tos = tos1+ tos, 即先从栈取出前2个item, 然后s[a] + b的结果推入栈。
             12 ROT_THREE                # 将第2个,第三个栈项向上提升一个位置,栈顶部的项移动到第3个位置。
             14 STORE_SUBSCR              #  tos1[tos] = tos3
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE
t[2] += [50,60]

当进行到上面👆粉色行时,要改变元组t的第3个元素,所以会报告❌。

由上面的例子想到:

  • 不要把可变对象放到tuple里
  • 增量赋值不atomic operation, 它抛出了❌,但完成了操作。
  • 查看Python的字节码并不难。

 

附加:

上一章提到的slicing,通过dis.dis()分析,发现a[slice(1:2)]等同于a[1:2]


 

 

list.sort方法和内建函数sorted()

 

list.sort()和sorted()的区别:

>>> b = [3,2,1]
>>> b.sort
<built-in method sort of list object at 0x10c99c180>
>>> b.sort()
>>> b
[1, 2, 3]
>>> c = [3,2,1]
>>> sorted(c)
[1, 2, 3]
>>> c = (3,2,1)
>>> sorted(c)
[1, 2, 3]

 

list.sort方法是对原list对象进行操作,不是复制,返回None,即操作原对象,不创造新list。

sorted()会创建一个新的list, 并返回这个list。它的参数可以是任何的iterable object。 

 

sorted(iterable,*, key=None, reverse=False)

具体参数讲解见文档: key接受一个函数明,算法根据key来排序。

>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry']
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple']
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']

 

 


 

 

使用bisect模块来管理已经排序的序列

这个模块提供了2个函数bisect, insort。使用了binary search algorithm来进行搜索或插入元素。这个算法广泛应用于二叉搜索数,B树系列。

 

bisect.bisect_left(a, x)

a是一个有序序列,x是要插入a的值。为了保持插入x后,a仍然是一个有序序列,使用二分搜索算法,返回一个数值,这个数值是x应该插入的索引位置。

如果a是list,那么返回值可以作为list.insert()的第一个参数。例子:

>>> NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
>>> import bisect
#用bisect(a, x)得到要插入的位置,然后使用insert()方法插入
>>> NEEDLES.insert(bisect.bisect(NEEDLES, 7),7)
>>> NEEDLES
[0, 1, 2, 5, 7, 8, 10, 22, 23, 29, 30, 31]

 

 

除了bisect_left还有bisect及bisect_right函数,它们的区别就是如果要插入的x元素已经存在于a中,则把x插入到已经存在的x元素的左边或右边

 

上面的代码仍然可以进一步封装,因此封装成bisect.insort(a, x)

>>> NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
>>> import bisect
>>> bisect.insort(NEEDLES, 7)
>>> NEEDLES
[0, 1, 2, 5, 7, 8, 10, 22, 23, 29, 30, 31]

 

一个很好玩的例子,格式输出:

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):
        positon = bisect_fn(HAYSTACK, needle)
        offset = positon * '  |'   #偏移
        print(ROW_FMT.format(needle, positon, 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)

 

可以试一试看看结果。

~/自我练习/python练习 ⮀ python3 linshi.py
#如果带参数left, 则插入位置发生变化。
 ~/自我练习/python练习 ⮀ python3 linshi.py  left

 

利用这个模块可以带来很多便利,具体见文档的例子。

需要⚠️,二分搜索算法的时间复制度O(lgn), 插入list的时间复杂度是O(n)。

 


 

 

在大数据下运行,寻求更快的运算速度。

list类型用起来很灵活,并且容易使用。但在一些特殊需求下,有更好的选择。

比如存放1000万个浮点数字,在这么大的大数据下,代码运算的快慢就体现出来了。

如果使用Python提供的array模块中的array,就可以带来更快的速度。因为一个array中存的不是float对象,而是打包的bytes类型,相当于机器值。这点和C语言中的数组类似。

还有,频繁对序列的第一个和最后一个元素进行添加和移除操作,那么deque或双deque类型数据运算的速度更快。

 

Python中提供了大量标准库包,这些都是可以挖掘的知识点,能为不同需求提供支持。所以如果寻求高效,那么就充分掌握对标准库的序列类型。

 

备注:

本章节从array之后未做阅读和学习,仅仅大略的了解了一下。