Python内置序列类型
Python标准库用C语言实现了丰富的序列类型。分为如下
-
容器序列: 可存放不同类型的项,其中包括嵌套容器。示例:
list
(列表)、tuple
(元组) 和collections.deque
(队列)。 -
扁平序列:可存放一种简单类型的项。示例:
str
(字符串)、bytes
(字节) 和array.array
(数组)。
容器序列
容器序列存放的是所包含对象的引用,对象可以是任何类型。

图中展示的是一个元组和一个数组的内存简图,它们各有 3 项。灰色方块(未按比例绘制)表示 各个 Python 对象的内存标头。元组中的每一项都是引用,引用的是不同的 Python 对象,对象中还可以 存放其他 Python 对象的引用,例如那个包含两个项的列表。
扁平序列
扁平序列在自己的内存空间中存储所含内容 的值,而不是各自不同的 Python 对象

Python 中的数组整体是一个对 象,存放一个 C 语言数组,包含 3 个双精度数因此,扁平序列更加紧凑,但是只能存放原始机器值,例如字节、整数和浮点数。
任何 Python 对象在内存中都有一个包含元数据的标头。最简单的 Python 对象,例如一个 float,内存标头中有一个值字段和两个元数据字段。
-
ob_refcnt:对象的引用计数。
-
ob_type:指向对象类型的指针。
-
ob_fval:一个 C 语言 double 类型值,存放 float 的值。
在 64 位设备中,每个字段占 8 字节。假如有一个浮点数数组和一个浮点数元组,显然前者比后者更紧凑,因为数组整体是一个对象,存放各个浮点数的原始值,而元组由多个对象构成:元组自身和存放的各个float对象
另外,还可按可变性对序列类型分类。
-
可变序列 例如
list、bytearray、array.array 和 collections.deque
。 -
不可变序列 例如
tuple、str 和 bytes
。
可变序列继承不可变序列的所有方法,另外还多实现了几个方法,
列表推导式和生成器表达式
使用列表推导式(目标是列表)或生成器表达式(目标是其他序列类型)可以快速构建一个序列。使用这 两种句法写出的代码更易于理解,而且速度通常更快。
列表推导式与可读性
一个例子,基于一个字符串得到其 Unicode 的值。
简单的for循环方式
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 定义空列表
codes = []
# 循环遍历字符串,然后把每一个字符转换成Unicode后,存入codes列表中
for s in strs:
codes.append(ord(s))
# 输出转换后的列表
print(codes) # [65, 66, 67, 68, 69]
列表推导式方式
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [ord(s) for s in strs]
# 通过列表推导式得到的列表
print(codes) # [65, 66, 67, 68, 69]
可以看到,列表推导式相对于for循环的方式,更加简单,且更加能简单明了。如果读者对列表推导式语法还不熟悉的花,可以去参考如下文章:Python基础-14-列表推导式
列表推导式写法说明:Python 会忽略 []、{} 和 () 内部的换行。因此,列表、列表推导式、元组、字典等结构完全可以分成几行来写,无须使用续行转义符 。如果不小心在续行转义符后面多输入一个空格,那反而不起作 用。另外,使用这 3 种括号定义字面量时,项与项之间使用逗号分隔,末尾的逗号将被忽略。因此, 跨多行定义列表字面量时,最好在最后一项后面添加一个逗号。这样不仅能方便其他程序员为列表添 加更多项,还可以减少代码差异给阅读带来的干扰。
上述推导式写法也可以这样写
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [
ord(s)
for s in strs
]
# 通过列表推导式得到的列表
print(codes) # [65, 66, 67, 68, 69]
注意:虽然列表推导式功能强大,但也不不是必须要使用,且不能滥用列表推导式。相反,for循环的功能也非常强大且容易列表;代码不仅仅写给机器看,还要写给自己和项目协作者看。
Python 3 中的列表推导式、生成器表达式,以及类似的集合推导式和字典推导式,for 子句中赋值的 变量在局部作用域内。代码如下
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [
ord(s)
for s in strs
]
# 通过列表推导式得到的列表
print(codes) # [65, 66, 67, 68, 69]
# 如下代码会报错,这里要使用列表推导式中的局部变量s,就会给出提示 NameError: name 's' is not defined
print(s)
然而,使用“海象运算符(即一个变量名后跟一个表达式或者一个值,这个和赋值运算符 = 类似,可以看作是一种新的赋值运算符。)” :=
赋值的变量在推导式或生成器表达式返回后依然可以访问,这与函数内的局部变量行为不同。根据“PEP 572—Assignment Expressions”,:=
运算符赋值的变量,其作用域限定在函数内,除非目标变量使用global
或nonlocal
声明。
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 通过列表推导式。直接生成一个新的列表
# 如果这部分代码不熟悉,可以去参考 Python基础-14-列表推导式
codes = [
# 这里使用海象运算符
lastValue := ord(s)
for s in strs
]
# 通过列表推导式得到的列表
print(codes) # [65, 66, 67, 68, 69]
# 可以使用海象运算符赋值的变量
print(lastValue) # 69
列表推导式与 map 和 filter
回顾一下map和filter的用法
map() 会根据提供的函数对指定序列做映射。第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。
# 定义一个方法,实现平方计算
def square(x):
return x ** 2
# map方法,把一个函数应用到指定可迭代的序列上。然后形成新的序列
result1 = map(square, [1, 2, 3, 4]) # 这里在Python3上,输出对象信息 <map object at 0x000001C41496BB80>
# 把结果转换成list后,在输出可以看到内容
print(list(result1)) # [1, 4, 9, 16]
# 还可以直接通过lambda表达式来代替第一个参数
print(list(map(lambda x: x ** 2, [1, 2, 3, 4]))) # [1, 4, 9, 16]
filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判断,然后返回 True 或 False,最后将返回 True 的元素放到新列表中。
注意: Python2.7 返回列表,Python3.x 返回迭代器对象
# 定义一个可以用于过滤的方法:返回奇数
def is_odd(n):
return n % 2 == 1
newline = filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(list(newline)) # [1, 3, 5, 7, 9]
列表推导式涵盖 map 和 filter 两个函数的功能,写出的代码不像 Python 的 lambda 表达式那样晦涩难懂。
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
# 实现过滤filter的功能,把字符串转换成Unicode时,判断大于67的才存入到新序列newline中
newline = [ord(s) for s in strs if ord(s) > 67]
print(newline) # [68, 69]
# 实现上述功能,仔细读代码
# filter是过滤,大于67的
# map是映射,映射的方法是ord,而数据是strs
# 最后把结果通过list方法转换成list列表
newline2 = list(filter(lambda s: s > 67, map(ord, strs)))
print(newline2) # [68, 69]
生成器表达式
虽然列表推导式也可以生成元组、数组或其他类型的序列,但是生成器表达式占用的内存更少,因为生成器表达式使用迭代器协议逐个产出项,而不是构建整个列表提供给其他构造函数。 生成器表达式的句法跟列表推导式几乎一样,只不过把方括号换成圆括号而已。
**案例:使用生成器表达式构建一个元组和一个数组 **
# 一个给定的字符串,需要把这个字符串给转换成Unicode,然后存入一个列表中
strs = "ABCDE"
tuples = (ord(s) for s in strs)
print(tuples) # <generator object <genexpr> at 0x00000243E17C9D20>
print(tuple(tuples)) # (65, 66, 67, 68, 69)
如果生成器表达式是函数唯一的参数,则不需要额外再使用圆括号括起来,
strs = "ABCDE"
print(tuple(ord(s) for s in strs)) # (65, 66, 67, 68, 69)
元组不仅仅是不可变列表
元组有两个作用,除了 可以作为不可变列表使用之外,还可用作没有字段名称的记录 。
用作记录
用元组存放记录,**元组中的一项对应一个字段的数据**,项的位置决定数据的意义。 如果只把元组当作不可变列表,那么项数和项的顺序就变得可有可无。**但是如果把元组当作字段的容器使用,那么项数通常是固定的,顺序也变得十分重要**。
# 拆包
name, age, phone, address = ("zhangsan", 20, 1509999999, "中国北京")
# 还可以通过_来接收不需要处理的
name, _, phone, address = ("zhangsan", 20, 1509999999, "中国北京")
通常没必要为了给字段指定一个名称而创建一个类。如果不使用索引访问字段,而只使用拆包,那就更没有必要创建类。
现在,我们把 tuple 类当作 list 类的一种不可变的变体。
用作不可变列表
Python 解释器和标准库经常把元组当作不可变列表使用。这么做主要有两个好处。
-
意图清晰 只要在源码中见到元组,你就知道它的长度永不可变。
-
性能优越 长度相同的元组和列表,元组占用的内存更少,而且 Python 可对元组做些优化。
然而,你要知道,元组的不可变性仅针对元组中的引用而言。元组中的引用不可删除、不可替换。倘若引 用的是可变对象,改动对象之后,元组的值也会随之变化。下面的代码段通过创建两个元组(a 和 b)来演 示这一点。一开始,a 和 b 是相等的,此时 b 在内存中的初始布局如下

说明: 元组的内容自身是不可变的,但是这仅仅表明元组中存放的引用始终指向同一批对象。倘若引用 的是可变对象,例如一个列表,那么元组的值就可能发生变化。
list1 = ["篮球", "足球", "羽毛球"]
tup = ("zhangsan", 20, 1509999999, "中国北京", list1)
print(tup) # "zhangsan", 20, 1509999999, "中国北京"
list2 = ["抽烟", "喝酒", "烫头"]
list1 = list2
print(list1) # ['抽烟', '喝酒', '烫头']
# 这里虽然list1变了,但是tup的值是固定了,且在list1变化之前就是固定的了
print(tup) # ('zhangsan', 20, 1509999999, '中国北京', ['篮球', '足球', '羽毛球'])
# 修改元组的最后一项,tup的值就变化了
tup[-1].append(list2)
print(tup) # ('zhangsan', 20, 1509999999, '中国北京', ['篮球', '足球', '羽毛球', ['抽烟', '喝酒', '烫头']])
总结
-
Python 编译器求解元组字面量时,经过一次操作即可生成元组常量的字节码。求解列表字面量时,生成的字节码将每个元素当作独立的常量推入数据栈,然后构建列表。
-
给定一个元组 t,tuple(t) 直接返回 t 的引用,不涉及复制。相比之下,给定一个列表 l,list(l) 创建 l 的副本。
-
tuple 实例长度固定,分配的内存空间正好够用。而 list 实例的内存空间要富余一些,时刻准备追加元素。
-
对元组中项的引用存储在元组结构体内的一个数组中,而列表把引用数组的指针存储在别处。二者不存储在同一个地方的原因是列表可以变长,一旦超出当前分配的空间,Python 就需要重新分配引用数组来腾出空间,而这会导致 CPU 缓存效率较低。
列表和元组方法的比较
元组支持所有不涉及增删项的列表方法,而且元组没有`__reversed__ `方法。其实,这正是一种优化措施。没有` __reversed__ `方法,`reversed(my_tuple)` 也能正常工作。
序列和可迭代对象拆包
拆包的特点是不用我们自己动手通过索引从序列中提取元素,这样就减少了出错的可能。拆包的目标可以是任何可迭代对象,包括不支持索引表示法([]
)的迭代器。拆包对可迭代对象的唯一要求是,一次只能产出一项,提供给接收端变量。不过也有例外,可以使用星号(*
)捕获余下的项,
最明显的拆包形式是并行赋值(parallel assignment),即把可迭代对象中的项赋值给变量元组,代码如下:
name, age, phone, address = ("zhangsan", 20, 1509999999, "中国北京")
利用拆包还可以轻松对调两个变量的值,省掉中间的临时变量 ,代码如下
a=1
b=2
a,b = b,a
调用函数时在参数前面加上一个 *,利用的也是拆包,代码如下
print(divmod(20, 8)) # (2, 4)
t = (20, 8)
# 这里就是拆包
print(divmod(*t)) # (2, 4)
使用 `*` 获取余下的项
定义函数时可以使用 *args
捕获余下的任意数量的参数,这是 Python 的一个经典特性。
a, b, *rest = range(5)
print(a) # 0
print(b) # 1
print(rest) # [2, 3, 4]
Python 3 把这一思想延伸到了并行赋值上。 并行赋值时,*
前缀只能应用到一个变量上,不过可以是任何位置上的变量。
a, *body, c, d = range(5)
print(a) # 0
print(body) # [1, 2]
print(c) # 3
print(d) # 4
在函数调用和序列字面量中使用 `*` 拆包
在函数调用中可以多次使用 *
,代码如下
def fun(a, b, c, d, *rest):
print(a) # 1
print(b) # 2
print(c) # 3
print(d) # 4
print(rest) # (5, 6)
return a, b, c, d, rest
print(fun(*[1, 2], 3, *range(4, 7))) # (1, 2, 3, 4, (5, 6))
定义列表、元组或集合字面量时,也可以使用*
,代码如下
print(*range(4), 4) # 0 1 2 3 4
print([*range(4), 4]) # [0, 1, 2, 3, 4]
print({*range(4), 4, *(5, 6, 7)}) # {0, 1, 2, 3, 4, 5, 6, 7}
嵌套拆包
拆包的对象可以嵌套,例如 (a, b, (c, d))。如果值的嵌套结构是相同的,则 Python 能正确处理。
# 每个元组是一个四字段记录,最后一个字段是坐标对
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)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for name, _, _, (lat, lon) in metro_areas: # 把最后一个字段赋值给一个嵌套元组,拆包坐标对。
# lon <= 0: 测试条件只选取西半球的城市。
if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
if __name__ == '__main__':
main()
序列模式匹配
def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for record in metro_areas:
# 这个 match 的匹配对象是 record,即 metro_areas 中的各个元组
match record:
# 一个 case 子句由两部分组成:一部分是模式,另一部分是使用 if 关键字指定的卫语句(guard clause,可选)
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
if __name__ == '__main__':
main()
序列模式可以写成元组或列表,或者任意形式的嵌套元组和列表,使用哪种句法都没有区别,因为在序列 模式中,方括号和圆括号的意思是一样的。上例中的模式写成列表形式,其中嵌套的序列则写成元组形式,这样做只是为了避免重复使用方括号或圆括号。
**注意: 在 ***match/case**
* 上下文中,**str**
、**bytes**
和 **bytearray**
实例不作为序列处理。**match**
把这 些类型视为“原子”值,就像整数 987 整体被视为一个值,而不是数字序列。**
** 与拆包不同,模式不析构序列以外的可迭代对象(例如迭代器) 。**
** **_ **
符号在模式中有特殊意义:匹配相应位置上的任何一项,但不绑定匹配项的值。另外,**_ **
是唯一可在模式中多次出现的变量。 模式中的任何一部分均可使用 **as**
关键字绑定到变量上。 **
case [name, _, _, (lat, lon) as coord]:
切片
在 Python 中,列表、元组、字符串等所有序列类型都支持切片操作 。
为什么切片和区间排除最后一项
切片和区间排除最后一项是一种 Python 风格约定,这与 Python、C 和很多其他语言中从零开始的索引相匹 配。排除最后一项可以带来以下好处。
-
在仅指定停止位置时,容易判断切片或区间的长度。例如,
range(3)
和my_list[:3]
都只产生 3 项。 -
同时指定起始和停止位置时,容易计算切片或区间的长度,做个减法即可:
stop - start
。 -
方便在索引
x
处把一个序列拆分成两部分而不产生重叠,直接使用my_list[:x]
和my_list[x:]
即可。
list = [10, 20, 30, 40, 50, 60]
# 在索引位2处拆分
print(list[:2]) # [10, 20]
print(list[2:]) # [30, 40, 50, 60]
切片对象
一个众所周知的秘密是,我们还可以使用`s[a:b:c]` 句法指定步距 c,让切片操作跳过部分项。步距也可 以是负数,反向返回项。代码如下
strs = "ABCDEFG"
# 从头切到未,步长为3
print(strs[::3]) # ADG
# 从头切到未,步长为负数,也就是倒序,步长为1
print(strs[::-1]) # GFEDCBA
# 从头切到未,步长为负数,也就是倒序,步长为2
print(strs[::-2]) # GECA
a:b:c
表示法只在 []
内部有效,表示索引或下标运算符,得到的结果是一个切片对象:slice(a, b, c)
。
切片可以起别名
strs = "ABCDEFG"
# 切片起了别名
ONE = slice(1, 2)
TWO = slice(3, 5)
# 使用切片别名
print(strs[ONE]) # B
print(strs[TWO]) # DE
为切片赋值
在赋值语句的左侧使用切片表示法,或者作为 del
语句的目标,可以就地移植、切除或以其他方式修改可变序列。 代码如下
list = list(range(10))
print(list) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 为切片赋值,是动态修改的源列表
list[2:5] = [11, 22]
print(list) # [0, 1, 11, 22, 5, 6, 7, 8, 9]
# 删除切片,动态修改的源列表
del list[5:7]
print(list) # [0, 1, 11, 22, 5, 8, 9]
如果赋值目标是一个切片,则右边必须是一个可迭代对象,即使只有一项。
使用 `+` 和 `*` 处理序列
Python 程序员预期序列支持 +
和 *
。通常,+
的两个运算对象必须是同一种序列,而且都不可修改,拼接的结果是一个同类型的新序列。 如果想多次拼接同一个序列,可以乘以一个整数。同样,结果是一个新创建的序列。 **+**
** 和 *****
始终创建一个新对象,绝不更改操作数**。
list = [1, 2, 3]
print(list * 2) # [1, 2, 3, 1, 2, 3]
print("abc" * 3) # abcabcabc
list.sort 与内置函数 sorted
list.sort
方法就地排序列表,即不创建副本。返回值为 None,目的就是提醒我们,它更改了接收者, 没有创建新列表。这是 Python API 的一个重要约定:就地更改对象的函数或方法应该返回 None, 让调用方清楚地知道接收者已被更改,没有创建新对象。例如,random.shuffle(s)
函数也有类似的行 为:就地混洗可变序列 s,返回 None。
与之相反,内置函数 sorted 返回创建的新列表。该函数接受任何可迭代对象作为参数,包括不可变序列 和生成器。无论传入什么类型的可迭代对象,sorted 函数始终返回新创建的列表。
list.sort 和 sorted 均接受两个可选的关键字参数。
-
reverse 值为 True 时,降序返回项(即反向比较各项)。默认值为 False。
-
key 一个只接受一个参数的函数,应用到每一项上,作为排序依据。例如,排序字符串 时,key=str.lower 执行不区分大小写排序,而 key=len 按字符长度排序各个字符串。默认值是恒等函 数(即比较项本身)。
原文始发于微信公众号(Python之家):Python深入-2-细说序列
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198432.html