《流畅的Python》学习笔记(一)——数据结构

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

第2章 序列构成的数组

  • 前言
  • 一、内置序列类型概览
    • 1. 列表推导和生成器表达式
      • 1.1 列表推导
      • 1.2 生成器表达式
    • 2. 元组
      • 2.1 元组拆包
        • 2.1.1 平行元组拆包
        • 2.2.2 嵌套元组拆包
      • 2.2 具名元组
    • 3. 切片
      • 3.1 切片和区间忽略最后一个元素的优势
      • 3.2 给切片赋值
    • 4. *,+=,\*= 符号的应用
      • 4.1 使用*进行复制
      • 4.2 += 符号
      • 4.3 *= 符号
    • 5. list.sort方法和内置函数sorted
    • 6. bisect模块管理排序的序列
      • 6.1 bisect函数
      • 6.2 bisect.insort
    • 7. 替换列表的数据结构
      • 7.1 数组(Array)
      • 7.2 内存视图(Memoryview)
      • 7.3 NumPy和SciPy
      • 7.4 双向队列和其他形式的队列


前言

《流畅的Python》学习笔记


提示:以下是本篇文章正文内容,下面案例可供参考

一、内置序列类型概览

  1. 容器序列
    list(列表) tuple(元组) collections.deque(双端队列)
    这些序列可存放不同类型数据,存放的是所包含的任意的对象的引用
  2. 扁平序列
    str、bytes 、bytearray 、memoryview 、array.array
    这类序列只能容纳一种类型,存放的是值

1. 列表推导和生成器表达式

列表推导是构建列表(list)的快捷方式;生成器表达式可以创建其它任何类型的序列。

1.1 列表推导

列表推导示例:

>>> symbols = '$¢¤'
>>> codes = [ord(symbol) for symbol in symbols]
...
>>> codes
[65284, 65504, 164]

使用列表推导原则:只用列表推导来创建新的列表,并且尽量保持简短。(超过两行可考虑是否使用列表推导)

ord():返回对应的Ascii数值,或者Unicode数值

1.2 生成器表达式

生成器表达式背后遵守了迭代器协议可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。

1.用生成器表达式初始化元组和数组:

>>> symbols = '$¢¤'
>>> tuple(ord(symbol) for symbol in symbols)
(65284, 65504, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols))
array('I', [65284, 65504, 164])

2.用生成器表达式计算笛卡尔积:

>>> colors = ['black', 'white']
>>> sizes = ['S','M','L']
>>> for tshirt in ('%s %s' % (c,s) for c in colors for s in sizes):
>>>	print(tshirt)
...
black S
black M
black L
white S
white M
white L

使用生成器表达式后,生成器表达式逐个产出元素,内存不会留下一个6个组合的列表,因为生成器表达式会在每次for循环运行时才生成一个组合

2. 元组

2.1 元组拆包

元组拆包:将元组中的数值分别赋值给多个变量。如:

>>> city, year, pop, area = ('Tokyo', 2003, 32450, 8014) #元组拆包

2.1.1 平行元组拆包

平行赋值:把一个可迭代对象里的元素,一并复制到由对应的变量组成的元组中。

>>> city, year, pop, area = ('Tokyo', 2003, 32450, 8014) #元组拆包
  • 不使用中间变量交换两个变量的值
  >>> b,a = a,b
  • 使用*运算符把一个可迭代对象拆开作为函数参数
 >>> divmod(20,8)
 (2,4)
 >>> t = (20,8)
 >>> divmod(*t)
 (2,4)

使用*运算符获取不确定的参数

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

小知识

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

  • 让一个函数以元组的方式返回多个值。

    >>> import os
    >>> _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
    >>> filename
    'idrsa.pub'
    

    os.path.splite(): 返回以路径和最后一个文件名组成的元组(path, last_part)。

    _: 在不需要输出元组中的某个值时,可以使用占位符。

2.2.2 嵌套元组拆包

metro_areas = [
	('Tokyo','JP',36.933,(35.689722,139.691667)),
	('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
	('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
	('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
	('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'                           #9.4f表示一共占9位,小数占4位
for name, cc, pop, (latitude, longitude) in metro_areas:
	if longitude <= 0:
		print(fmt.format(name, latitude, longitude))

输出为:
                |   lat.    |   long.
Mexico City     |   19.4333 |  -99.1333
New York-Newark |   40.8086 |  -74.0204
Sao Paulo       |  -23.5478 |  -46.6358

2.2 具名元组

collections.namedtuple(class_name, zd_name):构建一个带字段名的元组和一个有名字的类。该函数有两个参数:

  • class_name:类名

  • zd_name:类的各字段的名字。

# 定义和使用具名元组
from collections import namedtuple
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933,(35.689722,139.691667))
print(tokyo)
print(tokyo.population)print(tokyo[1])           ②

输出:
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
36.933
JP

输出字段可以用实例名.具体字段名表示,如①,也可以使用实例名[i]这种数组形式表示,如上图中的②

# 具名元组的属性和方法
>>> 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(){'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():接受一个可迭代对象生成这个类的一个实例,作用和City(*delhi_data)相同

_asdict():把具名元组以collections.OrderedDict的形式返回

3. 切片

可采用s[a:b:c]的形式对s在a和b之间以c为间隔取值。若c<0则表示反向取值。

>>> s = 'bicycle'
>>> s[::3]
'bye'

>>> s[::-1]
'elcycib'

>>> s[::-2]
'eccb'

3.1 切片和区间忽略最后一个元素的优势

  1. 当只有最后一个位置信息时,可以快速看出切片和区间中有多少个元素。例range(3)有三个元素。
  2. 当起止位置信息(start, stop)都可见时,区间长度=stop-start
  3. 可以利用任意一个下标把序列分成不重叠的两部分,只要写成my_list[:x]my_list[x:]即可。

3.2 给切片赋值

>>> l = list(range(10))               
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5]=[20,30]                  #将l中的第2到4的数据替换成20和30
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2]=[11,22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5]=100                          # 如果赋值对象是一个切片,那么赋值语句右侧必须是个可迭代对象(要给它加上[])
Traceback (most recent call last):
  File "", line 1, in <module>
TypeError: can only assign an iterable
    
>>> l[2:5]=[100]
>>> l
[0, 1, 100, 22, 9]

4. *,+=,*= 符号的应用

4.1 使用*进行复制

例1:

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

*:Python中可以使用*把一个序列复制几份再拼接起来

例2:

>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = '0'
>>> weird_board
[['_', '_', '0'], ['_', '_', '0'], ['_', '_', '0']]

比较这两个例子可以发现,在例1中对board[1][2]进行赋值,只影响了一个元素,而例2中,同样是对weird_board[1][2]赋值,但可以看见weird_board[0][2],weird_board[1][2],weird_board[2][2]的值都被影响了。

原因:例2的列表其实是包含3个指向同一列表的引用

4.2 += 符号

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

>>> 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])

可以看见,程序即完成了在后面添加值的过程,也报出了异常。它的执行过程如下图所示:

《流畅的Python》学习笔记(一)——数据结构_第1张图片
《流畅的Python》学习笔记(一)——数据结构_第2张图片

有以下几点需要注意:

  • 不要把可变对象放在元组里
  • 增量复制不是一个原子操作,它虽然抛出了异常,但还是完成了操作。

4.3 *= 符号

*=背后的特殊方法是__imul__

在可变序列(列表)和不可变序列(元组)中,该符号作用不同:

# 可变序列
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
2043293161600
# 不可变序列
>>> t = (1, 2, 3)
>>> id(t)
2043292853312
>>> t *= 2
>>> id(t)
2043292317344
>>> t
(1, 2, 3, 1, 2, 3)

可以看见,在列表中运用*=后,列表的ID不变,新的元素追加到列表上;在元组中运用*=后,元组的ID变化,新的元组被创建。解释器会先把原来对象中的元素先复制到新的对象里,然后再追加新的元素。

小知识

str是一个例外。程序在为str初始化内存时,会为它流出额外的可扩展空间,因此进行增量操作时,并不会涉及复制原有字符串到新位置这类操作。

5. list.sort方法和内置函数sorted

区别:

  • list.sort:就地排序列表,即不会把原列表复制一份。如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None,好让调用者知道传入的参数发生了变动,并且未产生新的对象。

  • sorted:此为内置函数,它会创建一个新的列表作为返回值。该方法接受任何形式的可迭代对象作为参数,最后返回一个列表。

相同点:

二者都有两个可选的关键字参数:

  • reverse:默认值为False,当值为True时,序列元素按字母降序输出。

  • key:一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。默认用元素自己的值来排序。

    key=len:基于字符串长度升序排序

    key=lower:忽略大小写排序

>>> fruits = ['grape', 'raspberry', 'apple', 'banana']

# 使用sorted排序
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry']
>>> fruits                                  #原列表没有变换
['grape', 'raspberry', 'apple', 'banana']   
>>> sorted(fruits, reverse=True)            #按照字母降序排序
['raspberry', 'grape', 'banana', 'apple']   
>>> sorted(fruits, key=len)                 #按照元素长度升序排序
['grape', 'apple', 'banana', 'raspberry']
>>> sorted(fruits, key=len, reverse=True)   #按照长度降序排列,长度一样时,grape和apple的相对位置不会改变
['raspberry', 'banana', 'grape', 'apple']
>>> fruits
['grape', 'raspberry', 'apple', 'banana']

# 使用.sort()排序
>>> fruits.sort()                           #原列表变换
>>> fruits
['apple', 'banana', 'grape', 'raspberry']

6. bisect模块管理排序的序列

模块包含两个函数:bisectinsort,二者都用二分查找法来在有序列表中查找或插入元素。

6.1 bisect函数

bisect(haystack,needle):在haystack里搜索needle的位置,该位置满足的条件是,把needle插入这个位置之后,haystack还能保持升序。其中,haystack必须是一个有序的序列。

例:在有序序列中使用bisect查找某个元素的插入位置

import bisect
import sys

HAYSTACK = [1,4,5,6,8,12,15,20,21,23,23,26,29,30]
NEEDLE = [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(NEEDLE):
		position = bisect_fn(HAYSTACK,needle)  # 利用特定的bisect函数计算元素应该出现的位置
		offset = position * ' |'               # 利用该位置计算需要几个分隔符
		print(ROW_FMT.format(needle, position, offset))    # 把元素及其应该出现的位置打印出来
		
if __name__=='__main__':
	
	if sys.argv[-1] == 'left':    # 根据命令上最后一个参数来选用bisect函数
		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)

执行结果:

DEMO: bisect_right
haystack ->  1  4  5  6  8 12 15 20 21 23 23 26 29 30
31 @ 14   | | | | | | | | | | | | | |31
30 @ 14   | | | | | | | | | | | | | |30
29 @ 13   | | | | | | | | | | | | |29
23 @ 11   | | | | | | | | | | |23
22 @  9   | | | | | | | | |22
10 @  5   | | | | |10
 8 @  5   | | | | |8
 5 @  3   | | |5
 2 @  1   |2
 1 @  1   |1
 0 @  0  0

bisect的两个可选参数:

  • lo: 默认值为0

  • hi: 默认值是序列长度

    可用这两个参数来缩小搜寻的范围

bisect_left和bisect_right:

  • bisect_left:返回的插入位置是原序列中跟被插入元素相等的元素的位置,即新元素会被放置于与它相等的元素前面
  • bisect_right:bisect函数的别名,返回的是跟它相等的元素之后的位置

利用bisect函数建立一个用数字作为索引的查询表格:

def grade(score, breakpoints=[60,70,80,90], grade='FDCBA'):
...     i = bisect.bisect(breakpoints,score)
...     return grades[i]
...
>>> [grade(score) for score in [33,99,77,70,89,90,100]]
['F', 'A', 'C', 'C', 'B', 'A', 'A']

该实例利用bisect函数得到分数在breakpoint的哪个位置上,再利用该位置,得出相应的等级。

6.2 bisect.insort

insort(seq, item):把变量item插入到序列seq中,并保持seq的升序序列。

import bisect
import random

size = 7
random.seed(1729)

my_list = []
for i in range(size):
	new_item = random.randrange(size*2)
	bisect.insort(my_list, new_item)
	print('%2d ->' % new_item, my_list)
-----------------------------------------------------
10 -> [10]
 0 -> [0, 10]
 6 -> [0, 6, 10]
 8 -> [0, 6, 8, 10]
 7 -> [0, 6, 7, 8, 10]
 2 -> [0, 2, 6, 7, 8, 10]
10 -> [0, 2, 6, 7, 8, 10, 10]

insort也可使用lo和hi两个可选参数控制查找范围。他也有个变体为insert_left,变体背后使用的是bisect_left

7. 替换列表的数据结构

如:

  • 要存放100万个浮点数时,选用数组(array)。因为数组背后存的不是float对象,而是数字的机器翻译,即字节表述。
  • 要频繁地对序列做先进先出的操作,选用双端队列(deque)
  • 包含操作较高(检查一个元素是否出现在一个集合当中),选用集合(set)

7.1 数组(Array)

创建数组需要一个类型码,这个类型码用来表示在底层的C语言应该存放怎样的数据类型。

Type code C Type Minimum size in bytes
‘b’ signed integer 1
‘B’ unsigned integer 1
‘u’ Unicode character 2
‘h’ signed integer 2
‘H’ unsigned integer 2
‘i’ signed integer 2
‘I’ unsigned integer 2
‘l’ signed integer 4
‘L’ unsigned integer 4
‘q’ signed integer 8
‘Q’ unsigned integer 8
‘f’ floating point 4
‘d’ floating point 8
# 一个浮点数组的创建、存入文件和从文件读取的过程
>>> from array import array
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7))) #利用一个可迭代对象建立一个双精度浮点数组(类型码为'd')
>>> floats[-1]     # 查看数组的最后一个元素
0.20972132845765767

>>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp)     # 把数组存入一个二进制文件里
>>> fp.close()

>>> floats2 = array('d')  # 新建一个双精度浮点空数组
>>> fp = open('floats.bin', 'rb')   
>>> floats2.fromfile(fp, 10**7)       # 把100万个浮点数从二进制文件里读取出来
>>> fp.close()

>>> floats2[-1]
0.20972132845765767
>>> floats2 == floats
True
  • array.tofile(f):把数组中所有的元素以机器值的形式写入一个文件f
  • array.fromfile(f, n):将二进制文件f内含有机器值读出来添加到尾部,最多添加n项

小知识

从Python3.4开始,数组类型不再支持诸如list.sort()这种就地排序方法。要给数组排序的话,得用sorted函数新建一个数组:

a = array.array(a.typecode, sorted(a))

7.2 内存视图(Memoryview)

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

memoryview.cast:能用不同的方法读写同一块内存数据,而且内容字节不会随意移动。

# 通过改变数组中的一个字节来更新数组里的某个元素的值
>>> import array
>>> numbers = array.array('h', [-2,-1,0,1,2])   # 利用含有5个短整型有符号整数的数组创建一个memoryview
>>> memv = memoryview(numbers)
>>> len(memv)
5
>>> memv[0]     # memoryview里的五个元素和数组里的没有区别
-2
>>> memv_oct = memv.cast('B')    # 把memv里的内容转为'B'类型,即无符号字符
>>> memv_oct.tolist()            # 以列表形式查看memv_oct的内容
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4        # 把位于位置5的字节赋值为4
>>> numbers
array('h', [-2, -1, 1024, 1, 2])

把占两个字节的整数的高位字节改成了4,原来0是表示为 0000 0000 0000 0000,高位改为4之后,变为:0000 0000 0010 0000,按照从右往左看,该值即为1024。

7.3 NumPy和SciPy

**NumPy:**实现了多为同质数组和矩阵,这些数据结构不但能够处理数字,还能存放其他由用户定义的记录。通过NumPy,用户能对这些 数据结构里的元素进行高效地操作。

**SciPy:**基于NumPy的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。

# 对numpy.ndarray的行列进行基本操作
>>> import numpy
>>> a = numpy.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape       # 查看数组维度,显示它是一维的,有12个元素的数组
(12,)
>>> a.shape = 3,4   # 把数组变成3行4列的二维数组
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> a[2]
array([ 8,  9, 10, 11])
>>> a[2,1]
9
>>> a[:,1]
array([1, 5, 9])
>>> a.transpose()     # 交换数组的行和列
array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])
# 对numpy.ndarray中的元素进行抽象的读取、保存和其他操作
>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt')  # 从文本文件中读取100万个浮点数
>>> floats[-3:]       # 利用序列切片来读取其中的最后三个数
array([3016362.69195522,  535281.10514262, 4566560.44373946])

>>> floats *= .5      # 将数组里的数都诚意0.5
>>> floats[-3:]
array([1508181.34597761,  267640.55257131, 2283280.22186973])

>>> from time import perf_counter as pc    # 导入精度和性能都比较高的计时器
>>> t0 = pc(); floats /= 3; pc() - t0      # 计算将100万个数都除以3所耗费的时间
0.03690556308299495

>>> numpy.save('floats-10M',floats)        # 把数组存入后缀为.npy的二进制文件
>>> floats2 = numpy.load('floats-10M.npy','r+')   # 将上面的数据导入另一个数组里,这次load方法使用内存映射的机制
>>> floats2 *= 6
>>> floats2[-3:]
memmap([3016362.69195522,  535281.10514262, 4566560.44373946])

内存映射:使得在内存不足的情况下仍然可以对数组做切片

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

collections.deque类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。如果想要有一种数据类型来存放“最近用到的几个元素”,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)

>>> 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)   # 对一个已满的队列进行头部添加时,尾部的元素会被删除
>>> 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)

.rotate(n):队列的旋转操作接受一个参数n,

  • 当n > 0时,队列的最右边的n个元素会被移动到队列的左边;
  • 当n < 0时,最左边的n个元素会被移动到右边

.extendleft(iter):会把迭代器里的元素逐个添加到双向队列的左边,因此迭代器里的元素会逆序出现在队列里

小知识

队列中的appendpopleft都是原子操作,也就是说deque可以再多线程程序中安全地当做先进先出的队列使用,而使用者不需要担心资源锁的问题。

Python中的其他队列:

  • queue

    提供了同步类 QueueLifoQueuePriorityQueue,不同的线程可以利用这些数据类型来交换信息。

    三者都有可选参数maxsize,接受整数来限定队列大小

    在队列满时,不会扔掉旧元素,而是被锁住,直到另外的线程溢出了某个元素而腾出了位置

    很适合用来控制活跃线程的数量

  • multiprocessing

    实现了自己的Queue,用于进程间通信

    含有multiprocessng.JoinableQueue类型,可以使管理任务变得更方便

  • asyncio

    Python3.4提供的包,包含QueueLifoQueuePriorityQueueJoinableQueue,为异步编程里的任务管理器提供了遍历

  • heapq

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


你可能感兴趣的:(Python,python,数据结构)