ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

手写“判断key存不存在“的模板代码太烦?一文讲透 collections 四件套

手写“判断key存不存在“的模板代码太烦?一文讲透 collections 四件套 「Python 进阶之路」系列 Day28写在前面collections模块里有一批dict/tuple的增强版几乎每个 Python 项目都会用到。今天把最常用的四个——defaultdict、Counter、OrderedDict、namedtuple——一次讲透重点回答一个经常被问到的疑问Python 3.7 后dict已经有序了OrderedDict是不是已经没用了一、是什么dict 和 tuple 的增强版工具箱collections模块提供了一批dict/tuple的增强版各自针对一种常见的使用痛点做了专门优化dictdefaultdict自动创建默认值Counter专门用来计数OrderedDict顺序敏感的相等比较tuplenamedtuple按名字访问二、为什么各自解决了什么手写代码的痛点defaultdict省掉手写判断 key 是否存在不存在就先初始化这种模板代码Counter省掉手写计数前先判断 key 存不存在的模板代码还顺带提供排序、数学运算这些计数场景的常用操作OrderedDict在 dict 已经天然有序的今天它解决的是顺序本身要不要参与相等比较、要不要支持手动调整顺序这类更细粒度的需求namedtuple在用索引访问可读性差的裸 tuple和写一个完整类太重之间找一个折中三、怎么用1. defaultdict省掉判断key存不存在的模板代码fromcollectionsimportdefaultdict ddefaultdict(list)d[fruits].append(apple)# 不需要先判断key存不存在d[fruits].append(banana)print(dict(d))# {fruits: [apple, banana]}normal_d{}normal_d[fruits].append(apple)# KeyError: fruits —— 普通dict必须先手动判断/初始化最典型的应用场景是分组统计students[(Tom,A),(Jerry,B),(Alice,A),(Bob,C),(Eve,B)]groupsdefaultdict(list)forname,gradeinstudents:groups[grade].append(name)print(dict(groups))# {A: [Tom, Alice], B: [Jerry, Eve], C: [Bob]}2. Counter专门用来计数Counter是dict的子类专门用来计数最大的便利是访问不存在的 key 会返回 0而不是报错fromcollectionsimportCounter cCounter(abracadabra)print(c)# Counter({a: 5, b: 2, r: 2, c: 1, d: 1})print(c[z])# 0不报错这是Counter和普通dict的关键区别配合most_common()方法可以直接拿到出现次数最多的前 N 项wordsthe quick brown fox jumps over the lazy dog the fox runs.split()word_countCounter(words)print(word_count.most_common(3))# [(the, 3), (fox, 2), (quick, 1)]Counter还支持直接做数学运算c1Counter(a3,b1)c2Counter(a1,b2)print(c1c2)# Counter({a: 4, b: 3})print(c1-c2)# Counter({a: 2}) —— 相减结果为负数或0的会被丢弃3. OrderedDict3.7后还有存在的意义吗Day26 讲过Python 3.7 起普通dict已经保证插入顺序了那OrderedDict是不是已经过时实测发现它还有两个普通dict没有的能力move_to_end()方法普通 dict 没有fromcollectionsimportOrderedDict odOrderedDict()od[a]1od[b]2od[c]3od.move_to_end(a)print(list(od.keys()))# [b, c, a]d1{}d1.move_to_end(a)# AttributeError: dict object has no attribute move_to_end这个方法常用来实现 LRU 缓存——每次访问一个 key 就把它挪到末尾最久没被访问的自然留在开头方便淘汰。相等比较会考虑顺序普通 dict 不会od1OrderedDict([(a,1),(b,2)])od2OrderedDict([(b,2),(a,1)])print(od1od2)# False —— OrderedDict比较相等时会考虑顺序d2a{a:1,b:2}d2b{b:2,a:1}print(d2ad2b)# True —— 普通dict比较相等不考虑顺序只看键值对所以OrderedDict并没有过时普通dict只是顺序稳定能保证遍历顺序但语义上依然把顺序当成无关紧要的实现细节OrderedDict把顺序当成这个对象身份的一部分需要顺序本身也参与判断的场景LRU 缓存、需要严格比较两个有序结构是否完全一致依然要用它。4. namedtuple给tuple的每个位置起名字普通tuple用索引访问可读性差point[0]不知道是 x 还是 y写一个完整的类又太重要手写__init__/__repr__/__eq__namedtuple是这两者之间的折中fromcollectionsimportnamedtuple Pointnamedtuple(Point,[x,y])pPoint(1,2)print(p.x,p.y)# 1 2 —— 属性访问可读性好print(p[0],p[1])# 1 2 —— 依然支持索引访问print(isinstance(p,tuple))# True —— namedtuple确实是tuple的子类x,yp# 支持解包print(x,y)# 1 2p.x100# AttributeError: cant set attribute —— 依然不可变和普通tuple一样更现代的写法是用typing.NamedTuple用类型注解语法定义字段可读性更好、还能配合类型检查工具fromtypingimportNamedTupleclassPoint2(NamedTuple):x:inty:intp2Point2(3,4)print(p2,p2.x,isinstance(p2,tuple))# Point2(x3, y4) 3 True四、面试追问Q1defaultdict 解决了什么问题访问不存在的 key 时自动用工厂函数创建一个默认值而不是抛KeyError省掉手写判断 key 是否存在再初始化的模板代码最典型的应用场景是分组统计。Q2Counter 和普通 dict 计数相比有什么优势访问不存在的 key 直接返回 0 而不报错可以直接写counter[key] 1而不用先判断此外还提供most_common()方法快速取出现次数最多的项并支持/-这类数学运算直接合并/相减多个计数结果。Q3Python 3.7 后 dict 已经有序了OrderedDict 还有存在的意义吗有。OrderedDict提供了move_to_end()方法普通 dict 没有常用于实现 LRU 缓存并且它的相等比较会把顺序也纳入判断普通 dict 比较相等时不考虑顺序只看键值对本身这两点是普通 dict 做不到的。Q4namedtuple 解决了什么问题它和普通类相比有什么取舍解决了普通 tuple 用索引访问可读性差、写完整类又太重这个折中问题用命名字段访问的同时保留了 tuple 的轻量、不可变、可解包特性。取舍是它不能像普通类那样自由添加方法或可变状态适合表示简单的、不可变的数据结构。Q5这几个 collections 工具的共同设计思路是什么都是针对 dict/tuple 某个特定使用场景的痛点做专门优化而不是重新发明一套通用数据结构defaultdict解决默认值问题、Counter解决计数问题、OrderedDict解决顺序敏感问题、namedtuple解决可读性问题各自只专注做好一件事。下一篇预告Day29 讲functools模块——lru_cache、partial、wraps这几个装饰器/工具函数到底解决了什么问题。
返回列表