Skip to content

数据结构

内置序列类型概览

Python 标准库用 C 实现了丰富的序列类型,列举如下

容器序列:list, tuple, 和 collection.deque 这些序列能存放不同类型的数据
扁平序列:str, bytes, bytearray, memoryview 和 array.array,这类序列只能容纳一种类型

容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。
换句话说,扁平序列其实是一段连续的内存空间。
由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型

序列类型还能按照能否被修改来分类

可变序列:list, bytearray, array.array, collection.deque 和 memoryview
不可变序列:tuple, str 和 bytes

列表推导和生成器表达式

Python 会忽略代码里 [], {} 和 () 中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以忽略不太好看的续行符 \

虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存

生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
print((f'{c} {s}' for c in colors for s in sizes))
for tshirt in (f'{c} {s}' for c in colors for s in sizes):
    print(tshirt)
"""
<generator object <genexpr> at 0x1017ce6d8>
black S
black M
black L
white S
white M
white L
"""

生成器表达式逐个产出元素,从来不会一次性产出一个含有 6 个 T 恤样式的的列表

元组不仅仅是不可变的列表

元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义

如果在任何的表达式里我们在元组内对元素排序,这些元素所携带的信息就会丢失,因为这些信息跟它们的位置有关的

元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用*来表示忽略多余的元素

一个很优雅的写法当属不使用中间变量交换两个变量的值:

1
b, a = a, b

还可以用*运算符把一个可迭代对象拆开作为函数的参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
print(divmod(20, 8))
t = (20, 8)
print(divmod(*t))
quotient, remainder = divmod(*t)
print(quotient, remainder)
"""
(2, 4)
(2, 4)
2 4
"""

下面是一个例子,这里元组拆包的用法则是让一个函数可以用元组的形式返回多个值,然后调用函数的代码就能轻松地接受这些返回值。比如 os.path.split() 函数就会返回以路径和最后一个文件名组成的元组 (ptah, last_part):

1
2
3
4
5
6
7
8
import os
print(os.path.split('/home/luciano/.ssh/idrsa.pub'))
_, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
print(filename)
"""
('/home/luciano/.ssh', 'idrsa.pub')
idrsa.pub
"""

在进行拆包的时候,我们不总是对元组里所有的数据都感兴趣,_ 占位符能帮助处理这种情况,上面这段代码也展示了它的用法

如果做的是国际化软件,那么_可能就不是一个理想的占位符,因为它也是 gettext.gettext 函数的常用别名,在其他情况下,_会是一个很好的占位符

除此之外,在元组拆包中使用*也可以帮我们把注意力集中在元组的部分元素上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a, b, *reset = range(5)
print(a, b, reset)
a, b, *reset = range(3)
print(a, b, reset)
a, b, *reset = range(2)
print(a, b, reset)
"""
0 1 [2, 3, 4]
0 1 [2]
0 1 []
"""
1
2
3
4
5
6
7
8
a, *body, c, d = range(5)
print(a, body, c, d)
*head, b, c, d = range(5)
print(head, b, c, d)
"""
0 [1, 2] 3 4
[0, 1] 2 3 4
"""

嵌套元组拆包

接受表达式的元组可以是嵌套的,例如 (a, b, (c, d))。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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}'
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
"""

具名元组

collection.nametuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类--这个带名字的类对调试程序有很大帮助

用 nametuple 构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为 Python 不会用 __dict__ 来存放这些实例的属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.coordinates)
print(tokyo[1])
"""
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
36.933
(35.689722, 139.691667)
JP
"""

创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以由数个字符串组成的可迭代对象,或者是由空格隔开的字段名组成的字符串

除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
print(City._fields)
LatLong = namedtuple('LatLong', 'lat long')
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.20889))
delhi = City._make(delhi_data)
print(delhi._asdict())
for key, value in delhi._asdict().items():
    print(f"{key}: {value}")
"""
('name', 'country', 'population', 'coordinates')
OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population', 21.935), ('coordinates', LatLong(lat=28.613889, long=77.20889))])
name: Delhi NCR
country: IN
population: 21.935
coordinates: LatLong(lat=28.613889, long=77.20889)
"""

除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。还有一个例外,元组没有__reversed__方法,但是这个方法只是个优化而已,reversed(my_tuple)这个用法在没有__reversed__的情况下也是合法的

切片

为什么切片和区间会忽略最后一个元素

在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语法里以 0 作为起始下标的传统。这样做带来的好处如下:
当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)my_list[:3] 都返回 3 个元素
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标 (stop - start) 即可
这样做也可以让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x]my_list[x:] 就可以了

众所周知,我们还可以用 s[a:b:c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,复制意味着反向取值

1
2
3
4
5
6
7
8
9
s = 'bicycle'
print(s[::3])
print(s[::-1])
print(s[::-2])
"""
bye
elcycib
eccb
"""

给切片赋值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
l = list(range(10))
l[2:5] = [20, 30]
print(l)
del l[5:7]
print(l)
l[3::2] = [11, 22]
print(l)
# l[2:5] = [100]
# print(l)
l[2:5] = 100
"""
[0, 1, 20, 30, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 8, 9]
[0, 1, 20, 11, 5, 22, 9]
Traceback (most recent call last):
  File "test.py", line 55, in <module>
    l[2:5] = 100
TypeError: can only assign an iterable
"""

对序列使用+和*

1
2
3
4
5
6
7
l = [1, 2, 3]
print(l * 5)
print(5 * 'abcd')
"""
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
abcdabcdabcdabcdabcd
"""

注意:如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用my_list = [[]] * 3 来初始化一个由列表组成的列表,但是你得到的列表里包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是用一个列表。这可能不是你想要的效果