编程语言研究书库

计数器 — 数东西这件事的最终形态

计数器 — 数东西这件事的最终形态

这一章讲一件事: 数一堆东西里每种各有多少个——这个任务你已经能用 字典写了,但 Counter(计数器)让你一行写完,还能顺手按多少排好序。 它是 collections 三件套的最后一件,也是这条演进线的终点: 普通字典 → defaultdict → Counter,样板代码一段一段被收进工具里,你的代码 只剩「要数什么」本身。

1. 这一章讲什么、在全书链条里的位置

第四篇的三件套,前两件各修一个结构的一种脾气: namedtuple 修元组的「没名字」,defaultdict 修字典的「缺键报错」。 Counter 走的更远一步——它为「计数」这一个任务整个定制: 它本质上是一个字典(键是元素、值是次数),但造它的时候直接把数据喂进去, 它自己数完1。这是全书的最后一个机制,也是数据篇的收官。

2. 顶层全景:数同一篮水果,三级台阶

  同一篮水果:['apple', 'banana', 'apple', 'orange', 'banana', 'banana']
  正确答案:banana 3 个、apple 2 个、orange 1 个

  第 ① 级 普通字典:循环 + 「在不在」判断 + 在则 +1 不在则建 ── 5 行
  第 ② 级 defaultdict(int):循环 + 直接 += 1 ───────────────── 3 行
  第 ③ 级 Counter:一行 Counter(数据) ──────────────────────── 1 行

  图说:三级台阶走完整本书 collections 篇的主线——把重复的样板 progressively 收进工具。

主线例子是这篮 6 个水果,走全程,每个数都出自原书的实际运行2。

3. 核心原理:两个机制,各走一步主线例子

3.1 前两级的写法,本章各给一段

普通字典版(第 ① 级):循环每个水果,先问在不在——在,次数加 1; 不在,建这个键、次数写成 1。5 行,其中 3 行是防守2。

defaultdict 版(第 ② 级):防守交给默认值,循环体只剩 fruit_count[fruit] += 1——查不到就先放 0 再加。3 行2。

这两级就是第 14、17 章的内容,原书在这里把它们并排印出,让台阶的落差自己说话。

3.2 Counter 版:连循环都省了

第 ③ 级长这样:

from collections import Counter
basket = ['apple', 'banana', 'apple', 'orange', 'banana', 'banana']
fruit_count = Counter(basket)

就这一行。Counter(basket) 做的事:把篮子整个接过去,自己遍历一遍, 每个元素见过几次记几次,直接给你一个数好的字典。 打印出来是 Counter({'banana': 3, 'apple': 2, 'orange': 1})—— 注意 banana 排在最前:Counter 顺手按次数从多到少排了序2。

为什么有效: 数数这个任务的三件套——遍历、判断、累加——前两件被 Counter 的构造动作整个吞掉。你不再告诉它「怎么数」,只告诉它「数什么」。

3.3 most_common():按频次排队取前几名

数完不算完,实际任务几乎总要追问一句「最多的是谁?前几个是谁?」 Counter 的 most_common() 方法管这个。书里的水果店例子:一天卖掉的流水是 apple×4、banana×3、orange×2(9 笔)。两行问答:

item_count.most_common(1)  # → [('apple', 4)]              卖得最好的一种
item_count.most_common(2)  # → [('apple', 4), ('banana', 3)] 前两名

返回值是一列表,每个元素是 (东西, 次数) 的小元组,已经按次数排好队。 数字母的例子更能看出它和字符串功夫的配合: 想数 Python is a powerful programming language 里每个字母出现几次, 先 .replace(' ', '') 去掉空格、.lower() 全转小写,再交给 Counter, most_common(5) 直接报前五名:a 和 g 各 4 次,p、o、n 各 3 次3。 去空格和转小写是计数前的「清洗」——这两步仍是你的活,Counter 只管数。

3.4 词频练习走一遍:从一段原文到一张前 10 榜

练习给了一段关于 Python 本身的四段介绍文字(从「high-level, general-purpose」 讲到「machine learning community」),要求忽略大小写、统计每个词出现几次、 打印最常用的前 10 个4。

word_count = Counter(paragraph.lower().split())
for word, count in word_count.most_common(10):
    print(f'{word}: {count}')

三步行:.lower() 全转小写(于是 'Python' 和 'python' 算同一个词)、 .split() 按空格切成词列表、Counter 接手数完并按多少排好。 榜单一出,第一名的数相当醒目:python: 9——这段文字里 'python' 出现了 9 次; the 和 in 各 6 次;a、programming、and、as、released 各 4 次5。 (这正是计数前「什么算一个词」的默认口径:按空格切,所以带句点的 'language.' 和不带句点的是两个词——见第 5 节的边界提醒。)

数字版练习同理:13 个数的列表 [1, 3, 2, 2, 3, 3, 4, 5, 1, 2, 2, 4, 1], Counter(numbers).most_common(3) 直接给出前三名:2 出现 4 次, 1 和 3 各 3 次——[(2, 4), (1, 3), (3, 3)]5。

4. 作者的判断与证据

作者明说的: Counter 会自动数出每个元素出现的次数、不需要写循环, 代码更干净、写得更快;并且它是「a subclass of dict」——字典的直系子孙, keys 是元素、values 是次数,most_common() 返回按频次排好序的列表6。

证据: 本章证据就是三级台阶的并排演示(同一篮水果,三种写法)和两个 可运行的小案例(水果店流水、字母频次),数字全部出自真实运行。 章末案例把尺度放大到整本书:把莎士比亚《罗密欧与朱丽叶》的全文喂给 Counter,统计词频——并指了一条获取全文的公开来源(MIT 的莎士比亚站点), 把解法留白4。

我们的解读(不是书里的): 三件套到这里收齐,演进线的意义超出「少写代码」: 每上一级,你的代码就更接近你要问的问题本身。「Counter(文本) 的 most_common(10)」几乎就是把「这段文字最常用的 10 个词是什么」这句中文 直译成了 Python。

判断(我们的,不是书里的): 词频统计是 Counter 最能打的场景, 而「数之前先洗」是这个场景的隐藏门槛——本章所有数字例子前面都有 lower()、replace()、split() 这三板斧。新手真正的障碍不在 Counter 的 用法,而在意识到「什么算是一个词」是个要自己先回答的问题。 如果错,会错在: 如果数据已经干净(比如程序内部计数、投票记录), 清洗这一步根本不存在,那「隐藏门槛」就只是个比喻;反过来,处理自然语言 时这一步的讲究(标点、大小写、分词)比 Counter 本身深得多,本章没有展开。

5. 边界与局限

计数前的那半步仍是你的活。 Counter 只「数」,不「洗」:原书数单词的 练习里,paragraph.lower().split()(转小写再按空格切开)发生在喂给 Counter 之前;数字母的例子也要先去空格、转小写3。大小写、标点、分隔方式, 这些决定「什么算是一个词」的判断,Counter 一概不做。

它只接受可哈希的东西当元素。 键是元素本身,所以元素必须可哈希—— 列表当元素会直接 TypeError(这条只出现在本章给 AI 的提问清单里,正文没展开)7。 可哈希的大白话:能被字典当键用的东西(字符串、数字、元组可以;列表、字典不行)。

「次数」默认是整数,出现负数或不按整数加时怎么办,正文没讲。 官方文档里有 Counter 之间做加减、处理缺键的行为说明,原书把链接放在章末, 正文只教了构造和 most_common7。还有两个正文没提、清单里点到的口子: most_common() 不传参数时给出全部元素按频次排好的完整榜,不只前几名; 两个 Counter 之间可以直接加减——今天卖的水果和昨天卖的相减, 差值就是每种水果销量的增减(这是官方文档的能力,原书只把它放进了 给 AI 的提问清单)7。

词频练习的一个小口径提醒: 练习里 paragraph.lower().split() 按空格分词, 所以 language.(带句点)和 language 会被当成两个词。统计结果的精确性 取决于这一步的切法——这是所有词频工具的共性,不是 Counter 的毛病。

6. 可带走的

  1. 数东西三级台阶:普通字典(循环+判断)→ defaultdict(只留循环)→ Counter(连循环都省);本章是终点,三件套主线在此收齐;
  2. 一行构造:Counter(任意可遍历的数据);它本质是字典,键是元素、值是次数;
  3. most_common(n) 直接给出前 n 名,排好序的 (元素, 次数) 列表; 不传 n 给全部;
  4. 数之前想清楚「什么算一个」:去空格、转小写、按什么切开——清洗是你 的责任,Counter 只管数;
  5. 元素必须可哈希(能当字典键);列表、字典不能当元素;
  6. 想看「榜样的全貌」就调 most_common() 不带参数——全部元素按频次排好, 不少不漏;两批数据各有一个 Counter 时,直接相减看增减;
  7. 全书的取景框到这里为止:理论(是什么)→ 流程控制(怎么走)→ 函数(怎么复用) → 数据结构(怎么装)→ collections 三件套(怎么少写样板)。下一本书再往后走。

这一章短而收束:三级台阶、一个方法(most_common)、一个留白的大作业—— 三件套至此讲完,数东西的整条演进线在你手里合上。

7. 原文地图

主题原书章原文位置
Counter 的定位(接在 defaultdict 之后)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:517(搜「designed specifically for counting」)
水果计数:普通字典版SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:527(搜「fruit count exercise」) · :530(搜「basket」) · :540(搜「'banana': 3」)
水果计数:defaultdict 版SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:542(搜「save some effort」) · :554(搜「'banana': 3」)
水果计数:Counter 版与「不需要循环」SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:557(搜「counts the occurrences」) · :563(搜「Counter(basket)」) · :567(搜「'banana': 3」)
Counter 是 dict 的子类、most_common 说明SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:572(搜「subclass of dict」) · :575(搜「most_common」)
水果店流水案例(前 1、前 2 名)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:579(搜「store sells fruits」) · :594(搜「most_common(1)」) · :605(搜「Top 2」)
数字母案例(清洗 + most_common(5))SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:610(搜「powerful programming language」) · :613(搜「replace」) · :617(搜「Top 5」)
词频练习(top 10)与数字练习(top 3)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:664(搜「paragraph.lower().split()」) · :669(搜「python: 9」) · :695(搜「(2, 4)」)
Romeo and Juliet 案例(留白的终章作业)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:697(搜「ROMEO AND JULIET」) · :706(搜「shakespeare.mit.edu」) · :707(搜「not provided」)
可哈希等边界(仅提问清单)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:730(搜「TypeError」)
官方文档链接SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:735(搜「EXPLORE MORE」)
章节引言(接在 defaultdict 之后)SECTION IV: Data Collectionstext/25-fm-section-iv-data-collections.txt:514(搜「key-value pairs efficiently」)

Footnotes

  1. 出处:「SECTION IV: Data Collections」第 512–523 段(text/25-fm-section-iv-data-collections.txt:516,搜「Taking this further」;:517,搜「designed specifically for counting」)。原文章节引言:「the Counter is a specialized type of dictionary designed specifically for counting tasks」。 ↩

  2. 出处:「SECTION IV: Data Collections」第 525–567 段(text/25-fm-section-iv-data-collections.txt:527,搜「fruit count exercise」;:540,搜「'banana': 3」;:542,搜「save some effort」;:563,搜「Counter(basket)」;:567,搜「'banana': 3」)。同一篮水果(basket,第 530 段)的三级写法与运行结果并排,结果一致:banana 3、apple 2、orange 1。 ↩ ↩2 ↩3 ↩4

  3. 出处:「SECTION IV: Data Collections」第 607–617 段(text/25-fm-section-iv-data-collections.txt:610,搜「powerful programming language」;:613,搜「replace」;:617,搜「Top 5」)。数字母前的清洗(去空格、转小写)与 most_common(5) 的运行结果。 ↩ ↩2

  4. 出处:「SECTION IV: Data Collections」第 697–707 段(text/25-fm-section-iv-data-collections.txt:697,搜「ROMEO AND JULIET」;:706,搜「shakespeare.mit.edu」;:707,搜「not provided」)。案例要求找全文、喂给 Counter 统计词频;原文明说「To save space, solution for this case study is not provided」。 ↩ ↩2

  5. 出处:「SECTION IV: Data Collections」第 619–695 段(text/25-fm-section-iv-data-collections.txt:664,搜「paragraph.lower().split()」;:669,搜「python: 9」;:681,搜「1, 3, 2, 2」;:695,搜「(2, 4)」)。词频榜前 10 与数字榜前 3 的运行结果。 ↩ ↩2

  6. 出处:「SECTION IV: Data Collections」第 569–576 段(text/25-fm-section-iv-data-collections.txt:572,搜「subclass of dict」;:575,搜「most_common」)。「counts the occurrences of elements without needing a(数元素的次数,不需要循环)」见第 557 段。 ↩

  7. 出处:「SECTION IV: Data Collections」第 726–733 段(text/25-fm-section-iv-data-collections.txt:730,搜「TypeError」;:731,搜「non-integer」;:732,搜「negative counts」)。可哈希要求、非整数增量、负计数都只在「INTERACT WITH GENAI」的提问清单里出现,正文未展开;官方文档链接见第 735–739 段。 ↩ ↩2 ↩3

On this page