恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python高级数据类型进阶:collections容器、推导式与性能避坑
首页
资讯中心
/
Python高级数据类型进阶:collections容器、推导式与性能避坑
Python高级数据类型进阶:collections容器、推导式与性能避坑
发布时间:2026/9/24 23:14:19
如果你已经把Python基础语法过了一遍开始用列表存数据、用字典做映射每天写得不亦乐乎那这篇文章就是给你准备的。我见过太多初学者甚至一些写了两年Python的人list.append、dict.get用得飞起但一碰到稍微复杂一点的数据组织就绕来绕去写了三五十行回头一看别人一个defaultdict加推导式几行就搞定了。说白了差距不在语法在于对Python数据类型的理解深度。这篇文章要聊的就是Python高级数据类型的核心玩法四大内置容器的进阶用法、collections模块的五个大杀器、推导式与解包的实战技巧以及复制、内存、性能相关的坑。内容不贪多但每个点都保证是有真实场景支撑的干货。适合刚入门想进阶的初学者也适合写了一阵子但觉得自己代码总是“绕远路”的朋友。看完之后你写出来的代码会更短、更清晰也更能扛住真实项目的复杂度。1. 内容整体设计与思路拆解先说我为什么把这些东西统称为“高级数据类型”。Python官方文档里list、dict这些就是内置类型哪有什么高级低级之分。但实际使用中我发现同一个数据类型不同人用出来的效果天差地别。list在小白手里就是个能装东西的数组在高手手里能当栈、当队列、做二分插入、玩切片赋值。所以“高级”的含义是在恰当的场景下把这类型的能力用到极致而不是满足于它最基础的增删改查。这篇文章的拆解逻辑是这样的先从最常用的四大内置容器入手把列表、元组、字典、集合里那些“你用过但没深究”的进阶技巧一个个挖出来然后进入collections模块那里面的Counter、defaultdict、OrderedDict、namedtuple、deque每一个都是冲着解决真实痛点去的接着讲推导式、切片、解包这三个让代码“肉眼可见变短”的语法糖最后讲复制与内存的底层逻辑以及实盘里天天遇到的坑。这样设计的理由很简单内置容器是第一层几乎每天都在写但多数人只用了30%的能力collections是第二层知道了能让代码质量上一个台阶语法糖是第三层主要解决“写得爽”复制和性能是第四层解决“跑得稳”。四层走完你对Python数据类型的理解基本就立体了。另外说一句文章里的代码我都尽量控制在能直接跑起来的长度环境就是普通的Python 3.8以上不需要额外装第三方库collections是标准库直接import就行。这样你本地验证起来也没有门槛。2. 四大内置容器的高级用法详解2.1 列表不要只会append和索引列表是Python里用最多的容器但大部分人只用到了append、pop、while i len(arr)这种水平。先说一个我经常考别人的点list.extend和到底有什么区别很多人说“没区别”其实后者会触发__iadd__落到列表上等价于extend都是原地修改并不会新建列表。但你换个场景试试def add_items(items, new_items): items new_items # 原地修改 return items def add_items_bug(items, new_items): items items new_items # 新列表原来的items没变 return items第一种会改到外部传入的列表第二种不会。这个坑在函数传参时非常容易踩尤其当你预期函数“顺手把传入的列表也改了”时两种写法结果完全不同。再说排序。list.sort()是原地排序返回Nonesorted(list)返回新列表原列表不动。这个大家都懂但key参数很多人只会传lambda x: x。实际项目里我经常用keyoperator.itemgetter(1)取字典里某个字段排序或者用functools.cmp_to_key处理老式比较逻辑。比如有个列表存着{name: 张三, score: 88}想按分数从高到低排data [{name: 张三, score: 88}, {name: 李四, score: 95}] data.sort(keylambda item: item[score], reverseTrue)还有一个容易被忽略的bisect模块。如果数据一直在动态插入但又需要保持有序不要每次插完都sort一次用bisect.insort做二分插入时间复杂度从O(n log n)降到O(n)插入位置靠二分查找定位虽然移动元素还是O(n)但至少查找的那部分成本省掉了。import bisect scores [60, 70, 80, 90] bisect.insort(scores, 85) print(scores) # [60, 70, 80, 85, 90]列表当队列用是另一个经典坑pop(0)和insert(0, x)都是O(n)操作数据量一上来就卡。Python的列表在内存里是连续数组头部操作要整体搬移实在要用队列就换collections.deque后面会讲。2.2 元组不只是不可变的列表很多人把元组当“不能改的列表”用来临时存几个值。这样理解没大毛病但它限制了你看到元组更重要的身份记录。元组的每个位置是有语义的比如坐标(x, y)、RGB颜色(255, 128, 0)、数据库一行记录的字段集合。既然是记录配套的解包unpacking就是元组的灵魂。a, b (1, 2)这种基础解包不说了星号表达式才是真正好用的first, *middle, last [1, 2, 3, 4, 5] print(first, middle, last) # 1 [2, 3, 4] 5这个写法在“取首尾、忽略中间”的场景里是神器。比如日志分析里每条记录格式固定你要取第一项和最后一项中间不关心这样一行就搞定了。还有一个容易忽略的元组可以作为字典的键列表不行。因为元组不可变、可哈希列表不可哈希。所以当你需要用一个组合值做字典的键时比如“(城市, 日期)”作为统计维度请用元组stat {} stat[(北京, 2025-01-01)] stat.get((北京, 2025-01-01), 0) 1另外namedtuple其实是元组的加强版我放到collections部分细说。这里只提醒一句单元素元组的定义是(1,)不是(1)后者只是数字1外面加了个括号。这个错误我见新手犯过无数次。2.3 字典不只是键值对字典在Python 3.7之后默认保持插入顺序这个特性让字典在很多场景可以直接当“有序映射”用。但比起顺序更值得关注的是那几个“能少写一堆if”的方法。dict.get(key, default)是基本操作了但setdefault知道的人少一点。它的逻辑是键不存在就设置默认值并返回存在就直接返回原值。visited {} # 传统写法 if a not in visited: visited[a] [] visited[a].append(1) # setdefault写法 visited.setdefault(a, []).append(1)两行变一行而且读起来语义很清楚。不过这里要提醒一下setdefault(key, [])里的[]每次调用都会新建一个空列表只不过只有键不存在时那个新建的才生效。性能上影响微小但如果你在循环里高频调用可以考虑用defaultdict替代后面会讲。字典合并在3.9之后有专门的|操作符d1 {a: 1, b: 2} d2 {b: 3, c: 4} merged d1 | d2 # {a: 1, b: 3, c: 4} d1 | d2 # 原地更新update方法也能合并但|表达更清晰特别是你要“合并但不修改原字典”的时候。再配合解包{**d1, **d2}也是一种经典写法效果一样但你要是注重性能|在CPython底层实现上更直接。字典推导式我放后面语法糖部分讲这里只提一个和“视图”有关的关键点d.keys()、d.values()、d.items()返回的是动态视图不是静态列表。意思是你拿到keys之后去修改原字典视图也会跟着变。如果你想要一个独立快照得显式list(d.keys())。嵌套字典取值的痛点也很典型。比如一个三级嵌套d[a][b][c]中间任何一层不存在就直接KeyError。有人写一堆try except有人写连环getvalue d.get(a, {}).get(b, {}).get(c, None)这种写法能跑但每层都新建空字典性能不好看而且代码丑。后面讲defaultdict的时候我专门说怎么优雅地处理多层嵌套。2.4 集合不只是去重集合最广为人知的用途是去重list(set(items))。但你要是只知道这一点就浪费了集合这个“数学课上最有用的数据结构”。集合的核心能力是集合运算交集、并集、差集、对称差集。a {1, 2, 3, 4} b {3, 4, 5, 6} print(a b) # {3, 4} 交集 print(a | b) # {1, 2, 3, 4, 5, 6} 并集 print(a - b) # {1, 2} 差集 print(a ^ b) # {1, 2, 5, 6} 对称差集实际场景太多了。比如你拉了两个不同渠道的用户名单想找出同时出现在两个渠道的人用列表遍历是O(n*m)转成集合做交集是O(min(n, m))性能差距在大数据量下非常可观。判断列表里有没有重复元素、两个列表有没有共同元素、某个集合是否是另一个集合的子集都是集合运算的经典场景has_duplicates len(items) ! len(set(items)) is_subset small.issubset(big)还有一个容易踩的坑set()可以创建空集合但{}创建的是空字典。以及集合里的元素必须是可哈希的列表、字典这些可变类型不能放进集合。如果你确实需要“装着列表的集合”可以把列表转成元组再放进去。frozenset是不可变集合可以被哈希能作为字典的键或者另一个集合的元素。虽然日常用得少但当你需要用集合做“配置项的键”时它就是正确的选择。3. 高效数据容器collections模块的五个利器collections是Python标准库里的宝藏模块里面有十几个专门解决特定场景的容器。我挑了五个实际项目里出镜率最高的每个都对应一类你大概率踩过的痛点。3.1 Counter词频统计的终极答案你写没写过这种代码初始化一个字典循环遍历判断键是否存在存在就1不存在就设为1这大概是每个Python初学者都写过的最重复的一段代码。Counter就是为这个场景而生的from collections import Counter words [apple, banana, apple, orange, banana, apple] counter Counter(words) print(counter) # Counter({apple: 3, banana: 2, orange: 1}) # 最常见的高频词TOP N print(counter.most_common(2)) # [(apple, 3), (banana, 2)]Counter本质上就是dict的子类所以你可以像操作字典一样操作它。但它额外提供了很多好用的方法elements()把所有元素按计数展开成迭代器subtract()做计数相减而且Counter之间可以直接用和-做合并。c1 Counter(a3, b1) c2 Counter(a1, b2) print(c1 c2) # Counter({a: 4, b: 3}) print(c1 - c2) # Counter({a: 2})注意b减没了多文件词频统计、日志关键词计数、购物车商品数量汇总这类“统计出现次数”的需求Counter应该作为你的第一反应。3.2 defaultdict告别判断键是否存在的日子defaultdict解决的是“键不存在时怎么办”这个问题。普通字典访问不存在的键会KeyErrordefaultdict会在你传入的工厂函数自动创建一个默认值。最常见的三个工厂函数from collections import defaultdict # int默认值是0适合计数 counter defaultdict(int) counter[click] 1 # list默认值是空列表适合分组收集 groups defaultdict(list) for item in items: groups[item[category]].append(item) # set默认值是空集合适合去重收集 tags defaultdict(set) tags[python].add(数据类型)用defaultdict(list)改写一下前面setdefault的例子代码会清爽很多visited defaultdict(list) visited[a].append(1)处理多层嵌套字典defaultdict也能帮你省掉好几层get。一种常见用法是自定义个函数返回多层默认字典def nested_dict(): return defaultdict(nested_dict) d nested_dict() d[user][1001][name] 张三 # 中间层级自动创建不会KeyError不过这里有个重要的坑defaultdict的默认值只在__getitem__也就是d[key]这种访问方式触发。用d.get(key)访问不存在的键时返回的是None而不是默认值。因为get是dict的方法绕过了defaultdict的__missing__钩子。记住这一点不然你会碰到“明明设置了默认工厂get还是返回None”的疑惑。3.3 OrderedDict有序字典不止是“有序”Python 3.7之后普通字典本身就保持插入顺序那OrderedDict是不是就没用了不是。普通字典的有序性只是“记录插入顺序”而OrderedDict提供了两个普通字典没有的方法move_to_end()和popitem(lastFalse)。move_to_end(key)可以把某个键移到末尾或开头这个操作在实现LRU缓存时非常关键。比如你写一个缓存容量满了要淘汰最久没用的那个键用OrderedDict就可以“访问过一次就把键移到末尾队首永远是最久没用的”from collections import OrderedDict class LRUCache: def __init__(self, capacity): self.capacity capacity self.cache OrderedDict() def get(self, key): if key not in self.cache: return -1 self.cache.move_to_end(key) return self.cache[key] def put(self, key, value): if key in self.cache: self.cache.move_to_end(key) self.cache[key] value if len(self.cache) self.capacity: self.cache.popitem(lastFalse)popitem(lastFalse)从队首弹出一个元素这是普通字典没有的。虽然现在普通字典也能用next(iter(d))拿到队首元素但popitem这种一步到位的操作还是OrderedDict更顺手。3.4 namedtuple让元组带上字段名前面说了元组是“记录”但坏处是你访问记录里的字段得靠位置索引record[0]、record[1]写多了自己都忘了哪个是哪个。namedtuple就是给元组的每个位置起名字from collections import namedtuple Point namedtuple(Point, [x, y]) p Point(10, 20) print(p.x, p.y) # 10 20 print(p[0], p[1]) # 10 20 # 位置索引依然可用namedtuple在存储数据库查询结果、解析接口返回的固定字段数据时非常好用。相比字典它更省内存相比普通元组它更可读。它有几个很实用但容易被忽略的方法。_asdict()把具名元组转成字典_replace(x30)基于原对象创建一个新对象并修改指定字段_make(iterable)从可迭代对象创建实例p2 p._replace(x30) # Point(x30, y20) p3 Point._make([100, 200]) # Point(x100, y200)字段名的规则是必须是合法标识符不能以数字开头不能和Python关键字冲突。如果你确实需要用不合法的字段名可以用renameTrue它会自动把不合法的名字改成带下划线的形式。另外Python 3.7之后官方推荐标准库里的typing.NamedTuple它支持类型注解但日常用collections.namedtuple完全够。3.5 deque双端队列两头都能操作deque是双端队列两端都能append和pop而且都是O(1)复杂度。对比一下列表列表头部的pop(0)和insert(0, x)是O(n)因为要搬移整个数组。所以你的代码里要是高频操作列表头部请果断换deque。from collections import deque dq deque([1, 2, 3]) dq.append(4) # 右端加 dq.appendleft(0) # 左端加 dq.pop() # 右端弹出 dq.popleft() # 左端弹出deque还内置了maxlen参数限定队列长度超出时另一端的元素会自动挤出。这个特性非常契合“滑动窗口”这种场景from collections import deque # 监控最近100次请求耗时 recent_requests deque(maxlen100) recent_requests.append(0.23) recent_requests.append(0.31) # 超过100个时最旧的那个自动消失rotate(n)方法可以把队列旋转n步正数向右转负数向左转这个在轮询任务、循环切换节点时很好用。另外deque操作是线程安全的多线程环境下一个线程append另一个线程popleft不会崩这一点列表做不到。4. 推导式、切片与解包三个让代码短一半的写法4.1 推导式用一行替代三行循环列表推导式大概是Python最出圈的写法之一[x * x for x in range(10)]一行生成平方数列表。它的优势不只是短而是声明式表达你告诉Python“我要什么”而不是一步步“怎么做”。字典推导式和集合推导式同样常用而且经常被忽略# 字典推导式键值互换 d {a: 1, b: 2} reversed_d {v: k for k, v in d.items()} # 集合推导式去重并筛选 numbers [1, 2, 2, 3, 4, 4, 5] unique_even {n for n in numbers if n % 2 0}推导式还支持嵌套和多层条件但这里要提醒一句嵌套太深会牺牲可读性。我见过的项目里两层嵌套是极限三层以上就该拆成函数或循环了。另外列表推导式里的条件放在for前面还是后面效果完全不同放在前面是“对元素做变换”放在后面是“过滤元素”。# 过滤 变换 result [x * 2 for x in range(10) if x % 2 0] # 只在偶数时乘以2奇数被过滤掉了如果列表特别大你又只是想要一个迭代器而不想一次性占内存用生成器推导式把中括号换成小括号就行gen (x * x for x in range(1000000)) # 不会立刻生成全部数据而是迭代时逐个计算4.2 切片不只是list[start:end]这么简单切片有两个容易被忽视的能力步长和赋值。list[::2]取偶数位元素list[::-1]反转列表这些很多人知道了。但切片赋值才是真正的生产力工具numbers [1, 2, 3, 4, 5] numbers[1:3] [20, 30, 40] # 变成 [1, 20, 30, 40, 4, 5]注意长度可以不一样 numbers[::2] [0, 0, 0] # 步长为2时赋值的长度必须匹配切片赋值时等号右边的序列长度不必和被替换部分一样Python会自动扩展或收缩列表。这个特性在做“批量替换数据片段”时非常省事。另外切片是浅拷贝这一点很多人容易忽略。new_list old_list[:]得到的确实是一个新列表但新列表里的元素和旧列表里的元素还是同一个对象。如果元素本身是可变对象修改元素内部的值两个列表都会变。深拷贝问题我第5部分专门展开。对于迭代器普通的切片语法it[:3]是不可用的会报TypeError。想要迭代器的前N个元素得用itertools.isliceimport itertools first_three list(itertools.islice(iter(range(100)), 3)) # [0, 1, 2]4.3 解包把“提取”这件事变成一行解包在Python里无处不在。函数调用时的*args把元组拆成位置参数**kwargs把字典拆成关键字参数这是我见过的最常见的两类。但这个语法用在其他场景更有味道# 合并字典的经典写法 merged {**d1, **d2} # 将可迭代对象拆开传入函数 points [(1, 2), (3, 4)] for x, y in points: print(x, y) # 用星号收集连续数据 a, b, *rest range(10) # a0, b1, rest[2,...,9]还有一个值得提的是循环里的解包字典推导式的组合拳比如把一个列表的相邻两项组合成字典pairs [(name, 张三), (age, 25)] info {k: v for k, v in pairs}这个用dict(pairs)也能一行搞定但推导式的意义在于你能同时做变换或过滤。海象运算符:是Python 3.8引入的可以在表达式内部赋值和推导式配合起来很强。比如你想一行实现“取所有长度大于等于3的单词并保留它们的长度”words [cat, dog, elephant, bird] result [n for w in words if (n : len(w)) 3] # result [3, 3, 8, 4]这个写法在每次循环里执行len(w)并赋值给n后续复用n避免了重复计算。不过海象运算符容易降低可读性我个人建议在推导式这种一行流里用普通的多行代码里没必要为了省一行去牺牲清晰度。5. 复制陷阱、内存与性能优化避坑5.1 浅拷贝与深拷贝这个坑踩一次就记住了Python的赋值b a不是复制而是复制引用。a和b指向同一个对象改一方另一方也跟着变。要真正复制你有几个选择列表用a[:]或a.copy()字典用dict(a)或a.copy()集合用set(a)。但这些全是浅拷贝。浅拷贝的问题在于它复制了容器本身但没有复制容器里的元素。如果容器里的元素是可变对象修改元素内容原容器和新容器都会跟着变因为它们共享同一个元素对象。看这个例子a [[1, 2], [3, 4]] b a[:] b[0].append(99) print(a) # [[1, 2, 99], [3, 4]]b[0]和a[0]是同一个列表对象所以改b里的子列表a也跟着变。真正要“连里面一起复制”得用copy.deepcopyimport copy a [[1, 2], [3, 4]] b copy.deepcopy(a) b[0].append(99) print(a) # [[1, 2], [3, 4]] # a没变但deepcopy不是免费的它对每个对象递归复制大数据量下性能开销很大。所以实际开发里你要先问一句我到底需不需要深拷贝如果容器里的元素本来就是不可变对象数字、字符串、元组浅拷贝就完全够了。5.2 内存视角id()能看到很多真相Python里有个自带函数id()返回对象的唯一标识。在CPython里这个标识本质上是内存地址。深入理解数据类型的表现用id()观察是最直观的。a [1, 2, 3] b a print(id(a) id(b)) # True同一个对象 c a[:] print(id(a) id(c)) # False新列表不只是列表字符串和整数的某些行为也能从id()看出端倪。比如小整数在Python里有缓存机制a 256; b 256时a is b是True但a 257; b 257时又是False。这种底层实现细节不建议刻意去记但理解“不可变对象可以被复用”这个方向是对的。看内存占用用sys.getsizeof()。它会告诉你对象本身占多少字节但不包括内部元素占用的内存import sys print(sys.getsizeof([])) # 56 print(sys.getsizeof([1, 2, 3])) # 80 print(sys.getsizeof({})) # 64 print(sys.getsizeof({a: 1})) # 184可以看到简简单单一个空字典就占64字节装一个键值对直接涨到184字节。所以当你要存储大量固定结构的数据比如百万级的坐标点、记录行namedtuple或元组比字典省太多内存。这也是为什么很多大数据处理的代码宁愿用元组成批处理也不开一堆字典。5.3 性能选择容器选对了代码就快了一半Python里不同容器的性能特征差别很大。先说查询列表的in判断是O(n)集合和字典的in判断是O(1)。所以“判断某个字符串是否在一堆字符串里”要高频执行请务必用集合# 慢每次都是O(n)扫描 blacklist [a, b, c] if name in blacklist: ... # 快集合哈希查找O(1) blacklist {a, b, c} if name in blacklist: ...我做过一个实际测试列表长度10万随机取1万个元素做in判断集合比列表快了两三个数量级。数据量越大差距越夸张。所以凡是“存在性判断”的场景第一反应应该是集合而不是列表。列表和元组的选择上元组更省内存、创建更快因为不可变的特性让Python可以做更多底层优化。如果你的数据只读不会变用元组。函数需要返回多个值时返回元组是Python惯例。for循环里能用局部变量就尽量用局部变量。这个其实不属于数据类型问题而属于Python名字解析机制。访问局部变量比访问全局变量快得多因为局部变量存在数组里按下标访问全局变量要做字典查找。把len(arr)、math.sqrt这类函数赋值给局部变量再用是Python性能优化里很朴素但有效的一招。6. 常见问题与排查技巧实录6.1 类型相关报错速查表我把这些年遇到的、和数据类型强相关的报错整理成一个速查表每一条背后都有故事报错信息原因解决办法TypeError: unhashable type: list用列表做字典的键或集合元素改成元组tuple(lst)TypeError: NoneType object is not iterable函数返回了None但被当列表遍历检查函数是否所有分支都有返回值AttributeError: NoneType object has no attribute append典型的是a a.append(x)这种错误append是原地操作别把返回值重新赋给原变量TypeError: list object is not callable变量名把内置list覆盖了检查是否有变量叫list、dict、strValueError: too many values to unpack解包时等号两边数量不匹配用*rest接收多余值RuntimeError: dictionary changed size during iteration遍历字典时增删了键先list(d.keys())再遍历这个列表修改原字典list对象不可调用这个坑几乎每个项目都有人踩。变量命名时一定避开list、dict、str、int这些内置名字。一旦覆盖后面的代码再想用list()转类型就直接报错而且报错位置距离真正出问题的地方可能隔了好几行排查起来很头疼。6.2 可变对象作为默认参数最经典的一道面试题def add_item(item, my_list[]): my_list.append(item) return my_list第一次调用add_item(1)返回[1]第二次调用add_item(2)返回[1, 2]。为什么因为默认参数在函数定义时就被创建了以后每次调用用的都是同一个列表对象不是每次新建。这个坑我讲过无数次解法就一个默认参数用None占位函数体内再初始化def add_item(item, my_listNone): if my_list is None: my_list [] my_list.append(item) return my_list只要是可变类型列表、字典、集合做默认参数都得按这个套路来。6.3 遍历时删除元素别直接改原容器在遍历列表时删除元素最经典的翻车现场是numbers [1, 2, 3, 4, 5] for n in numbers: if n % 2 0: numbers.remove(n)表面上你想删掉偶数结果删完发现4没被删掉。原因是在遍历过程中删除元素后面的元素会往前移动导致你跳过了本应检查的元素。正确做法有两种一是遍历原列表的副本修改原列表二是用列表推导式重建numbers [1, 2, 3, 4, 5] numbers [n for n in numbers if n % 2 ! 0]6.4 多线程下共享容器复合操作要加锁deque的append、popleft单个操作是线程安全的但如果你做的是“先检查再操作”这种复合操作比如if len(dq) 0: item dq.popleft()依然可能在检查后被另一个线程抢先取走。多线程场景下要么用queue.Queue这种线程安全的队列要么给复合操作加锁。Python的GIL会让你误以为多线程很安全但GIL只保证单个字节码的原子性不保证你的逻辑原子性。6.5 手写一个简单的缓存装饰器综合应用最后用一个小例子把前面讲的内容串起来。写一个装饰器缓存函数的计算结果容量不够就淘汰最久没用的LRU策略数据结构用OrderedDict判断命中即移到最后from functools import wraps from collections import OrderedDict def lru_cache(maxsize128): def decorator(func): cache OrderedDict() wraps(func) def wrapper(*args, **kwargs): key (args, tuple(sorted(kwargs.items()))) if key in cache: cache.move_to_end(key) return cache[key] result func(*args, **kwargs) cache[key] result if len(cache) maxsize: cache.popitem(lastFalse) return result return wrapper return decorator lru_cache(maxsize32) def expensive_function(x): # 假设这里有大量计算 return x * x这个例子覆盖了OrderedDict的move_to_end和popitem、元组作为字典键、闭包和装饰器可以说是高级数据类型知识的综合演练。当然实际项目里有functools.lru_cache可以直接用我在这里写一遍主要是为了把前面讲的容器知识串起来。根据我自己的实操经验学数据类型最有效的方式不是背方法列表而是带着问题去用写代码的时候多问一句“有没有专门为这个场景设计的容器”遇到报错先查是不是类型用错了。collections模块里的每个类都是为了解决一个真实痛点而存在的你只需踩过一次坑再看到它就会觉得相见恨晚。先用好列表和字典再逐步引入defaultdict和Counter最后能够自如地根据场景选容器到这一步Python数据类型这一关你就算真正过了。