这篇文章总结在开发中使用字典的一些技巧。
字典存储的内容不是单一维度的线性序列,而是多维度的key: value键值对。
字典也有两种定义方式,字面量表达式和dict() 内置函数。
# 字面量表达式创建字典
movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
# 内置函数创建字典
d = dict(name='Burning', type='movie')
# 遍历字典获取key和value
movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
for key, value in movie.items():
print(f'遍历输出字典的key和value:{key}: {value}')
当用不存在的键访问字典内容时,程序会抛出KeyError异常,我们称为程序的边界情况。常用处理方式有两种。
# 获取字典中let键的值,如果该键不存在则返回一个默认值
# 第一种写法判断key是否存在
if 'let' in movie:
let = movie['let']
else:
let = 0
# 第二种写法捕获异常
try:
let = movie['let']
except KeyError:
let = 0
除了上面的两种方法,字典还提供了一个get方法获取key的值,它接收两个参数,其中default参数,当访问的key不存在时,方法会返回default默认值。
# 当key不存在时返回设置的默认值
movie.get('let', 0)
有时,我们需要修改字典中某个可能不存在的键,比如下面的代码示例,我需要往字典movie的hobby键里追加新值,但movie[‘hobby’] 的键可能不存在。因此需要写一段异常捕捉逻辑,如下面示例。
movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
try:
movie['hobby'].append('读书')
except KeyError:
movie['hobby'] = ['读书']
print(f'字典添加新值:{movie}')
# 输出结果
字典添加新值:{'name': 'Burning', 'type': 'movie', 'year': 2018, 'hobby': ['读书']}
针对上面的情况,还有一个更适合的工具:setdefault(key, default=None) 方法。使用它可以直接删掉上面异常捕获,代码会变得更简单。
setdefault(key, default=None) 方法会产生两种结果,当key不存在时,该方法会把default值写入字典的key位置,并返回该值;当key存在时,该方法就会直接返回字典中的值。
movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
movie.setdefault('hobby',[]).append('读书')
print(f'setdefault方法添加新值:{movie}')
# 输出结果
setdefault方法添加新值:{'name': 'Burning', 'type': 'movie', 'year': 2018, 'hobby': ['读书']}
当删除字典中某个键,一般会使用del d[key] ;但如果要删除的键不存在就会抛出KeyError异常,因此要安全的删除某个键,需要加上一段异常捕获。
movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
# del删除不存在的键会报KeyError异常
del movie['hobby']
# 异常信息
KeyError: 'hobby'
# 捕获异常
try:
del movie['hobby']
except KeyError:
print(KeyError)
假如只是想去掉某个键,不关系它存在与否,删除是否成功,使用dict.pop(key, default) 方法代码更简单,省去了捕获异常的代码,同时当键不存在时也不会抛出异常。
movie.pop('year',None)
print(f'使用pop删除键:{movie}')
# 输出结果
使用pop删除键:{'name': 'Burning', 'type': 'movie'}
movie.pop('hobby', None)
print(f'使用pop删除不存在的键:{movie}')
# 输出结果
使用pop删除不存在的键:{'name': 'Burning', 'type': 'movie'}
字典和列表一样都有自己的推导式,可以用它来过滤和处理字典成员。
movie = {'name': 'Burning', 'type': 'movie', 'year': 2018}
d = {key:value for key, value in movie.items() if key == 'year'}
print(f'列表推导式,创建一个新的字典:{d}')
在Python3.6版本以前,python的字典是无序的。例如当你按照某种顺序把内容放进字典后,获取字典内容时顺序就变量,这是因为字典的key是按照hash值排序。但Python在进化,Python3.6版本为字典引入了一个改进,优化了底层实现,同时实现了存放数据和读取数据顺序一致。
在python的collections模块里还有一个有序字典对象OrderedDict,他可以保证在在3.6之前的版本里字典是有序的。
既然普通的字典已经实现了有序性,那么OrderedDict是否还有必要保留吗。其实他们还是有一个细微的差别的,比如在对比两个内容相同而顺序不同的字典时,普通对象会返回True,而OrderedDict会返回False。
d1 = {'a': 'a', 'b': 'b', 'c': 'c'}
d2 = {'c': 'c', 'a': 'a', 'b': 'b'}
print(f'对比两个普通字典对象是否一致: {d1 == d2}')
# 输出结果
对比两个普通字典对象是否一致: True
from collections import OrderedDict
d1 = OrderedDict(a='a', b='b')
d2 = OrderedDict(b='b', a='a')
print(f'对比两个有序字典对象是否一致: {d1 == d2}')
# 输出结果
对比两个有序字典对象是否一致: False
案例特点
# 日志分析案例
from enum import Enum
class PagePerflevel(str, Enum):
LT_100 = 'Less than 100ms'
LT_300 = 'Between 100 and 300ms'
LT_1000 = 'Between 300 and 1s'
Gt_1000 = 'Greater than 1s'
def analyze_v1():
with open("analyze_log.txt", "r") as fp:
path_group = {}
# 格式化日志内容
for line in fp:
path, cost_time = line.strip().split()
# 将响应时间转化为对应的等级
cost_time = int(cost_time)
if 100 > cost_time:
level = PagePerflevel.LT_100
elif 100 < cost_time < 300:
level = PagePerflevel.LT_300
elif 300 < cost_time < 1000:
level = PagePerflevel.LT_1000
elif 1000 < cost_time:
level = PagePerflevel.Gt_1000
# 按照路径分类,统计不同耗时等级的次数
if path not in path_group:
path_group[path] = {}
try:
path_group[path][level] += 1
except KeyError:
path_group[path][level] = 1
# 输出结果
for path, result in path_group.items():
print(f'path: {path}')
total = sum(result.values())
print(f' Total request: {total}')
print(f' Performance:')
sorted_item = sorted(result.items(), key=lambda pair: list(PagePerflevel).index(pair[0]))
for level_name, count in sorted_item:
print(f' - {level_name}: {count}')
analyze_v1()
日志文件内容
/api/home/ 120
/api/home/ 100
/api/home/ 200
/api/home/ 400
/api/user/ 100
/api/user/ 200
/api/user/ 300
/api/user/ 300
/api/shop/ 100
/api/shop/ 150
/api/shop/ 200
/api/shop/ 2000
/api/shop/ 2000
/api/shop/ 2000
defaultdict(default_factory, …) 是一种特殊的字典类型,他在被初始化时,接收一个可调用对象default_factory作为参数。之后每次操作字典时,如果访问的key不存在,defaultdict对象会自动调用default_factory()并将结果作为值保存在对应的key里。
为了更好理解defaultdict类型字典,下面是一个使用示例。
# defaultdict类型字典
from collections import defaultdict
int_dict = defaultdict(int)
print(f'初始化defaultdict类型字典,参数类型是int:{int_dict}')
int_dict['foo'] += 1
print(f'字典赋值,当key foo不存在时,自动调用default_factory也就是int()初始化值为0,然后赋值。foo的值为:{int_dict}')
自定义字典和普通字典很像,但它可以给字典的默认行为添加一些自定义功能。比如我们让字典的操作 “响应耗时” 键时自动转为性能等级。
# 创建自定义字典
from collections.abc import MutableMapping
class PerfLevelDict(MutableMapping):
def __init__(self):
self.data = defaultdict(int)
def __getitem__(self, key):
return self.data[self.compute_level(key)]
def __setitem__(self, key, value):
self.data[self.compute_level(key)] = value
def __delitem__(self, key):
del self.data[key]
def __iter__(self):
return iter(self.data)
def __len__(self):
return len(self.data)
@staticmethod
def compute_level(time_cost_str):
'''
根据响应耗时转换为性能等级,如果传来的值已经是性能等级,不做
转换直接返回。
'''
if time_cost_str in list(PagePerflevel):
return time_cost_str
time_cost = int(time_cost_str)
if time_cost < 100:
return PagePerflevel.LT_100
elif time_cost < 300:
return PagePerflevel.LT_300
elif time_cost < 1000:
return PagePerflevel.LT_1000
return PagePerflevel.Gt_1000
d = PerfLevelDict()
d[50] += 1
d[500] += 2
print(dict(d))
在上面代码中,编写了一个继承了MutableMapping的字典类PerfLevelDict。但是光继承还不够,要让这个类变得像字典一样,需要重写包括 getitem、setitem、在内的6个魔法方法。下面介绍下重要的几个步骤
from collections import defaultdict
from collections.abc import MutableMapping
from enum import Enum
class PagePerfLevel(Enum):
LT_100 = "Less than 100 ms"
LT_300 = "Between 100 and 300 ms"
LT_1000 = "Betten 300ms and 1 s"
GT_1000 = "Greater than 1 s"
class PerfLevelDict(MutableMapping):
def __init__(self):
self.data = defaultdict(int)
print("---",self.data)
def __getitem__(self, key):
get_data = self.data[self.compute_level(key)]
print("get_data",get_data)
return get_data
def __setitem__(self, key, value):
print("key",key)
print("value",value)
self.data[self.compute_level(key)] = value
print("data",self.data)
print("data key",self.data.keys())
def __delitem__(self, key):
del self.data[key]
def __iter__(self):
return iter(self.data)
def __len__(self):
return len(self.data)
def items(self):
return sorted(
self.data.items(),
key=lambda pair:list(PagePerfLevel).index(pair[0])
)
def total_requests(self):
return sum(self.values())
@staticmethod
def compute_level(time_cost_str):
if time_cost_str in list(PagePerfLevel):
return time_cost_str
time_cost = int(time_cost_str)
if time_cost<100:
print(PagePerfLevel.LT_100)
return PagePerfLevel.LT_100
elif time_cost<300:
return PagePerfLevel.LT_300
elif time_cost<1000:
return PagePerfLevel.LT_1000
return PagePerfLevel.GT_1000
def analy_2():
# 初始化一个空的defaultdict字典,default_factory参数类型为PerfLevelDict
path_groups = defaultdict(PerfLevelDict)
with open("analyze_log.txt", "r") as fp:
for line in fp:
path,time_cost = line.strip().split()
# 使用defaultdict字典添加key和value
path_groups[path][time_cost] +=1
for path, result in path_groups.items():
print(f'==Path: {path}')
# result对象类型是 PerfLevelDict 因此可以调用total_requests方法
print(f' Total request: {result.total_requests()}')
print(f'Performance')
for level_name, count in result.items():
print(f' {level_name}:{count}')
if __name__ == '__main__':
analy_2()
合并字典使用**运算符来解包,在字典中可以动态解包字典任何内容,并与当前字典合并
d1 = {'name': 'apple'}
d2 = {'foo': 'banana'}
d3 = {**d1, **d2}
print(d3)
# 输出结果
{'name': 'apple', 'foo': 'banana'}
除了使用**解包字典,可以使用单星号解包任何可迭代对象
l1 = [1, 2]
l2 = [3, 4]
l3 = [*l1, *l2]
集合里的成员是不会重复的,因此经常使用它来去重。但是,使用集合去重有一个很大的缺点,得到的结果会丢失原有的排序。
如果既需要去重,有要保持排序,可以使用有序字典来去重。
OrderedDict同时满足两个条件
因此只要根据列表构建一个字典,它的键就是有序去重的结果。
# 有序字典去重
nums = [10, 2, 3, 10, 4, 6]
from collections import OrderedDict
# 调用fromkeys方法会创建一个有序字典对象,值默认为None
l = list(OrderedDict.fromkeys(nums).keys())
print(l)
标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误地修改某个映射。
假如给客户端暴露一个字典对象,里面的数据都是提前初始化好的,并且不希望调用方修改字典中的数据,只能访问字典中的数据。例如给客户端返回一个错误代码集合,包含了错误号代表了不同的含义。
从Python 3.3开始,types模块中引入了一个封装类名叫MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。
from types import MappingProxyType
d = {'a': 1, 'b': 2}
d_proxy = MappingProxyType(d)
print(f'输出不可变类型字典内容:{d_proxy}')
# 输出
输出不可变类型字典内容:{'a': 1, 'b': 2}
print(f'读取不可变类型字典内容:{d_proxy["a"]}')
#输出
读取不可变类型字典内容:1
d['c'] = 3
print(f'原字典内容可修改:{d_proxy}')
# 输出
原字典内容可修改:{'a': 1, 'b': 2, 'c': 3}
d_a3 = d_proxy["a"] = 3
print('不可变类型字典不可修改内容:', d_a3)
#输出
TypeError: 'mappingproxy' object does not support item assignment