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

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


前言

《流畅的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 "<stdin>", 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 "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

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

在这里插入图片描述
在这里插入图片描述

有以下几点需要注意:

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

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 codeC TypeMinimum size in bytes
‘b’signed integer1
‘B’unsigned integer1
‘u’Unicode character2
‘h’signed integer2
‘H’unsigned integer2
‘i’signed integer2
‘I’unsigned integer2
‘l’signed integer4
‘L’unsigned integer4
‘q’signed integer8
‘Q’unsigned integer8
‘f’floating point4
‘d’floating point8
# 一个浮点数组的创建、存入文件和从文件读取的过程
>>> 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方法,让用户可以把可变序列当做堆序列或者优先序列来使用


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值