1. 字典是什么,以及为什么它如此重要
如果你刚开始学python,或者已经写过一些脚本,那么 dict (字典)这个数据结构,你肯定绕不过去。它几乎无处不在,从读取配置文件、处理json数据,到构建缓存、管理对象属性,字典都扮演着核心角色。很多人觉得字典不就是“键值对”嘛,有什么好讲的?但恰恰是这种看似简单的结构,里面藏着不少门道和“坑”。用好了,你的代码简洁高效;用不好,可能就是性能瓶颈和bug的温床。
简单来说,python的字典是一个可变、无序的映射类型,它存储的是键(key)和值(value)的对应关系。键必须是不可变类型(如字符串、数字、元组),而值可以是任意python对象。它的核心优势在于,通过键来查找、插入或删除对应的值,其平均时间复杂度是o(1),也就是常数时间,速度极快。这背后是哈希表(hash table)的功劳。你可以把它想象成一个超级高效的“电话本”:你知道一个人的名字(键),就能瞬间找到他的电话号码(值),而不需要从头到尾翻一遍。
为什么我要专门总结字典的常用操作?因为在日常开发中,我看到太多人只停留在 dict[key] = value 和 dict.get(key) 的层面。实际上,字典提供了丰富的方法和技巧来处理更复杂的场景,比如合并多个字典、安全地访问嵌套数据、按照特定条件筛选内容,甚至是利用字典来实现一些巧妙的逻辑。掌握这些,能让你从“会用字典”升级到“善用字典”,写出更pythonic、更健壮的代码。
2. 字典的创建与初始化:不止一种方式
创建字典是最基础的一步,但你知道有多少种方法吗?每种方法适用的场景又是什么?这里我为你梳理了最实用的几种。
2.1 最直接的方式:花括号{}
这是最常用、最直观的方法,适合在代码中直接定义已知的键值对。
# 创建一个空字典
empty_dict = {}
# 创建一个包含初始数据的字典
user_info = {
"name": "张三",
"age": 30,
"city": "北京"
}
这种方式清晰明了,键和值一目了然。在定义配置、常量映射或者小型数据集合时,我强烈推荐使用它。
2.2 使用dict()构造函数
dict() 函数更加灵活,它可以从多种数据结构构造字典。
# 1. 使用关键字参数(键必须是合法的变量名,即字符串且不含特殊字符)
dict1 = dict(name="李四", age=25, city="上海")
# 结果: {'name': '李四', 'age': 25, 'city': '上海'}
# 2. 使用包含二元组(或列表)的可迭代对象
dict2 = dict([("name", "王五"), ("age", 28), ("city", "广州")])
# 结果: {'name': '王五', 'age': 28, 'city': '广州'}
# 3. 使用zip函数合并两个列表
keys = ["a", "b", "c"]
values = [1, 2, 3]
dict3 = dict(zip(keys, values))
# 结果: {'a': 1, 'b': 2, 'c': 3}
什么时候用 dict() ?
当你的数据源是动态生成的(比如从两个列表合并而来),或者键名包含连字符等不能作为变量名的字符时(这时不能用关键字参数方式), dict() 就派上用场了。例如,从api接口拿到两个列表,一个存字段名,一个存值,用 zip 加 dict() 就能快速组装成字典。
2.3 字典推导式:优雅且强大
如果你需要对数据进行一些处理再生成字典,字典推导式是你的最佳选择。它的语法和列表推导式类似,非常pythonic。
# 将一个列表的元素作为键,其平方作为值
numbers = [1, 2, 3, 4, 5]
squared_dict = {x: x**2 for x in numbers}
# 结果: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# 带条件过滤的推导式
even_squared = {x: x**2 for x in numbers if x % 2 == 0}
# 结果: {2: 4, 4: 16}
# 处理两个列表,键值都做转换
keys = ["apple", "banana", "cherry"]
prices = [10, 20, 30]
price_dict = {k.upper(): v*1.1 for k, v in zip(keys, prices)} # 键转大写,值加10%税
# 结果: {'apple': 11.0, 'banana': 22.0, 'cherry': 33.0}
字典推导式不仅代码简洁,而且执行效率通常也高于显式的循环。在处理数据转换和过滤时,我几乎总是首选它。
2.4 使用fromkeys方法快速创建默认字典
当你需要创建一个新字典,并且所有键都对应同一个初始值时, fromkeys 方法非常方便。
# 为多个键设置相同的默认值
default_dict = dict.fromkeys(["key1", "key2", "key3"], "default_value")
# 结果: {'key1': 'default_value', 'key2': 'default_value', 'key3': 'default_value'}
# 如果不提供第二个参数,默认值为none
keys_only = dict.fromkeys(["a", "b", "c"])
# 结果: {'a': none, 'b': none, 'c': none}
这个方法在初始化计数器、状态标记或者缓存结构时特别有用。但要注意,如果默认值是一个可变对象(如列表、字典),所有键会共享同一个对象引用,这通常不是你想要的,会导致意外的数据污染。我们后面在“坑与技巧”部分会详细讨论这个问题。
3. 核心操作:增删改查的学问
创建了字典,接下来就是对它进行“增删改查”。这些操作看似基础,但细节决定成败。
3.1 访问元素:安全第一
访问字典元素最直接的方式是用方括号 [] ,但如果键不存在,会抛出 keyerror 异常。
my_dict = {"a": 1, "b": 2}
value = my_dict["a"] # 正确,得到 1
# value = my_dict["c"] # 错误!keyerror: 'c'
在不确定键是否存在时,更安全的做法是使用 get(key, default) 方法。如果键存在,返回对应的值;如果不存在,则返回你指定的默认值(默认为 none ),而不会引发异常。
value = my_dict.get("c") # 返回 none
value = my_dict.get("c", 0) # 返回指定的默认值 0
我的经验是 :在大多数从外部获取数据(如解析json、读取数据库)后访问字典的场景中,优先使用 get 方法。这能有效避免因为数据格式意外变化而导致的程序崩溃。只有在你百分之百确定键一定存在时,才使用 [] 直接访问。
3.2 新增与修改元素:本质相同
向字典中添加新元素或修改现有元素,语法是一样的: dict[key] = value 。如果键已存在,则更新其值;如果不存在,则创建新的键值对。
my_dict = {}
my_dict["new_key"] = "new_value" # 新增
my_dict["new_key"] = "updated_value" # 修改
3.3 删除元素:多种方法各有用处
删除操作有几种方式,适用于不同场景:
del语句 :直接删除指定键的项。如果键不存在,同样会引发 keyerror 。
my_dict = {"a": 1, "b": 2, "c": 3}
del my_dict["b"]
# 现在 my_dict 是 {'a': 1, 'c': 3}
# del my_dict["d"] # keyerror!
pop(key, default) 方法 :删除指定键的项,并 返回被删除的值 。这是 del 的“有返回值”版本。如果提供了 default 参数且键不存在,则返回 default 而不抛异常;否则抛 keyerror 。
value = my_dict.pop("a") # 删除键'a',并返回1
# my_dict 现在是 {'c': 3}
value = my_dict.pop("d", none) # 键'd'不存在,返回none,不抛异常
popitem() 方法 :在python 3.7+中,字典会保持插入顺序。 popitem() 会移除并返回最后插入的(lifo顺序)键值对,作为一个元组。对于空字典调用会引发 keyerror 。这个方法在实现栈或缓存淘汰(如lru cache的简单实现)时很有用。
my_dict = {"a": 1, "b": 2}
key, value = my_dict.popitem() # 移除并返回 ('b', 2)
clear() 方法 :清空字典中的所有项,使其变为空字典 {} 。
my_dict.clear()
# my_dict 现在是 {}
选择建议 :如果你需要知道被删除的值,用 pop ;如果只是要删除,用 del ;如果想清空整个字典,用 clear ; popitem 则在特定顺序操作场景下使用。
3.4 检查键是否存在:in操作符
判断一个键是否在字典中,使用 in 操作符。这是最快、最pythonic的方式。
my_dict = {"a": 1, "b": 2}
if "a" in my_dict:
print("键 'a' 存在")
# 检查键不存在
if "c" not in my_dict:
print("键 'c' 不存在")
不要用 my_dict.get(key) 是否等于 none 来判断,因为值本身可能就是 none ,这样会产生歧义。
4. 遍历与迭代:高效获取数据
字典的遍历有多种方式,分别用于获取键、值或键值对。理解它们的区别和性能影响很重要。
4.1 遍历键:最直接的方式
直接对字典进行迭代,默认就是遍历它的键。
my_dict = {"name": "alice", "age": 25, "city": "london"}
for key in my_dict:
print(key)
# 输出:
# name
# age
# city
你也可以显式地使用 keys() 方法,它返回一个视图对象( dict_keys ),这个视图会动态反映字典的变化。
for key in my_dict.keys():
print(key)
在大多数情况下,直接迭代字典和迭代 my_dict.keys() 效果一样。但 keys() 视图在某些需要集合操作的场景下更方便,比如求两个字典键的交集: my_dict.keys() & other_dict.keys() 。
4.2 遍历值:使用values()方法
如果你只关心字典中的值,可以使用 values() 方法,它返回一个 dict_values 视图。
for value in my_dict.values():
print(value)
# 输出:
# alice
# 25
# london
4.3 遍历键值对:使用items()方法
这是最常用的遍历方式,可以同时拿到键和值。 items() 返回一个 dict_items 视图,其中每个元素是一个 (key, value) 元组。
for key, value in my_dict.items():
print(f"{key}: {value}")
# 输出:
# name: alice
# age: 25
# city: london
重要技巧 :在遍历过程中修改字典(如删除项)是危险的,可能会引发 runtimeerror 。安全的做法是先收集要处理的键,遍历结束后再统一修改,或者遍历其键或项的副本。
# 错误示例:在遍历时删除
# for key in my_dict:
# if some_condition(key):
# del my_dict[key] # 可能引发 runtimeerror
# 正确做法1:先记录要删除的键
keys_to_delete = []
for key, value in my_dict.items():
if value == 25: # 假设要删除值为25的项
keys_to_delete.append(key)
for key in keys_to_delete:
del my_dict[key]
# 正确做法2(python 3+):遍历keys()的副本
for key in list(my_dict.keys()):
if my_dict[key] == 25:
del my_dict[key]
5. 字典的合并与更新
在实际项目中,我们经常需要将多个字典的内容合并到一起。python提供了几种方法,它们的行为有细微差别。
5.1update()方法:就地更新
update() 方法将一个字典(或键值对序列)中的所有项合并到当前字典中。如果键重复,后者的值会覆盖前者的值。这个操作是 就地修改 原字典。
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
dict1.update(dict2)
print(dict1) # 输出: {'a': 1, 'b': 3, 'c': 4}
# dict1被修改了,dict2不变
update() 也可以接受关键字参数或其他可迭代对象。
dict1.update(d=5, e=6) # 使用关键字参数
# dict1 现在是 {'a': 1, 'b': 3, 'c': 4, 'd': 5, 'e': 6}
5.2 合并运算符|和|=(python 3.9+)
从python 3.9开始,引入了字典合并运算符,让合并操作更直观。
| (合并运算符):创建一个新字典,包含两个字典的所有项。重复键的值来自右边的操作数。
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
merged_dict = dict1 | dict2
print(merged_dict) # 输出: {'a': 1, 'b': 3, 'c': 4}
print(dict1) # 输出: {'a': 1, 'b': 2} (原字典未变)
|= (更新运算符):相当于 update() 的增强版,就地更新左边的字典。
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
dict1 |= dict2
print(dict1) # 输出: {'a': 1, 'b': 3, 'c': 4}
如何选择? 如果你需要保留原始字典不变,就使用 | 运算符或后面提到的字典解包。如果你明确要修改原字典,使用 update() 或 |= 。
5.3 字典解包**(python 3.5+)
在函数调用和字典字面量中,可以使用 ** 运算符来解包字典。这为合并多个字典提供了一种非常优雅的方式。
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
dict3 = {"d": 5}
# 合并多个字典到一个新字典
merged_dict = {**dict1, **dict2, **dict3}
print(merged_dict) # 输出: {'a': 1, 'b': 3, 'c': 4, 'd': 5}
# 注意:dict2中的'b'覆盖了dict1中的'b'
解包语法非常灵活,你可以在创建新字典时混合使用解包和显式的键值对。
base_config = {"host": "localhost", "port": 8080}
user_config = {"port": 9090, "debug": true}
final_config = {**base_config, **user_config, "timeout": 30}
# 结果: {'host': 'localhost', 'port': 9090, 'debug': true, 'timeout': 30}
6. 字典的常用方法进阶
除了基础的增删改查,字典还有一些内置方法能解决特定问题,极大地提升代码效率。
6.1setdefault(key, default):安全的“获取或设置”
这个方法用于安全地获取一个值。如果键存在,则返回其值;如果键不存在,则先将 key: default 插入字典,再返回 default 。
my_dict = {"a": 1}
# 键存在,直接返回值
value = my_dict.setdefault("a", 100)
print(value) # 输出: 1
print(my_dict) # 输出: {'a': 1} (字典未变)
# 键不存在,插入并返回默认值
value = my_dict.setdefault("b", 2)
print(value) # 输出: 2
print(my_dict) # 输出: {'a': 1, 'b': 2} (字典已更新)
经典应用场景:分组计数或初始化复杂值 。比如统计一段文本中单词出现的频率:
text = "apple banana apple orange banana apple"
word_count = {}
for word in text.split():
# 如果word不在字典中,将其计数初始化为0,然后加1
# 如果word已在字典中,获取当前计数,然后加1
word_count[word] = word_count.setdefault(word, 0) + 1
print(word_count) # 输出: {'apple': 3, 'banana': 2, 'orange': 1}
在没有 setdefault 的情况下,你需要写一个 if...else 判断,代码会冗长一些。
6.2 视图对象:keys(),values(),items()
前面提到这些方法返回的是“视图对象”,而不是列表。这一点非常重要。视图对象是动态的,它们会实时反映字典的变化,并且支持集合操作。
my_dict = {"a": 1, "b": 2}
keys_view = my_dict.keys()
values_view = my_dict.values()
items_view = my_dict.items()
print(keys_view) # 输出: dict_keys(['a', 'b'])
print(values_view) # 输出: dict_values([1, 2])
print(items_view) # 输出: dict_items([('a', 1), ('b', 2)])
# 动态性演示
my_dict["c"] = 3
print(keys_view) # 输出: dict_keys(['a', 'b', 'c']) 视图同步更新了!
视图对象还支持集合的交集( & )、并集( | )、差集( - )等操作( values() 视图除外,因为值可能不唯一)。
dict1 = {"a": 1, "b": 2, "c": 3}
dict2 = {"b": 20, "c": 3, "d": 4}
# 找出两个字典中都有的键
common_keys = dict1.keys() & dict2.keys()
print(common_keys) # 输出: {'b', 'c'}
# 找出在dict1中但不在dict2中的键
unique_to_dict1 = dict1.keys() - dict2.keys()
print(unique_to_dict1) # 输出: {'a'}
6.3copy():浅拷贝与深拷贝的陷阱
copy() 方法返回字典的一个浅拷贝(shallow copy)。这意味着它创建了一个新字典,但新字典中的值是对原字典中值的引用(对于可变对象)。
original = {"list": [1, 2, 3], "num": 10}
shallow_copied = original.copy()
# 修改浅拷贝字典中的不可变值,不影响原字典
shallow_copied["num"] = 20
print(original["num"]) # 输出: 10 (未变)
# 修改浅拷贝字典中的可变值(列表),原字典也会被影响!
shallow_copied["list"].append(4)
print(original["list"]) # 输出: [1, 2, 3, 4] (被修改了!)
这就是一个常见的“坑”。如果你需要完全独立地复制一个字典,包括它内部所有嵌套的可变对象,你需要使用 copy 模块的 deepcopy 函数。
import copy
original = {"list": [1, 2, 3]}
deep_copied = copy.deepcopy(original)
deep_copied["list"].append(4)
print(original["list"]) # 输出: [1, 2, 3] (不受影响)
经验法则 :如果字典的值只包含不可变对象(数字、字符串、元组),用 copy() 就够了。如果字典嵌套了列表、字典或其他可变对象,并且你希望副本完全独立,一定要用 deepcopy() 。
7. 嵌套字典与复杂数据访问
现实世界的数据很少是扁平的。我们经常要处理嵌套的字典,比如从json api返回的数据。安全、优雅地访问嵌套数据是一项必备技能。
7.1 链式访问与keyerror风险
访问嵌套字典最直接的方式是链式使用 [] 。
data = {
"user": {
"name": "bob",
"address": {
"city": "new york",
"zipcode": "10001"
}
}
}
city = data["user"]["address"]["city"] # 得到 'new york'
但这种方式的缺点是,如果中间任何一层键不存在,就会立即抛出 keyerror ,导致程序中断。
7.2 使用get()方法进行安全链式访问
我们可以通过多次调用 get() 来避免异常,但代码会变得冗长。
city = data.get("user", {}).get("address", {}).get("city")
# 如果任何一层缺失,city将为none
这虽然安全,但写起来麻烦,尤其是嵌套很深的时候。
7.3 更优雅的解决方案:collections模块的chainmap
chainmap 可以将多个字典链接成一个逻辑上的单一映射。查找时,它会按顺序在多个字典中搜索,直到找到第一个匹配的键。这可以用来为嵌套访问提供默认值,但它更适合管理多个上下文的配置,而不是纯粹的深层访问。
7.4 终极方案:自定义递归访问函数或使用第三方库
对于复杂的嵌套字典访问,一个更健壮的方法是写一个辅助函数。
def safe_get(dict_obj, keys, default=none):
"""
安全地获取嵌套字典中的值。
:param dict_obj: 目标字典
:param keys: 键的列表或元组,如 ['user', 'address', 'city']
:param default: 如果路径不存在,返回的默认值
:return: 找到的值或默认值
"""
current = dict_obj
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current
# 使用示例
city = safe_get(data, ["user", "address", "city"]) # 返回 'new york'
country = safe_get(data, ["user", "address", "country"], "usa") # 路径不存在,返回默认值 'usa'
对于大型项目,你可能会考虑使用像 glom 或 jmespath 这样的第三方库,它们提供了更强大、更声明式的语法来查询复杂的嵌套结构。
8. 字典排序:从无序到有序
在python 3.7之前,字典是无序的,你无法依赖其插入顺序。从python 3.7开始,字典会 保持插入顺序 ,这是一个官方保证的语言特性。这意味着你遍历字典时,项的顺序就是你添加它们的顺序。
但“保持插入顺序”不等于“按某种规则排序”。如果你需要按键或按值进行排序,仍然需要额外的操作。
8.1 按键排序
使用内置的 sorted() 函数,它可以对字典的键(通过 keys() 获取)进行排序,然后你可以根据排序后的键来访问值。
my_dict = {"banana": 3, "apple": 4, "pear": 1, "orange": 2}
# 按键升序排序
for key in sorted(my_dict.keys()):
print(key, my_dict[key])
# 输出:
# apple 4
# banana 3
# orange 2
# pear 1
# 按键降序排序
for key in sorted(my_dict.keys(), reverse=true):
print(key, my_dict[key])
更简洁的方式是直接对 items() 返回的 (key, value) 元组进行排序。因为元组比较时,默认先比较第一个元素(即键)。
# 按键排序,并直接获得排序后的键值对列表
sorted_items = sorted(my_dict.items())
print(sorted_items) # 输出: [('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)]
for key, value in sorted_items:
print(key, value)
8.2 按值排序
按值排序稍微复杂一点,因为我们需要告诉 sorted() 函数根据每个键值对的第二个元素(即值)来排序。这可以通过 key 参数实现。
my_dict = {"banana": 3, "apple": 4, "pear": 1, "orange": 2}
# 按值升序排序
sorted_by_value = sorted(my_dict.items(), key=lambda item: item[1])
print(sorted_by_value) # 输出: [('pear', 1), ('orange', 2), ('banana', 3), ('apple', 4)]
# 按值降序排序
sorted_by_value_desc = sorted(my_dict.items(), key=lambda item: item[1], reverse=true)
print(sorted_by_value_desc) # 输出: [('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)]
这里的 lambda item: item[1] 是一个匿名函数,它接收一个元组 item (如 ('banana', 3) ),然后返回其第二个元素 3 , sorted() 函数就根据这个返回值进行排序。
8.3 使用operator模块
对于简单的按值排序,使用 lambda 很清晰。但 operator 模块提供了更高效且可读性可能更好的方式。
import operator # 按值排序(使用itemgetter获取元组的第二个元素) sorted_by_value = sorted(my_dict.items(), key=operator.itemgetter(1)) # 按键排序(使用itemgetter获取元组的第一个元素) sorted_by_key = sorted(my_dict.items(), key=operator.itemgetter(0))
operator.itemgetter(1) 会创建一个函数,这个函数的作用和 lambda x: x[1] 一样,但通常性能稍好一些,尤其是在处理大量数据时。
重要提示 : sorted() 函数返回的是一个 列表 ,而不是字典。如果你需要一个保持排序顺序的字典,python 3.7+的普通字典就可以,因为你只需要按排序后的顺序插入项。或者,你可以使用 collections.ordereddict (在python 3.7后与普通字典在有序性上功能重叠,但仍有其特定方法)。
# 创建一个按键排序的新字典 (python 3.7+)
sorted_dict = dict(sorted(my_dict.items(), key=lambda item: item[0]))
print(sorted_dict) # 输出: {'apple': 4, 'banana': 3, 'orange': 2, 'pear': 1}
# 这个字典的遍历顺序就是按键排序的顺序
9. 字典与json的亲密关系
字典和json(javascript object notation)可以说是天生一对。json是一种轻量级的数据交换格式,其结构(对象、数组、字符串、数字、布尔值、null)几乎可以直接映射到python的数据类型(字典、列表、字符串、int/float、true/false、none)。
9.1 字典转json字符串:json.dumps()
使用 json 模块的 dumps() 函数,可以将字典序列化为json格式的字符串。
import json
data_dict = {
"name": "小明",
"age": 20,
"courses": ["数学", "物理"],
"is_student": true,
"address": none
}
json_str = json.dumps(data_dict, ensure_ascii=false, indent=2)
print(json_str)
# 输出格式化的json字符串:
# {
# "name": "小明",
# "age": 20,
# "courses": ["数学", "物理"],
# "is_student": true,
# "address": null
# }
参数说明 :
ensure_ascii=false:确保中文字符正常显示,而不是被转义为\u形式。indent=2:使输出的json字符串带有缩进,更易读。在生产环境为了节省空间通常不设置。sort_keys=true:可选参数,对输出的键进行排序。
9.2 json字符串转字典:json.loads()
反过来,使用 loads() 函数可以将json字符串解析为python字典。
json_string = '{"name": "小红", "age": 22, "city": "上海"}'
parsed_dict = json.loads(json_string)
print(parsed_dict["name"]) # 输出: 小红
print(type(parsed_dict)) # 输出: <class 'dict'>
9.3 处理文件
更常见的场景是从文件读取json或写入json。
# 将字典写入json文件
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data_dict, f, ensure_ascii=false, indent=2)
# 从json文件读取到字典
with open("data.json", "r", encoding="utf-8") as f:
loaded_dict = json.load(f)
踩坑提醒 :json和python的数据类型并非完全一致。json中的 true 、 false 、 null 对应python的 true 、 false 、 none 。json的数值都是浮点数,但在python中 json.loads() 会智能地解析为 int 或 float 。最重要的是,json的键 必须是字符串 ,而python字典的键可以是多种不可变类型。如果你尝试序列化一个键为元组的字典,会得到 typeerror 。
10. 性能考量与高级技巧
字典以其o(1)的平均时间复杂度而闻名,但了解其内部机制和边界条件,能帮助你写出性能更好的代码。
10.1 键的选择:为什么必须是不可变类型?
字典的快速查找依赖于哈希表。每个键都会被计算出一个哈希值(通过 __hash__ 方法),这个值用于确定键值对在内存中的存储位置。如果键是可变对象(如列表、字典),其内容变化会导致哈希值变化,这就破坏了哈希表的完整性,使得之前存储的值无法被正确找到。因此,python要求字典的键必须是“可哈希的”(hashable),即不可变对象(如数字、字符串、元组(仅当元组内所有元素也都是可哈希的))。
# 有效的键
valid_dict = {
1: "integer", # 整数
"hello": "string", # 字符串
(1, 2): "tuple", # 元组(元素都是不可变的)
# [1,2]: "list" # 错误!列表不可哈希
# {"a":1}: "dict" # 错误!字典不可哈希
}
10.2 字典的大小与扩容
字典在内部会维护一个哈希表。当字典中的项数增加,使得负载因子(已用槽位/总槽位)超过某个阈值时,字典会进行扩容(resize),即分配一个更大的内存空间,并重新哈希(rehash)所有现有的键。这是一个相对昂贵的操作(o(n)时间复杂度)。
这意味着,如果你能提前知道字典的大致规模,在创建时指定一个初始容量,可以避免多次扩容,提升性能。
# 如果你知道大约要存1000个项,可以这样创建
d = dict.fromkeys(range(1000)) # 一种方式
# 或者,更直接地(虽然不能精确控制,但给了提示)
d = {i: none for i in range(1000)}
# python解释器会根据这个规模进行优化
10.3in操作符 vsget()vs[]的性能
检查一个键是否在字典中, in 操作符是最快的方式,因为它只进行哈希查找,不涉及值的返回或异常处理。 get() 方法在键不存在时返回默认值,比先用 in 检查再用 [] 访问要快,因为 get() 只做一次哈希查找。而直接使用 [] 访问不存在的键会引发异常,异常处理的开销是最大的。
性能排序(从快到慢) : in ≈ get() (键存在时) > [] (键存在时) > get() (键不存在,但避免了异常) >> [] (键不存在,引发异常)。
10.4 使用字典模拟switch-case语句
python没有 switch-case 语句,但我们可以用字典巧妙地模拟,使代码更简洁。
def handle_operation(operation, a, b):
# 定义一个操作到函数的映射字典
operations = {
"add": lambda x, y: x + y,
"subtract": lambda x, y: x - y,
"multiply": lambda x, y: x * y,
"divide": lambda x, y: x / y if y != 0 else "division by zero"
}
# 使用get方法,并提供默认处理函数
func = operations.get(operation, lambda x, y: f"unknown operation: {operation}")
return func(a, b)
print(handle_operation("add", 5, 3)) # 输出: 8
print(handle_operation("power", 2, 3)) # 输出: unknown operation: power
这种方法将逻辑分发(dispatch)从一长串 if-elif-else 语句中解放出来,使代码更易于维护和扩展。
10.5 使用字典进行缓存(memoization)
字典是实现缓存(memoization)的绝佳数据结构,常用于优化递归函数,避免重复计算。
def fibonacci(n, cache={}):
"""使用缓存计算斐波那契数列"""
if n in cache:
return cache[n]
if n <= 1:
result = n
else:
result = fibonacci(n-1, cache) + fibonacci(n-2, cache)
cache[n] = result
return result
print(fibonacci(50)) # 如果没有缓存,这个计算会非常慢
这里利用了一个技巧:默认参数 cache={} 在函数定义时被求值,并且对于每次函数调用,如果调用者没有提供 cache 参数,就会使用这个 同一个 默认字典对象。这相当于在函数外部隐藏了一个全局缓存,但封装得更好。不过要小心,这个缓存会在多次调用 fibonacci 时持续存在并累积。
以上就是python基础指南之字典核心操作与高级技巧全解析的详细内容,更多关于python字典操作的资料请关注代码网其它相关文章!
发表评论