嵌套字典缺失键的常见问题
在Python中操作嵌套字典时,如果直接通过键访问不存在的层级,会直接抛出KeyError异常。比如我们有一份用户信息的嵌套字典,其中部分用户的地址信息不完整,直接访问深层键就会出现问题。

示例字典结构如下:
user_data = {
"user1": {
"name": "张三",
"address": {
"city": "北京",
"street": "朝阳路"
}
},
"user2": {
"name": "李四"
# 缺失address键
}
}
如果直接执行user_data["user2"]["address"]["city"],就会因为user2下没有address键而抛出异常,这时候就需要我们提前处理缺失键的情况,给不存在的键填充NULL值。
基础处理方法及不足
使用get方法逐层判断
最常用的方式是使用字典的get方法,给不存在的键设置默认值,逐层访问可以避免异常:
# 获取user2的城市信息,缺失的键填充NULL
city = user_data.get("user2", {}).get("address", {}).get("city", "NULL")
print(city) # 输出NULL
这种方法在嵌套层级较少时还可以使用,但如果嵌套层级达到四五层甚至更多,代码会变得非常冗长,可读性和可维护性都会下降。
手动判断键是否存在
另一种方式是通过in关键字判断键是否存在,逐层校验:
if "user2" in user_data:
user2_info = user_data["user2"]
if "address" in user2_info:
address_info = user2_info["address"]
if "city" in address_info:
city = address_info["city"]
else:
city = "NULL"
else:
city = "NULL"
else:
city = "NULL"
print(city) # 输出NULL
这种方式的逻辑非常清晰,但是嵌套层级越多,缩进越深,代码冗余度越高,不适合复杂场景使用。
优雅的通用处理方案
自定义递归工具函数
我们可以编写一个递归函数,接收嵌套字典、要访问的键路径以及默认值,自动处理所有层级的缺失键:
def get_nested_value(data, key_path, default="NULL"):
"""
递归获取嵌套字典中的值,缺失键填充默认值
:param data: 嵌套字典
:param key_path: 键路径,用列表表示,比如["address", "city"]
:param default: 缺失键的默认值
:return: 对应键的值或默认值
"""
current = data
for key in key_path:
# 判断当前对象是否是字典且包含当前键
if isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current
# 测试使用
user_data = {
"user1": {
"name": "张三",
"address": {
"city": "北京",
"street": "朝阳路"
}
},
"user2": {
"name": "李四"
}
}
# 获取user1的城市
print(get_nested_value(user_data["user1"], ["address", "city"])) # 输出北京
# 获取user2的城市,缺失键返回NULL
print(get_nested_value(user_data["user2"], ["address", "city"])) # 输出NULL
# 获取不存在的用户的信息
print(get_nested_value(user_data.get("user3", {}), ["address", "city"])) # 输出NULL
这个函数可以适配任意层级的嵌套字典,只需要传入键的路径列表即可,不需要修改代码逻辑,扩展性很强。
使用defaultdict实现自动填充
如果我们需要的不是临时获取值,而是直接修改原字典,给所有缺失的键填充NULL值,可以使用collections模块的defaultdict结合递归实现:
from collections import defaultdict
def fill_nested_dict(data, key_path, fill_value="NULL"):
"""
递归填充嵌套字典的缺失键
:param data: 嵌套字典
:param key_path: 要填充的键路径,用列表表示
:param fill_value: 填充的默认值
"""
current = data
for i, key in enumerate(key_path):
# 如果是最后一层键,直接赋值默认值
if i == len(key_path) - 1:
if key not in current:
current[key] = fill_value
else:
# 如果当前键不存在,创建defaultdict继续嵌套
if key not in current:
current[key] = defaultdict(dict)
current = current[key]
# 测试使用
user_data = {
"user2": {
"name": "李四"
}
}
# 填充address和city键
fill_nested_dict(user_data, ["address", "city"])
print(user_data)
# 输出:{'user2': {'name': '李四', 'address': {'city': 'NULL'}}}
这种方式会直接修改原字典结构,适合需要对字典进行持久化修改的场景,比如数据清洗时统一补全缺失字段。
结合reduce函数简化路径访问
如果熟悉函数式编程,也可以使用functools模块的reduce函数简化键路径的访问逻辑:
from functools import reduce
def get_value_by_path(data, key_path, default="NULL"):
"""
使用reduce函数获取嵌套字典的值
:param data: 嵌套字典
:param key_path: 键路径,用列表表示
:param default: 默认值
:return: 对应值或默认值
"""
try:
# 逐层获取值,遇到不存在的键返回默认值
return reduce(lambda d, key: d.get(key, {}), key_path, data)
except AttributeError:
return default
# 测试使用
user_data = {
"user1": {
"name": "张三",
"address": {
"city": "北京"
}
},
"user2": {
"name": "李四"
}
}
# 获取user1的城市
print(get_value_by_path(user_data["user1"], ["address", "city"])) # 输出北京
# 获取user2的城市
print(get_value_by_path(user_data["user2"], ["address", "city"])) # 输出{}
# 可以结合get方法优化默认值
result = get_value_by_path(user_data["user2"], ["address", "city"])
print(result if result != {} else "NULL") # 输出NULL
这种写法代码更简洁,但是需要额外处理返回空字典的情况,适合熟悉函数式编程的开发者使用。
不同场景的选择建议
- 如果嵌套层级少、临时获取值,优先使用
get方法链式调用,简单直接 - 如果需要适配任意层级、频繁获取值,优先使用自定义递归工具函数,扩展性强
- 如果需要修改原字典、补全所有缺失字段,优先使用
defaultdict结合的递归方案 - 如果团队熟悉函数式编程,可选择
reduce函数的实现方式,代码更简洁
实际开发中可以根据具体场景选择最合适的方案,核心目标是让代码简洁、可读、易维护,同时避免因为缺失键导致的程序异常。