Chapter 2
序列组成的数组
2.1 序列分类
按存放数据的类型是否一致,序列分为容器序列和扁平序列。
容器序列存放数据的类型可以不同,如:list, tuple, collections.deque;
扁平序列存放灵气的类型必需一致,如:str, bytes, bytearray, memoryview, array.array。
另外,序列数型也可以按能否被修改,将序列分为:可变序列(MutableSequence) 和 不可变序列(Sequence)。
除了字面意思的可变和不可变的不同外,最根本的区别是继承的父类不同。可变序列继承自不可变序列(即不可变序列是可变序列的父类)。
2.2 列表推导和生成器表达式
列表推导(list comprehension, listcomps) 是构建列表(list)的快捷方式,而 生成器表达式(generator expression, genexps) 则可以用来创建其他任何类型的序列。
2.2.1 列表推导的可读性
pass # 列表推导
symbols = '$¢£¥€¤'
# for 循环创建列表
codes = []
for symbol in symbols:
codes.append(ord(symbol))
# print(codes)
# 列表推导创建列表
codes_ = [ord(symbol) for symbol in symbols]
# print(codes_)
列表推导使用准则:
- 列表推导的代码超过两行,则考虑使用 for 循环;
- 列表推导只能生成列表。如果想生成其他类型的序列,则需要用到生成器表达式。
2.2.2 列表推导同 filter 和 map 的比较
# 例 2-3:超越 ASCII 的数字
# 列表推导完成
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
# print(beyond_ascii)
# map, filter 组合完成
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
# print(beyond_ascii)
2.2.3 笛卡尔积
# 列表推导计算笛卡尔积
# 例 2-4:3 种不同尺寸的 T 恤衫,每个尺寸都有 2 个颜色,将 6 种不同的组合放在列表中
# 列表推导完成
colors = ["black", "white"]
sizes = ["S", "M", "L"]
# tshirts = [(color, size) for color in colors for size in sizes] # flag_1
tshirts = [(color, size) for size in sizes for color in colors] # flag_2
# print(tshirts)
# for 循环完成
tshirts = []
for color in colors:
for size in sizes:
tshirts.append((color, size))
# print(tshirts)
注释 flag_1 和 flag_2 的 for 循环中 size, color 的位置不同,tshirt 的分组方式不同。
flag_1 中, color 是外循环,size 是内循环, 以 color 分组;
flag_2 中, size 是外循环,color 是内循环, 以 size 分组。
flag_1 输出结果:
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
flag_2 输出结果:
('black', 'S')
('white', 'S')
('black', 'M')
('white', 'M')
('black', 'L')
('white', 'L')
2.2.4 生成器表达式
生成器表达式 与 列表推导 一样,只是将 列表推导中 的 方括号 改成 圆括号。
# 例 2-5
tuple_ = tuple(ord(symbol) for symbol in symbols) # one
# print(tuple_)
array_ = array.array("I", (ord(symbol) for symbol in symbols)) # two
# print(array_)
生成器表达式 是一个函数的唯一参数时,不需要用括号括起来,如注释 one;
array 函数有两个参数,此时 生成器表达式 需要用括号括起来,如注释 two。
# 例 2-6:生成器表达式计算笛卡尔积
tshirts = ("%s %s" % (c, s) for c in colors for s in sizes)
print(tshirts) # 输出 tshirts 的类型和地址,<generator object <genexpr> at 0x000002597C146820>
for tshirt in tshirts:
print(tshirt)
2.3 元组
元组的特点:
- 元组为不可变列表;
- 元组可以用于没有字段的记录。
2.3.1 元组拆包
把元组中的所有元素分别赋值到不同的变量,称为元组拆包。元组拆包可以应用到任何可以迭代的对象上。元组中有 N 个元素,则需要有 N 个变量。
# 元组拆包形式之平行赋值
lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates # 元组拆包
print(latitude)
print(longitude)
平行赋值:把一个可迭代对象里的元素,一并赋值到由对应的变量组成的元组中。
平行赋值可以不使用中间变量交换两个变量的值
# 不用中间变量交换两个变量的值
a = 1
b = 2
a, b = b, a
# print(a)
# print(b)
* 与 _(下划线) 的使用
# * 号运算符把一个可迭代对象拆开作为函数的参数
s = divmod(20, 8) # divmod 模运算
print(s)
t = (20, 8)
s1 = divmod(*t) #
print(s1)
quotient, remainder = divmod(*t)
print(quotient)
print(remainder)
# os.path.split() 返回 (path, filename)
_, filename = os.path.split("/home/breky/.ssh/idrsa.pub")
print(filename)
"""
元组拆包后,不需要的元素可以用占位符(_)代替。上例
用 * 号处理剩下的元素。下例
"""
aa, bb, *rest = range(5)
print(aa, bb, rest)
aaa, *rest1, bbb = range(5) # * 号可以用在任意位置,但只能出现一次
print(aaa, rest1, bbb)
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('city', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas:
print(fmt.format(name, latitude, longitude))
2.3.2 具名元组
顾名思义,带名的元组称为具名元组。
collections.namedtuple 可以用来构建一个带名的元组和一个有名字的类。
from collections import namedtuple
City = namedtuple("City", "name country population coordinates") # 1
tokyo = City("Tokyo", "JP", 36.933, (35.689722, 139.1667)) # 2
# 以下为输出语名,需在终端中运行。如需在 IDE 中运行,需要使用 print。
tokyo
tokyo.population # 3
tokyo.coordinates
tokyo[1]
# 1: 具名元组创建语法。创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段(类的属性)的名字。各个字段可以是由数个字符串组成的可迭代对象,也可以是由空格分隔开的各字段名组成的字符串。
# 2: 对具名元组(类)City 的各字段名(属性)赋值。数据要以一串参数的形式传入到构造函数中,只接受单一的可迭代的对象。
# 2 中的 City 是 # 1 中赋值语句(=)左边的 City 还是右边的 City?
from collections import namedtuple
Person = namedtuple("People", "name age sex")
Jason = Person("Jason", 18, "M")
Jason # 输出 People(name='Jason', age=18, sex='M')
Jason.name # 输出 'Jason'
People # 输出 NameError: name 'People' is not defined
People.name # 输出 NameError: name 'People' is not defined
Peterson = People("Peterson", 21, "M") # 输出 NameError: name 'People' is not defined
通过实验可以看出:具名元组的定义语句中 (Person = namedtuple("People", "name age sex")),赋值语句(=)左侧的变量名(Person)为定义具名元组时的类名,右侧的第一个参数(People)不知道是个什么东西,应该是元组(("name", "age", "sex"))的名,可能通常这两个值(赋值语句左侧的变量名和右侧的第一个参数)应该相同。
# 3: 可以通过字段名(tokyo.population)或者位置(tokyo[1])来获取一个字段的信息。
具名元组常的其他属性和方法
City._fields # 1
LatLong = namedtuple("LatLong", "lat long")
delhi_data = ("Delhi NCR", "IN", 21.935, LatLong(28.613889, 77.208889))
delhi = City._make(delhi_data) # 2
delhi._asdict() # 3
for key, value in delhi._asdict().items():
print(key + ":", value)
# 1: _fields 属性是一个包含这个类所有字段名称的元组
# 2: _make() 接受一个可迭代对象来生成一个实例,与 City(*delhi_data) 一样
# 3: _asdict() 把元组名以 collections.OrderedDict 的形式返回。可以利用它来把元组里的信息友好的显示出来。
2.4 切片
高级切片形式的用法。
2.4.1 切片和区间忽略最后一个元素
切片和区间操作不包含区间范围的最后一个元素,其他语言中称之为左闭右开。这样的如处是:
- 当只有最后一个位置信息时,可以快速看出切片和区间里有几个元素,如
range(3)和my_list[:3]都返回3个元素。 - 当起止位置信息都可见时,可以快速计算出切片和区间的长度,用后一个数减去第一个下标(
stop - start)即可。 - 可以利用凭意一个下标来把序列分割成不重叠的两部分,只要写成
my_list[:x]和my_list[x:]就可以了
l = [10, 20, 30, 40, 50, 60]
l[:2] # 在下标 2 的地方分割,输出 [10, 20]
l[2:] # 输出 [30, 40. 50, 60]
l[:3] # 在下标 3 的地方分割,输出 [10, 20, 30]
l[3:] # 输出 [40, 50, 60]
2.4.2 对象切片
s[a:b:c] 的意思是对 s 在 a 和 b 之间以 c 为间隔取值。c 值也可以为负值,负值意味着反向取值。
s = 'bicycle'
s[::3] # 输出 'bye'
s[::-1] # 输出 'elcycib'
s[::-2] # 输出 'eccb'
例 2-4-1: 纯文本文件形式的收据,以一行字符串的形式解析
invoice = """
... 0.....6................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... """
SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split('\n')[2:]
for item in line_items:
print(item[UNIT_PRICE], item[DESCRIPTION])
# 输出
# $17.5 09 Pimoroni PiBrella
# $4.9 89 6mm Tactile Switch x20
# $28.0 10 Panavise Jr. - PV-201
# $34.9 01 PiTFT Mini Kit 320x240
2.4.3 多维切片和省略
[] 运算符里可以使用逗号分开的多个索引或者是切片。
2.4.4 给切片赋值
把切片放在赋值语句的左边,或把它作为 del 操作的对象,就可以对序列进行嫁接、切除或修改操作。
l = list(range(10))
l # 输出 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 1
l[2:5] = [20, 30] # 2
l # 输出 [0, 1, 20, 30, 5, 6, 7, 8, 9] # 3
del l[5:7] # 4
l # 输出 [0, 1, 20, 30, 5, 8, 9] # 5
l[3::2] = [11, 22] # 6
l # 输出 [0, 1, 20, 11, 5, 22, 9] # 7
l[2:5] = 100 # 输出以下内容 # 8
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
l[2:5] = [100] # 9
l # 输出 [0, 1, 100, 22, 9] # 10
- 经过
# 2的切片赋值操作,# 3的输出结果与# 1的结果对比,除了下标2和3的值不两同外,l的长度也不同。 分析一下# 2操作,赋值符号左边l[2:5]取的是 下标2、3和4的值,一共3个值,赋值符号右边[20, 30]是两个值,所以# 2的操作把l列表中下标为2和3的值分别改为20和30,下标为4的值直接删除,后边的值顺序前移,整个l的长度减1。 # 8操作报错原因。# 8取出的是l的子序列,共3个值,不能用整数赋值。应该用相同的序列赋值,如# 9。
2.5 序列操作之 + 和 *
Python 中 + 操作两侧的序列相同,在拼接过程中不会被修改,而是会新建一个序列来作为拼接结果,* 操作也一样。
l = [1, 2, 3]
l + [1, 2, 3] # 输出:[1, 2, 3, 1, 2, 3]
l # 输出: [1, 2, 3]
l * 5 # 输出: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
l # 输出:[1, 2, 3]
如上代码段,+ 和 * 都遵循这个规律,不修改原有的操作对像,而是构建一个全新的序列。
需要注意的是:
使用 * 号对二维列表初始化时,产生二维列表中的 n 个一维列表,是指向了同一对象的引用的列表。即更改这 n 个一维列表中的某个值,其他 n-1 个一维列表中对应的值也会一起改为相同的值。如下代码段显示。
weird_board = [["_"] * 3] * 3
weird_board # 输出: [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2] = "O"
weird_board # 输出:[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
如上代码段,本意是想将 weird_board 的第二行第三列的元素改为 O(weird_board[1][2] = "O"),但实际输出却是所有行的第三个元素都改为了 O( [['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']])。可以使用列表推导的方式始初化二维列表中的一维列表,来达到每行列表均不相同的目的。如下代码段显示。
board = [["_"] * 3 for i in range(3)]
board # 输出: [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = "X"
board # 输出: [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
2.6 序列操作之增量赋值
增量赋值运算符:+=、*=。
+=:调用序列的 __iadd__(就地加法),如果没有则调用 __add__。
a += b
上述代码,如果序列 a 有 __iadd__ 方法,则调用。同时将结果赋值给 a ,与 a.extend(b) 一样。如果序列 a 没有 __iadd__ 方法,a += b 就变成跟 a = a + b 一 样。但是,不可变序列不支持这个操作。
l = [1, 2, 3]
id(l) # 输出 l 的存储地址:2159542330752 # 1
l *= 2
l # 输出: [1, 2, 3, 1, 2, 3]
id(l) # 输出 l 的存储地址:2159542330752 # 2
t = (1, 2, 3)
id(t) # 输出 t 的存储地址:2159542259392 # 3
t # 输出:(1, 2, 3)
t *= 2 # 输出 t 的存储地址: 2159541382976 # 4
id(t)
t # 输出:(1, 2, 3, 1, 2, 3)
可变序列 l,*= 操作前后,l 的值不同,对比 # 1 和 # 2 的输出结果发现两个值相等,说明是同一变量。
不可变序列 t, #= 操作前后, 虽然 t 的值不同,但是,对比 # 3 和 # 4 的输出结果发现两个值不相等,说明不是同一变量,前一个 t 被后一个 t 覆盖,丢失。因为每次的拼接操作都会有一个新对象产生,所以效率很低。
2.7 list.sort 方法和内置函数 sorted
区别:
list.sort方法就地排序列表,即在原列表中排序。sorted内置函数,会新建一个列表作为返回值,需要另一个变量存放,原表不发生改变。
相同之处: 两者都有两个可选关键字参数:reverse、key
reverse:True,降序排列;False,升序排列。key:一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。如:key=str.lower,排序时忽略大小写;key=len,按字符串长度排序。默认是恒等函数(identity function),即按元素本身排序。
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']