文章目录
- 2.1 内置序列类型
- 按存放数据类型分类
- 按是否能被修改来分类
- 2.2 列表推导和生成器表达式
- 2.2.1 列表推导和可读性
- 代码2-1 把一个字符串变成Unicode码位的列表
- 2.2.3 笛卡尔积
- 代码2-4 使用列表推导计算笛卡尔积
- 2.2.4 生成器表达式
- 代码2-5 使用生成器表达式
- 2.3 元组
- 2.3.1 元组和记录
- 代码2-7 把元组用作记录
- 2.3.2 元组拆包
- 代码2-6 元组拆包
- 2.3.3 嵌套元组拆包
- 2.3.4 具名元组
- 代码2-8 具名元组
- 2.3.5 作为不可变列表的元组
- 2.4 切片
- 2.4.1 为什么切片和区间会忽略最后一个元素
- 2.4.2 对对象进行切片
- 代码2-9 切片示例
- 2.4.4 给切片赋值
- 2.5 对序列使用 + 和 *
- 代码2-10 + 和 * 示例
- 代码2-11 建立由列表组成的列表
- 2.6 序列的增量赋值
- 一个谜题
- 2.7 list.sort方法和内置函数sorted
- 代码2-11 排序示例
- 2.8 用bisect来管理已排序的序列
- 2.8.1 用bisect来搜索
- 用bisect.insort插入新元素
- 2.9 当列表不是首选时
- 2.9.1 数组
- 2.9.2 内存视图
- 2.9.4 双向队列和其他形式的队列
- 声明
2.1 内置序列类型
按存放数据类型分类
容器序列——存放的是它们所包含的任意类型的对象的引用,有:list(列表)、tuple (元组)和 collections.deque (双向列表)。
扁平序列——存放具体的值,显然,扁平序列更加紧凑,但是其仅能存放如字符、字节、数值等基础数据类型,有:str(字符串)、bytes(字节)、bytearray(字节数组)、memoryview (内存查看对象)和 array.array(数组)。
按是否能被修改来分类
可变序列
list、bytearray、array.array、collections.deque 和 memoryview。
不可变序列
tuple、str 和 bytes。
2.2 列表推导和生成器表达式
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。
2.2.1 列表推导和可读性
代码2-1 把一个字符串变成Unicode码位的列表
#《流畅的python》p59
#常规写法
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
#ord()函数以一个字符为参数,返回它对应的ascii编码或Unicode编码
... codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
#列表推导
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]
列表推导具有更好的可读性,因为其代码的功能能从字面上就轻松看出来。
使用列表推导的通用原则是:只用列表推导来创建新的列表,并且尽量保持简短。
由于列表推导在py3中有自己的局部作用域,其变量泄露问题得以解决。
2.2.3 笛卡尔积
代码2-4 使用列表推导计算笛卡尔积
#《流畅的python》p62
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
('white', 'M'), ('white', 'L')]
>>> for color in colors:
... for size in sizes:
... print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes
... for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
('black', 'L'), ('white', 'L')]
2.2.4 生成器表达式
迭代器与生成器的区别请参考:这篇文章
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存。
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
注意,生成器表达式不能直接打印,直接打印结果为<generator object <genexpr> at 0x000001DD8A6D6938>,必须实例化或者使用for循环查看。
代码2-5 使用生成器表达式
# 《流畅的python》p63
# 用生成器表达式初始化元组和数组
>>> 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])
#➊ 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
#➋ array 的构造方法需要两个参数,因此括号是必需的。
# 用生成器表达式计算笛卡尔积
>>> 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 循环运行时才生成一个组合。
#如果要计算两个各有 1000 个元素的列表的笛卡儿积,生成器表达式就可以帮忙省掉运行 for 循环的开销。
2.3 元组
元组不仅仅是不可变的列表,其还可以作为没有字段名的记录。
2.3.1 元组和记录
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。
这里的位置信息按我的理解应该是元素出现位置,如第一个元素,第二个元素等。
如果在任何的表达式里我们在元组内对元素排序,这些元素所携带的信息就会丢失,因为这些信息是跟它们的位置有关的。
代码2-7 把元组用作记录
#《流畅的python》p64
>>> lax_coordinates = (33.9425, -118.408056) ➊
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014) ➋
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ➌
... ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids): ➍
... print('%s/%s' % passport) ➎
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids: ➏
... print(country)
...
USA
BRA
ESP
#❶ 洛杉矶国际机场的经纬度。
#❷ 东京市的一些信息:市名、年份、人口(单位:百万)、人口变化(单位:百分比)和面积(单位:平方千
#米)。
#❸ 一个元组列表,元组的形式为 (country_code, passport_number)。
#❹ 在迭代的过程中,passport 变量被绑定到每个元组上。
#❺ % 格式运算符能被匹配到对应的元组元素上。
#❻ for 循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因为元组中第二个元素对我们没有什么
#用,所以它赋值给“_”占位符。
2.3.2 元组拆包
元组拆包有两种方式,其一是平行赋值,其二是借助占位符进行拆包。
代码2-6 元组拆包
#《流畅的python》p65
# 平行赋值示例
>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # 元组拆包
>>> latitude
33.9425
>>> longitude
-118.408056
# 利用 * 运算符把一个可迭代对象拆开作为函数的参数
# *参数作为函数调用时表示将元组中的数据逐个传入函数中
# divmod() 函数把除数和余数运算结果结合起来,返回一个包含商和余数的元组(a // b, a % b)
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4)
# 借助占位符进行拆包
# os.path.split()函数返回以路径和最后一个文件名组成的元组 (path, last_part)
>>> import os
>>> _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
>>> filename
'idrsa.pub'
# 用*来处理剩下的元素
>>> 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, [])
# * 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)
2.3.3 嵌套元组拆包
2.3.4 具名元组
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类,这个类所消耗的内存与元组是一致的。
在系列文章最开始创建的Card类就是一个具名元组(详见此处)
代码2-8 具名元组
>>> 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
# _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)
# 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟City(*delhi_data) 是一样的。
>>> delhi._asdict()
# _asdict() 把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。
# OrderedDict 有序字典,在下一章会着重介绍。
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)
>>>
2.3.5 作为不可变列表的元组
2.4 切片
这一节仅讨论高级切片的用法。
2.4.1 为什么切片和区间会忽略最后一个元素
当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3] 都返回 3 个元素。
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成my_list[:x] 和 my_list[x:] 就可以了。
2.4.2 对对象进行切片
对对象进行切片所涉及到的特殊方法为__ getitem__。
切片可以传入三个参数,start,stop,step(可选)。其中,步长为负时表示反向取值。
代码2-9 切片示例
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
2.4.4 给切片赋值
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [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]
2.5 对序列使用 + 和 *
Python 程序员会默认序列是支持 + 和 * 操作的。
+ 和 * 均不改变原有操作对象,而是构建一个全新的序列。
代码2-10 + 和 * 示例
>>> s = [4, 5, 6]
>>> l = [1, 2, 3]
>>> l + s
[1, 2, 3, 4, 5, 6]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
注意:如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话,比如,你想用my_list = [ [\ ]] * 3 来初始化一个由列表组成的列表,但是你得到的列表里包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。也就是说当你修改一处时所有地方都会修改。这肯定不是你想要的。深层次的原因请看这篇:参考文章
代码2-11 建立由列表组成的列表
# 正确方法一,采用循环构建三个一样的对象
>>> board = [['_'] * 3 for i in range(3)] #使用列表推导
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
# 正确方法二
>>> board = []
>>> for i in range(3):
... row=['_'] * 3
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]
# 错误方法一
>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O'
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
# 错误方法二
row=['_'] * 3
board = []
for i in range(3):
board.append(row)
2.6 序列的增量赋值
在此处我们只讨论+=,对于*=和其他增量运算符可平行推广。
+=背后的特殊方法是__ iadd__,当__ iadd__没有实现该方法时,python就会退而调用__ add__。
如果 a 实现了 __ iadd__ 方法,就会调用这个方法。同时对可变序列(例如list、bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b)一样。但是如果 a 没有实现 __ iadd__ 的话,a += b 这个表达式的效果就变得跟 a = a + b 一样了:首先计算 a + b,得到一个新的对象,然后赋值给 a。也就是说,在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现 __ iadd__ 这个方法。
一个谜题
答案是d,即ab均正确(在2.7、3.4、3.6中均得到同样的结果)
那么发生了什么?为什么明明已经抛出了TypeError元组内的列表仍改变了呢?
- 不要把可变对象放在元组里面。
- 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
2.7 list.sort方法和内置函数sorted
列表排序常见的方式有两种,
- list.sort方法会就地排序列表,返回None,python一般通过返回None来提醒这种方法是对对象的就地改动
- 内置函数sorted将会接受任何形式的可迭代对象作为参数,并新建一个列表作为返回值
无论是list.sort方法还是sorted函数都有两个可选的关键字参数,
- reverse :True 降序输出 False(默认)升序
- key: 一个排序所依据的函数,该函数将会作用到每一个需要排序的元素上,并按照作用结果排序,默认值为恒等函数(id)
代码2-11 排序示例
>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> 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)
['raspberry', 'banana', 'grape', 'apple']
>>> fruits
['grape', 'raspberry', 'apple', 'banana']
>>> fruits.sort()
>>> fruits
['apple', 'banana', 'grape', 'raspberry']
2.8 用bisect来管理已排序的序列
2.8.1 用bisect来搜索
bisect(haystack, needle) 在 haystack(干草垛)里搜索 needle(针)的位置,使得将 needle 插入这个位置之后,haystack 还能保持升序。其中传入的haystack必须是一个有序的数列,返回插入的needle的index(索引)。
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}'
#格式化字符串函数format()采用{}和:代替%
#{position:format} 第一个参数表示该处采用format传入的第几个参数,format表示该处采用何种格式
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)
可以从以下两方面定制bisect函数
- 可选参数 lo 和 hi 用以缩小查找的范围,其中 lo 的默认值为0, hi 的默认值是序列的长度。
- bisect 函数其实是 bisect_right 函数的别名,后者还有个姊妹函数叫bisect_left。两者的分别是前者needle的插入位置为右侧而后者为左侧。
用bisect.insort插入新元素
bisect.insort(seq, item) 把变量 item 插入到序列 seq 中,并能保持 seq 的升序顺序。其背后使用的是bisect函数。insort 跟 bisect 一样,有 lo 和 hi 两个可选参数用来控制查找的范围。它也有个变体叫 insort_left,这个变体在背后用的是 bisect_left。
2.9 当列表不是首选时
有些时候列表并不是处理问题最好的数据结构,这是我们将使用一些其他的异于列表的数据结构。
2.9.1 数组
当我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。其原因是数组储存的并非对象,而是数字的机器翻译,也就是字节表达。
Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。下图展示了array的类型码。
python不允许在数组中存放不符合类型码的数据,如此,在处理大量数字时可以节省非常多的空间,使得性能得到提高。
>>> from array import array
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))
#创建储存了10^7个double类型数据的数组
>>> floats[-1]
0.07802343889111107
>>> 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.07802343889111107
>>> floats2 == floats
True
从python3.4开始,数组不再支持诸如 list.sort() 这种就地排序方法。要给数组排序的话,得用 sorted 函数新建一个数组:a = array.array(a.typecode, sorted(a))
2.9.2 内存视图
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。
memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)
>>> len(memv)
5
>>> memv[0]
-2
>>> memv_oct = memv.cast('B')#把memv里的内容转换成'B'类型
>>> memv_oct.tolist() #以list的方式查看memv_oct的内容
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4
>>> numbers
array('h', [-2, -1, 1024, 1, 2])
2.9.4 双向队列和其他形式的队列
collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,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)
声明
本文来自《流畅的python》以及笔者自己的思考,如有错误,欢迎指正。