HOWTO · Python

Python 中的查找表

本教程演示了如何在 Python 中使用查找表,重点介绍了字典、列表、NumPy 数组和 Pandas DataFrame 等方法。学习实现高效的数据检索技术,以优化您的编程技能并提升应用程序性能。探索实际代码示例和详细解释,以掌握 Python 中的查找表。

本页内容

查找表会存储值,让程序能够通过键或位置检索这些值。当程序需要反复查询同一种对应关系时,查找表就很有用,例如将产品代码映射到价格。预先计算结果可以避免重复计算,但查找表会占用内存,而且源数据发生变化时也需要更新。

应根据键的形式和所需操作选择结构:字典接受任意可哈希键,列表和数组使用整数位置,而 DataFrame 支持带标签的列和表格操作。下面的示例展示了如何使用不同结构完成实际查找。

为数据选择合适的查找表结构

先确定用什么来标识一个值。对于名称、代码或其他不属于稠密整数位置的键,使用字典。如果位置是从零开始的连续整数,且值可以存放在 Python 序列中,则使用列表。如果数据是数值,并且需要数组运算,则使用 NumPy 数组。如果每条记录包含多个命名字段,或需要筛选及其他表格操作,则使用 Pandas DataFrame。NumPy 和 Pandas 是第三方依赖;使用相应示例前,请先在项目环境中安装它们。

使用字典按键查找

字典将每个互不相同且可哈希的键映射到一个值。它适合处理非连续的键,也可以通过 get 明确指定备用值。键不存在时,get 会返回你提供的默认值;使用方括号访问则会在键不存在时引发 KeyError。

运行下面的示例。代码会显示茶的价格,以及果汁缺货时返回的提示;示例中的英文提示文字保持英文。

本节中的代码以及显示的标签或输出仍为英文。

prices = {"tea": 2.5, "coffee": 3.0, "cocoa": 2.75}
print(prices["tea"])
print(prices.get("juice", "not listed"))
2.5
not listed

字典查找的平均时间复杂度为 O(1),但这并不保证所有工作负载下都如此。键必须可哈希;如果为已有键赋值,新值会替换原值。源记录发生变化时,也要更新字典,以免查找结果过时。

使用列表存储稠密整数索引

当有效索引从零开始且连续时,列表是一种简单的查找表:索引 i 对应第 (i + 1) 个项目。列表不适合客户编号等稀疏标识符,因为编号间的空缺会需要未使用的位置,或另建映射。列表索引的时间复杂度为 O(1);列表中的值仍是普通 Python 对象。

运行下面的示例。它会显示索引 2 对应的标签,以及索引超出范围时的提示;这两条英文文字保持英文。

本节中的代码以及显示的标签或输出仍为英文。

labels = ["bronze", "silver", "gold"]

def label_for(index):
    if 0 <= index < len(labels):
        return labels[index]
    return "index out of range"

print(label_for(2))
print(label_for(5))
gold
index out of range

显式的范围检查可以处理小于零以及大于或等于列表长度的索引。Python 列表也允许用负索引从末尾倒数,因此,如果查找表中的键只定义为非负位置,就应先进行验证。

使用 NumPy 数组存储数值数据

如果查找表包含数值,并且还需要向量化计算、切片或其他数组操作,NumPy 数组会很有用。NumPy 需要单独安装;它不属于 Python 标准库。标量查找仍只是一次按索引取值,因此数组不会自动成为比字典更快的替代方案。

运行下面的示例,查看按索引取出的数值和数组乘以二后的结果。代码中的越界提示是英文,并且在此示例的有效索引下不会显示。

本节中的代码以及显示的标签或输出仍为英文。

import numpy as np

rates = np.array([1.0, 1.5, 2.0, 2.5])
index = 2
if 0 <= index < len(rates):
    print(rates[index])
else:
    print("index out of range")
print((rates * 2).tolist())
2.0
[2.0, 3.0, 4.0, 5.0]

当索引来自外部输入时,范围检查很有用。NumPy 数组通常使用单一元素类型,这有利于数值运算;应根据要存储的数据选择合适的元素类型和形状。

使用 Pandas DataFrame 处理表格数据

对于包含命名列的记录,DataFrame 很合适,尤其是在查找属于更大规模筛选或分析任务的一部分时。Pandas 需要单独安装。按列筛选会逐行比较;如果只选择行,结果仍是 DataFrame。在下面的表达式中,通过 .loc 选取单独的 price 列会返回 Series;筛选操作可能需要扫描各行。若要反复按键查找,可以考虑设置有意义的索引,再使用 .loc。

运行下面的示例,查看代码为 B 的商品价格。代码中的英文备用提示保持原样;此示例找到匹配项时不会显示该提示。

本节中的代码以及显示的标签或输出仍为英文。

import pandas as pd

items = pd.DataFrame({
    "code": ["A", "B", "C"],
    "price": [1.25, 2.50, 3.75],
})
match = items.loc[items["code"] == "B", "price"]
if not match.empty:
    print(match.iloc[0])
else:
    print("not found")
2.5

这种写法可以处理没有匹配代码的情况,避免对空结果进行索引。如果代码唯一且会被反复查询,items.set_index("code").loc["B", "price"] 可以按标签查找;访问前应确认该代码存在,并决定如何处理重复标签。

处理缺失的键和无效索引

应根据调用方的需求选择缺失值处理策略。dict.get(key, default) 会返回备用值,而 key in mapping 可以区分键不存在与键对应的值恰好等于备用值这两种情况。如果周边逻辑将缺失键视为异常,可使用 try/except KeyError。对于列表和数组,如果负索引不应从末尾倒数,就应根据 0 <= index < len(table) 验证索引。对于 DataFrame 筛选结果,应先检查结果是否为空,再选取第一行。各函数应采用一致的策略,让调用方清楚缺失时会得到哨兵值、默认值还是错误。

比较性能与实际取舍

字典按键查找的平均时间复杂度为 O(1),列表或一维数组按位置查找的时间复杂度也为 O(1)。DataFrame 的布尔筛选通常为 O(n),因为它需要检查各行;索引可以支持按标签访问,但其成本和行为取决于索引的构建方式及数据形状。这些复杂度描述无法预测所有输入规模下的实际耗时:初始化成本、键类型、数据布局、库操作和内存都会产生影响。

应选择与键和操作相匹配的最简单结构:任意键用字典,稠密位置用列表,数值数组运算用 NumPy 数组,带标签的表格操作用 DataFrame。如果性能是决定因素,应使用有代表性的数据进行基准测试。也要考虑内存:字典存储键值映射,列表保存 Python 对象的引用,NumPy 数组存储具有类型的元素;实际占用取决于数据和实现。如果查找表由会变化的源数据推导而来,应在源数据变化时重建或更新查找表,以免返回过时结果。