第二章 数组与序列
Python从ABC语言继承了对序列的统一处理方式。ABC是Guido van Rossum在开发Python前参与的一个研究项目,它引入了许多如今被认为是"Pythonic"的理念:
- 对不同类型序列的通用操作
- 内置的元组和映射类型
- 依靠缩进定义结构
- 强类型但无需变量声明
这些特性使Python成为用户友好的编程语言。在Python中,字符串、列表、字节序列、数组、XML元素和数据库查询结果共享一套丰富的通用操作,包括迭代、切片、排序和拼接。
内置序列概览
标准库提供了多种用C语言实现的序列类型,可以从两个维度进行分类。
**容器序列(Container sequences)**可以容纳不同类型的元素,包括嵌套容器。保存对其所含对象的引用,例如:list、tuple和collections.deque。适用于任意类型的对象。**扁平序列(Flat sequences)**只能容纳一种简单类型的元素,在自己的内存空间中直接存储元素的值。例如:str、bytes和array.array,仅限于存储字节、整数和浮点数等原始机器值,更加紧凑(无额外的引用开销)。
内存结构差异:
- 容器序列(如元组)包含指向其元素的引用数组,每个元素都是独立的Python对象
- 扁平序列(如数组)是单一对象,内部持有一个包含原始值的C语言数组
- 在64位Python中,每个Python对象都有包含元数据的头部(引用计数、类型指针等),这使得扁平序列比容器序列更节省内存
[!TIP]
**内存中的每个 Python 对象都有一个包含元数据的头部。**最简单的 Python 对象(如 float)包含一个值字段和两个元数据字段:
- ob_refcnt:对象的引用计数
- ob_type:指向对象类型的指针
- ob_fval:一个 C 语言 double 类型,存储浮点数值
在 64 位 Python 构建版本中,每个字段占用 8 字节。这就是为什么 float 数组比 float 元组紧凑得多:数组是单一对象,直接存储浮点数值的原始值;而元组由多个对象组成——元组本身及其包含的每个 float 对象。
**可变序列(Mutable sequences)**例如:list、bytearray、array.array和collections.deque,支持原地修改。不可变序列(Immutable sequences),例如:tuple、str和bytes,创建后不能修改。
列表推导式与生成器表达式
列表推导式提供了一种构建列表的简洁、高效方式
# 传统方式
symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
codes.append(ord(symbol))
print("传统方式:", codes) # [36, 162, 163, 165, 8364, 164]
# 列表推导式
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
print("列表推导式:", codes) # [36, 162, 163, 165, 8364, 164]
列表递推式意图更明确,通常比等效的map和filter组合更易读,通常性能更好。但是滥用列表推导式可能写出烂代码,不打算使用生成的列表,就不该使用这种语法。应尽量保持简短。如果列表推导式超过两行,最好将其拆分或改写为普通的 for 循环。
# 列表推导式
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
print("列表推导式过滤:", beyond_ascii) # [162, 163, 165, 8364, 164]
# map和filter组合
symbols = '$¢£¥€¤'
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
print("map/filter组合:", beyond_ascii) # [162, 163, 165, 8364, 164]
[!TIP]
在 Python 代码中,位于 []、{} 或 () 这类配对分隔符内的换行符会被忽略。因此构建多行的列表、列表推导式、元组、字典等,而无需使用 \ 行续行符(如果你不小心在 \ 后面加了空格,续行符就会失效)。
当分隔符用于定义包含逗号分隔项的字面量时,末尾的逗号会被忽略。在编写多行列表字面量时,在最后一项后加上逗号是个体贴的做法,这样其他开发者更容易添加新项,同时在查看代码差异(diffs)时也能减少噪音。
局部作用域
Python 3中,列表推导式、生成器表达式及其同类(集合推导式和字典推导式)有一个局部作用域,用于保存 for 子句中赋值的变量。海象运算符(:=)赋值的变量在推导式返回后仍然可访问。
x = 'ABC'
codes = [ord(x) for x in x]
print("原始x值:", x) # 'ABC'
print("codes列表:", codes) # [65, 66, 67]
# 使用海象运算符(:=)的变量在推导式后仍然可访问
codes = [last := ord(c) for c in x]
print("最后的ord值:", last) # 67
try:
print(c) # 会引发NameError
except NameError:
print("c变量在推导式后不可访问")
笛卡尔积
列表推导式可以计算多个可迭代对象的笛卡尔积,笛卡尔积的元素是由每个输入可迭代对象中各取一个元素组成的元组。结果列表的长度等于各输入可迭代对象长度的乘积。
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
# 按颜色再按尺码排列
tshirts = [(color, size) for color in colors for size in sizes]
print("按颜色再按尺码排列:", tshirts)
# [('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
# 按尺码再按颜色排列
tshirts = [(color, size) for size in sizes for color in colors]
print("按尺码再按颜色排列:", tshirts)
# [('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'), ('black', 'L'), ('white', 'L')]
生成器表达式
生成器表达式使用迭代器协议逐个生成元素,节省内存。
symbols = '$¢£¥€¤'
# 生成器表达式用于创建元组
tup = tuple(ord(symbol) for symbol in symbols)
print("元组:", tup) # (36, 162, 163, 165, 8364, 164)
# 生成器表达式用于创建数组
import array
# 生成器表达式的语法与列表推导式相同,但使用圆括号
arr = array.array('I', (ord(symbol) for symbol in symbols))
print("数组:", arr) # array('I', [36, 162, 163, 165, 8364, 164])
# 生成器表达式中的笛卡尔积(节省内存)
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
print("T恤清单:")
# 如果生成器表达式是函数调用的唯一参数,则无需重复外层的圆括号
for tshirt in (f'{c} {s}' for c in colors for s in sizes):
print(tshirt)
# black S, black M, black L, white S, white M, white L
元组
作为记录
元组不仅仅是不可变的列表,也可以作为没有字段名的记录。当用作记录时,元组中的每一项代表一个字段的数据,其位置决定了其含义。
lax_coordinates = (33.9425, -118.408056) # 洛杉矶国际机场的经纬度
# 解包
city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014) # 东京数据
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
# 遍历排序后的旅行者ID
print("按国家代码排序:")
for passport in sorted(traveler_ids):
print('%s/%s' % passport)
# 只提取国家代码
print("\n国家代码:")
# 约定 _ 用作哑变量
for country, _ in traveler_ids:
print(country)
当元组用作记录时,元素数量通常是固定的,顺序至关重要。
作为不可变列表
元组的清晰性在于长度不会改变,性能优势在于与相同长度的列表相比,元组占用更少的内存。
[!CAUTION]
元组本身的内容是不可变的,但这仅意味着元组持有的引用将始终指向相同的对象。然而,如果其中一个被引用的对象是可变的(例如列表),其内容仍可能发生变化。
# 展示元组不可变性仅适用于引用
a = (10, 'alpha', [1, 2])
b = (10, 'alpha', [1, 2])
print(f"a == b: {a == b}") # True
# 修改元组中的可变对象
# 如果其中一个引用指向一个可变对象,而该对象发生了变化,那么元组的值也会随之改变
b[-1].append(99)
print(f"a == b after modification: {a == b}") # False
print(f"b: {b}") # (10, 'alpha', [1, 2, 99])
包含可变项的元组可能成为bug的来源,只有当对象的值永远不会改变时,它才是可哈希的。不可哈希的元组不能作为字典的键或集合的元素。
def fixed(o):
"""判断对象是否具有固定值(可哈希)"""
try:
hash(o)
except TypeError:
return False
return True
tf = (10, 'alpha', (1, 2)) # 可哈希
tm = (10, 'alpha', [1, 2]) # 不可哈希
print(f"fixed(tf): {fixed(tf)}") # True
print(f"fixed(tm): {fixed(tm)}") # False
元组相比列表有以下性能优势:
- 内存效率:元组一次性生成常量字节码,而列表需要逐个推入元素
- 复制效率:
tuple(t)返回对原元组的引用,无需复制;list(l)必须创建新副本 - 内存分配:元组精确分配所需空间,列表会预留额外空间以摊销append成本
- 缓存效率:元组直接存储引用数组,避免了列表的间接性带来的CPU缓存效率降低
元组与列表的比较详见书籍。
解包
解包避免了不必要的索引使用,适用于任何可迭代对象。其要求可迭代对象产生的元素数量必须与接收端的变量数量完全匹配,除非使用 * 来捕获多余元素。
并行赋值
# 基本解包
lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates
print(f"Latitude: {latitude}, Longitude: {longitude}")
# 交换变量值
a, b = 1, 2
b, a = a, b
print(f"After swap: a={a}, b={b}")
函数调用解包
# 使用*解包参数
# 这个方法要输入两个参数 输入元组然后解包就可以了
t = (20, 8)
quotient, remainder = divmod(*t)
print(f"Quotient: {quotient}, Remainder: {remainder}")
# os.path.split()返回元组
import os
_, filename = os.path.split('/home/luciano/.ssh/id_rsa.pub')
print(f"Filename: {filename}")
捕获多余元素
# 基本用法
a, b, *rest = range(5)
print(f"a={a}, b={b}, rest={rest}") # a=0, b=1, rest=[2, 3, 4]
a, b, *rest = range(3)
print(f"a={a}, b={b}, rest={rest}") # a=0, b=1, rest=[2]
a, b, *rest = range(2)
print(f"a={a}, b={b}, rest={rest}") # a=0, b=1, rest=[]
# *前缀只能应用于一个变量,但是可以出现在任何位置
a, *body, c, d = range(5)
print(f"a={a}, body={body}, c={c}, d={d}") # a=0, body=[1, 2], c=3, d=4
*head, b, c, d = range(5)
print(f"head={head}, b={b}, c={c}, d={d}") # head=[0, 1], b=2, c=3, d=4
在函数调用和序列字面量中使用*
def fun(a, b, c, d, *rest):
return a, b, c, d, rest
result = fun(*[1, 2], 3, *range(4, 7))
print(f"Function result: {result}") # (1, 2, 3, 4, (5, 6))
# 序列字面量中的*解包
print(f"Tuple with unpacking: {*range(4), 4}") # (0, 1, 2, 3, 4)
print(f"List with unpacking: [*range(4), 4]") # [0, 1, 2, 3, 4]
print(f"Set with unpacking: {*range(4), 4, *(5, 6, 7)}") # {0, 1, 2, 3, 4, 5, 6, 7}
# 字典解包
dict_result = dict(**{'x': 1}, y=2, **{'z': 3})
print(f"Dict unpacking: {dict_result}") # {'x': 1, 'y': 2, 'z': 3}
嵌套解包
# 例2-8. 解包嵌套元组以访问经度
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 print_western_hemisphere_cities():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for name, _, _, (lat, lon) in metro_areas:
if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
print_western_hemisphere_cities()
输出:
| latitude | longitude
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
São Paulo | -23.5478 | -46.6358
模式匹配
match/case语句进行模式匹配
基本模式匹配
class Robot:
def __init__(self):
self.leds = {}
def beep(self, times, frequency):
print(f"Beeping {times} times at {frequency}Hz")
def rotate_neck(self, angle):
print(f"Rotating neck to {angle} degrees")
def handle_command(self, message):
# match 关键字后的表达式是主题 subject
# 主题是 Python 尝试与每个 case 子句中的模式匹配的数据。
match message:
# 此模式匹配任何包含三个元素的序列。
# 第一个元素必须是字符串 'BEEPER'。
case ['BEEPER', frequency, times]:
self.beep(times, frequency)
case ['NECK', angle]:
self.rotate_neck(angle)
case ['LED', ident, intensity]:
self.leds[ident] = intensity
case ['LED', ident, red, green, blue]:
self.leds[ident] = (red, green, blue)
case _:
raise ValueError(f"Invalid command: {message}")
# 测试机器人命令
robot = Robot()
robot.handle_command(['BEEPER', 440, 3])
robot.handle_command(['NECK', 45])
match/case 看起来像switch/case 语句,但其进行了解构(destructuring)。
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 record in metro_areas:
match record: # ①
case [name, _, _, (lat, lon)] if lon <= 0: # ②
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
# 序列模式匹配主题的条件是:
# 1. 主题是一个序列
# 2. 主题和模式具有相同数量的元素
# 3. 每个对应的元素都匹配,包括嵌套元素
# 例如,模式[name, _, _, (lat, lon)]匹配一个包含四个元素的序列,且最后一个元素必须是一个包含两个元素的序列。
序列模式可以写成元组、列表或嵌套元组和列表的任意组合,但使用哪种语法没有区别,在序列模式中,方括号和圆括号含义相同。
兼容性
序列模式可以匹配collections.abc.Sequence的大多数实际或虚拟子类的实例,但str、bytes和bytearray除外。
在match/case的上下文中,str、bytes和bytearray的实例不被视为序列。这些类型之一的match主题被视为"原子"值,就像整数987被视为一个值,而不是数字序列。将这三种类型视为序列可能导致意外匹配的bug。如果想将这些类型的对象视为序列主题,需在match子句中进行转换:
# 将号码转化成了一个元组
match tuple(phone):
case ['1', *rest]: # 北美和加勒比地区
...
case ['2', *rest]: # 非洲和一些地区
...
case ['3' | '4', *rest]: # 欧洲
...
在标准库中,以下类型与序列模式兼容:
| list | memoryview | array.array |
|---|---|---|
| tuple | range | collections.deque |
与解包不同,模式不会解构非序列的可迭代对象(如迭代器)。
高级特性
在模式中,_符号是特殊的,它会匹配该位置上的任何值,匹配到的值不会被保存到任何变量中,是唯一可以在同一个模式中多次出现的占位符。
as关键字允许将模式的任何部分绑定到一个新变量,当需要同时以不同方式引用同一数据时很有用。
case [name, _, _, (lat, lon)额外保存成 coord对象 as coord]:
# 将(lat, lon)额外保存成 coord对象
给定主题['Shanghai', 'CN', 24.9, (31.1, 121.3)],上述模式将匹配,并设置以下变量:
| 变量 | 值 |
|---|---|
| name | ‘Shanghai’ |
| lat | 31.1 |
| lon | 121.3 |
| coord | (31.1, 121.3) |
类型检查
# 通过添加类型信息使模式更具体
case [str(name), _, _, (float(lat), float(lon))]:
# 匹配任何以`str`开头、以两个`float`的嵌套序列结尾的主题序列
case [str(name), *, (float(lat), float(lon))]:
# * 匹配任意数量的元素,但不会将它们绑定到变量。
# 使用 *extra 而不是 * 会将元素绑定到 extra,作为一个包含 0 个或更多元素的列表。
守卫子句
以if开头的可选守卫子句仅在模式匹配时才求值,并可以引用模式中绑定的变量:
# 以if开头的可选守卫子句仅在模式匹配时才求值,并可以引用模式中绑定的变量
match record:
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
只有当模式匹配且守卫表达式为真值时,才会执行包含print语句的嵌套块。
切片
为什么切片和范围排除最后一个元素
Python中切片和范围排除最后一个元素的惯例与基于零的索引很好地配合,具有以下便利特性,如当只给出停止位置时,很容易看出切片或范围的长度:range(3)和my_list[:3]都会产生三个元素。当给出起始和停止位置时,很容易计算切片或范围的长度:只需用停止位置减去起始位置。在任何索引x处将序列分成两部分很容易,且不会重叠:只需获取my_list[:x]和my_list[x:]。
l = [10, 20, 30, 40, 50, 60]
print(l[:2]) # [10, 20]
print(l[2:]) # [30, 40, 50, 60]
print(l[:3]) # [10, 20, 30]
print(l[3:]) # [40, 50, 60]
切片对象
切片语法a:b:c 仅在用作索引或下标运算符时在 [] 内有效。a:b:c 实际上是 slice(a, b, c) 对象的简写形式。当Python解释器遇到表达式 seq[start:stop:step] 时,会调用 seq.__getitem__(slice(start, stop, step))
# 其中a是起始位置 b是结束位置 c是步长 步长为负的时候就会进行反向迭代
s = 'bicycle'
print(s[::3]) # 'bye'
print(s[::-1]) # 'elcycib'
print(s[::-2]) # 'eccb'
import numpy as np
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
slice_obj = slice(1, 8, 2)
print(arr[slice_obj]) # 输出: [1 3 5 7]
多维切片和省略号
[] 运算符可以接受多个索引,Python会将 a[i, j] 视为 (i, j) 元组。
# NumPy二维数组示例
a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(a[1:3, 0:2]) # [[4, 5], [7, 8]]
[!NOTE]
除了
memoryview之外,Python内置的序列类型(list,tuple,str,bytes,bytearray等)本质上都是一维的,对于一维序列,索引使用单个整数或切片,而不是元组。my_list = [10, 20, 30, 40] print(my_list[2]) # 30 (单个索引) print(my_list[1:3]) # [20, 30] (单个切片)如果尝试使用元组索引,会引发
TypeError:my_list = [10, 20, 30, 40] try: print(my_list[1, 2]) # 会报错 except TypeError as e: print(e) # "list indices must be integers or slices, not tuple"
memoryview是Python内置类型,但它可以处理多维数据。通过memoryview.cast()可以创建多维视图。data = bytearray(b'abcdef') mv = memoryview(data) # 一维索引 print(mv[1]) # 98 # 创建2D视图 mv2d = mv.cast('B', (2, 3)) print(mv2d[0, 1]) # 98 (二维索引)NumPy的
ndarray是多维数组,它支持元组索引(如a[1, 2])。但NumPy不是Python内置的,而是外部库,所以不在此讨论范围内。import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) print(a[1, 2]) # 6 (二维索引)
省略号 (...)是 Ellipsis 对象的别名,用于多维数组切片的快捷方式。
# 四维数组示例
# x[i, ...] 是 x[i, :, :, :] 的快捷方式
x = [[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]]
print(x[0, ...]) # [[1, 2], [3, 4]]
print(x[..., 0]) # [[1, 3], [5, 7]]
赋值
可变序列可以通过切片赋值进行原地修改
l = list(range(10))
print(l) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l[2:5] = [20, 30]
print(l) # [0, 1, 20, 30, 5, 6, 7, 8, 9]
del l[5:7]
print(l) # [0, 1, 20, 30, 5, 8, 9]
l[3::2] = [11, 22]
print(l) # [0, 1, 20, 11, 5, 22, 9]
l[2:5] = [100]
print(l) # [0, 1, 100, 22, 9]
赋值目标是切片时,右侧必须是可迭代对象,即使只有一个元素。
使用 + 和 * 与序列
+ 和 * 总是创建新对象,不会修改操作数。
l = [1, 2, 3]
print(l * 5) # [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
print(5 * 'abcd') # 'abcdabcdabcdabcdabcd'
当a是包含可变项目的序列时,要注意像a * n这样的表达式。例如使用上述技巧初始化二维列表将导致一个包含三个指向同一内部列表的引用的列表。
weird_board = [[' '] * 3] * 3
weird_board[1][2] = 'O'
print(weird_board) # [['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
正确方式:
board = [[' '] * 3 for _ in range(3)]
board[1][2] = 'X'
print(board) # [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
增强赋值
使+=工作的特殊方法是__iadd__。当序列没有实现 __iadd__ 或 __imul__ 时,a += b 与 a = a + b 等效,其先生成新对象 c =a+b,然后将其绑定到a。绑定到a的对象的身份是否改变,这取决于__iadd__是否可用。
所以增强赋值运算符 += 和 *= 的行为取决于序列的可变性。如下列表是可变的但是元组不是可变的。
# 可变序列(列表)
l = [1, 2, 3]
print(id(l)) # 4311953800
l *= 2
print(l) # [1, 2, 3, 1, 2, 3]
print(id(l)) # 4311953800 # 同一个对象 只是新增了项目
# 不可变序列(元组)
t = (1, 2, 3)
print(id(t)) # 4312681568
t *= 2
print(t) # (1, 2, 3, 1, 2, 3)
print(id(t)) # 4301348296 # 建立了一个新元组
当序列没有实现 __iadd__ 或 __imul__ 时,a += b 与 a = a + b 等效,然后将其绑定到a。绑定到a的对象的身份是否改变,这取决于__iadd__是否可用。
对不可变序列重复拼接效率低下,因为解释器必须复制整个目标序列, 创建一个新序列,包含要拼接的项,而不是简单追加新项。(但是字符串例外 实现的时候已经做了优化)
+=赋值谜题
t = (1, 2, [30, 40])
t[2] += [50, 60]
# 会引发 TypeError,但列表元素已被修改
# 输出:
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'tuple' object does not support item assignment
# (1, 2, [30, 40, 50, 60])
因为t[2] += [50, 60] 等价于 t[2] = t[2] + [50, 60]。首先计算 t[2] + [50, 60],得到 [30, 40, 50, 60],然后尝试将结果赋值给 t[2],但元组是不可变的,所以引发 TypeError。但在赋值失败前,t[2] 已被修改为 [30, 40, 50, 60]。
可知增强赋值不是原子操作,因此要避免在元组中放置可变项目。
排序
list.sort对列表进行原地排序,返回 None(提醒我们未创建新列表),适用于可变序列。
[!IMPORTANT]
**原地更改对象的函数或方法应返回None,以向调用者明确表示接收者已更改,且未创建新对象。**例如
random.shuffle(s)函数原地打乱可变序列s,并返回None。
fruits = ['grape', 'raspberry', 'apple', 'banana']
fruits.sort()
print(fruits) # ['apple', 'banana', 'grape', 'raspberry']
sorted创建新列表并返回,适用于任何可迭代对象,返回新列表,原始序列不变。
fruits = ['grape', 'raspberry', 'apple', 'banana']
print(sorted(fruits)) # ['apple', 'banana', 'grape', 'raspberry']
print(fruits) # ['grape', 'raspberry', 'apple', 'banana']
上述两个方法都可以使用如下参数:
-
reverse=True:降序排序 -
key:排序键函数(如key=len按长度排序)
print(sorted(fruits, reverse=True)) # ['raspberry', 'grape', 'banana', 'apple']
print(sorted(fruits, key=len)) # ['grape', 'apple', 'banana', 'raspberry']
当列表不是最佳选择
有些时候列表不见得是最好的选择。例如当需要处理数百万个浮点值时,数组可以节省大量内存。以下列举了各种情况下的更优选择。
数组
数组可以高效存储同类型数字,其适用于仅包含数字(如浮点数、整数)的大规模序列。其优势在于内存更紧凑(只存打包字节,类似 C 数组),同时支持所有可变序列操作(.append, .pop, .insert, .extend 等),并且提供高效 I/O 方法如.fromfile, .tofile, .frombytes, .tobytes。
[!NOTE]
只存打包字节,类似 C 数组
数组中数据在内存中是连续存放的,没有间隙或额外的元信息穿插其中。例如,一个长度为 10 的字节数组,就只占 10 个字节的内存空间,所以说内存过呢国家紧凑。
打包(packed)表示:数据被紧挨着存储,不进行对齐填充(padding)。比如,在某些结构体中,为了 CPU 访问效率,编译器可能会在字段之间插入空字节(填充),以满足内存对齐要求。而“打包”就是取消这种填充,让数据紧紧排列。“只存字节”说明这个数组只用来存原始的
byte数据,不存其他类型对象的引用或额外信息。C 语言的数组是典型的内存连续 + 无运行时开销的结构。例如:
char arr[5] = {1, 2, 3, 4, 5};而 Python 列表就是非紧凑的
my_list = [1, 2, 3, 4, 5]存的是指向整数对象的指针,不是原始字节。每个元素可能是一个 PyObject*,占用 8 字节(64位系统),还带对象头。内存不紧凑,有开销。
数组的限制在于创建时需指定 typecode(如 'd' 表示双精度浮点,'B' 表示无符号字节),不支持混合类型以及无原地排序方法(截至 Python 3.10);需用 sorted() 重建数组:
a = array.array(a.typecode, sorted(a))
# 进行导入
from array import array
from random import random
# 创建含1000万个双精度浮点数的数组
floats = array('d', (random() for _ in range(10**7)))
print("Last element:", floats[-1])
# 保存到二进制文件
with open('floats.bin', 'wb') as fp:
floats.tofile(fp)
# 从二进制文件加载
floats2 = array('d')
with open('floats.bin', 'rb') as fp:
floats2.fromfile(fp, 10**7)
print("Last element after load:", floats2[-1])
print("Arrays equal:", floats2 == floats)
# 两个数组中中的内容是一致的
array.fromfile 加载 1000 万浮点数约需 0.1 秒,远快于文本解析;二进制文件体积也显著小于文本。
详细使用方法自己去查。
内存视图
内存视图(memoryviewl类)是一种共享内存序列类型,其允许对二进制数据(如 array, bytes)进行切片或重塑,但不复制底层字节。内存视图允许在数据结构(如PIL图像、SQLite数据库、NumPy数组等)之间共享内存,而无需先复制。修改视图会直接影响原始数据,处理大型二进制数据时可提升效率。
from array import array
# 创建 6 字节数组
octets = array('B', range(6))
# 创建 memoryview
m1 = memoryview(octets)
print(m1.tolist()) # [0, 1, 2, 3, 4, 5]
# 重塑为 2x3 矩阵
m2 = m1.cast('B', [2, 3])
print( m2.tolist()) # [[0, 1, 2], [3, 4, 5]]
# 重塑为 3x2 矩阵
m3 = m1.cast('B', [3, 2]) # [[0, 1], [2, 3], [4, 5]]
# 通过不同视图修改数据
m2[1, 1] = 22
m3[1, 1] = 33
print(octets)
# 输出: array('B', [0, 1, 2, 33, 22, 5])
通过修改16位整数数组中一个项目的单个字节来更改其值
from array import array
numbers = array('h', [-2, -1, 0, 1, 2]) # 'h': 16位有符号整数
memv = memoryview(numbers)
memv_oct = memv.cast('B') # 转为字节视图
# 每一个数组元素是16位(2B) 所以视图中有10个元素
print(memv_oct.tolist()) # [254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
memv_oct[5] = 4 # 修改第5字节
print(numbers)
# 输出: array('h', [-2, -1, 1024, 1, 2])
NumPy
Numpy的核心类型是numpy.ndarray,支持多维、同质数组。其优势在于高效向量化操作(无需 Python 循环)同时实现内存映射(numpy.load(..., 'r+'))支持超大数组。
基本数组操作
import numpy as np
# 创建一维数组并重塑
a = np.arange(12) # array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
print(a.shape) # (12, )
a.shape = 3, 4
print(a)
# array([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# 索引与切片
print("Row 2:", a[2]) # array([ 8, 9, 10, 11])
print("Element [2,1]:", a[2, 1]) # 9
print("Column 1:", a[:, 1]) # array([1, 5, 9])
# 转置
print("Transposed:\n", a.transpose())
# array([[0, 4, 8],
# [1, 5, 9],
# [2, 6, 10],
# [3, 7, 11]])
高级操作
import numpy as np
from time import perf_counter
# 假设存在 'floats-10M-lines.txt'
floats = np.loadtxt('floats-10M-lines.txt')
print("Last 3 elements:", floats[-3:]) # array([ 3016362.69195522, 535281.10514262, 4566560.44373946])
floats *= 0.5
print("After *= 0.5:", floats[-3:]) array([ 1508181.34597761, 267640.55257131, 2283280.22186973])
t0 = perf_counter()
floats /= 3
print("Time for division:", perf_counter() - t0, "seconds")
# 0.03690556302899495
np.save('floats-10M.npy', floats)
floats2 = np.load('floats-10M.npy', mmap_mode='r+')
floats2 *= 6
print("After *= 6 (memory-mapped):", floats2[-3:])
双端队列与其他队列
.append和.pop方法使列表可用作栈或队列,但从列表头部(索引0端)插入和删除是代价高昂的,因为必须在内存中移动整个列表。
collections.deque可以高效从两端插入/删除(O(1)),适合 FIFO/LIFO。其可设置 maxlen(有界 deque,已满时插入新内容会自动丢弃对端元素)同时是线程安全(append/popleft 原子操作)的,并提供 .rotate(n), .appendleft(), .extendleft() 等方法。但中间操作效率较低(如 del d[i])。
from collections import deque
dq = deque(range(10), maxlen=10)
print("Initial deque:", dq)# deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
dq.rotate(3)
print("After rotate(3):", dq) # deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
dq.rotate(-4)
print("After rotate(-4):", dq) # deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
dq.appendleft(-1)
print("After appendleft(-1):", dq) # deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
dq.extend([11, 22, 33])
print("After extend([11,22,33]):", dq) # deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
dq.extendleft([10, 20, 30, 40])
print("After extendleft([10,20,30,40]):", dq) # deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10) extendleft 会反转输入顺序
拓展
- 容器:某些包含对其他对象的引用的对象
- 容器序列:可以嵌套,可以包含任何类型的对象,包括容器本身的类型。dict 和 set 都是容器,但是它们不是序列
- 平面序列:不能嵌套的序列类型,只保存简单如整数、浮点数或字符的原子类型
- 列表可以包含混合类型的对象,但除非其中的项可以比较,否则无法对其进行排序。
- key的使用
# 要使用key只需定义一个参数函数来检索或计算你要用来排序对象的任何标准
# key函数每个项目只调用一次,而两参数比较在排序算法在需要比较两个项时每次都被调用
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
>>> sorted(l, key=int)
[0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
>>> sorted(l, key=str)
[0, '1', 14, 19, '23', 28, '28', 5, 6, '9']

被折叠的 条评论
为什么被折叠?



