☰
一行dict.get()轻松搞定Python计数统计
2026/10/14 8:08:49 网站建设 项目流程

如果你写代码时遇到“统计一个列表里每个元素出现了几次”“数一数一段文本里每个字符的频率”“盘点日志里各种级别出现了多少条”这类需求,第一反应多半是循环加判断,再手动维护一个字典。做得多了你会发现,Python 里最顺手的计数统计写法,其实是一行dict.get()。这个技巧并不高深,但能让代码从四五行缩到一行,而且逻辑清楚、不容易出错,我自己在项目里用它替换了大量手工计数代码。

这篇文章会从最普通的实现方式讲起,拆解dict.get()一行计数的原理,对比get、setdefault、defaultdict、Counter这几个容易混淆的方案,再给出一批可以直接用的实战例子和踩坑记录。无论你是刚接触 Python 的初学者,还是写了好几年代码的老手,应该都能从中拿到一点能直接用上的东西。

1. 我为什么盯上dict.get()这个写法

1.1 先看看大多数人是怎么写计数统计的

假设有个需求:给出一份城市名单,统计每个城市出现了多少次。最直白的实现大概是这样的:

cities = ["北京", "上海", "广州", "北京", "深圳", "上海", "北京"] counter = {} for city in cities: if city in counter: counter[city] += 1 else: counter[city] = 1 print(counter) # {'北京': 3, '上海': 2, '广州': 1, '深圳': 1}

逻辑没有任何问题:先看键在不在字典里,在就加一,不在就初始化成 1。当年我也是这么写的,写多了就覺得这段代码又长又重复,每一处计数都要写一遍if...else...。

后来有一阵我改用try...except...:

counter = {} for city in cities: try: counter[city] += 1 except KeyError: counter[city] = 1

这是用异常处理来代替判断。对单个键来说,抛异常的成本比较高,如果键大量不存在,性能反而不理想;而且代码读起来绕,新人看到KeyError还要想一下这是哪来的。这种写法适合某些特殊场景,但作为通用计数方案并不算好。

1.2 这些老写法到底有什么问题

第一种if...else...写法最大的问题是重复。每一次“计数”这个动作,都要准备两份分支逻辑。一个脚本里如果有五个地方需要计数,那就要写五段几乎一样的代码,后面维护的人看着就想重构。

第二个问题,也是最容易被忽略的:当初始值不是 1,而是其他值的时候,if...else...会变得更啰嗦。比如统计总分:

scores = {} for name, score in data: if name in scores: scores[name] += score else: scores[name] = score

这就不是“加一”了,而是“存在就累加,不存在就赋值”。分支结构会更明显。

我真正开始留心dict.get(),是因为有一次需要统计一份十几万行日志里的错误级别。当时想都没想就写了if...else...,回头去看那段代码的时候突然意识到:这一段逻辑明明只是一个“往字典里累加一个值”的动作,为什么要写这么多行?

于是我把那段代码改成了dict.get()的版本,这下整个人舒服了。

2. 一行计数统计的正确打开方式

2.1 核心代码拆解:counter[city] = counter.get(city, 0) + 1

直接看代码:

cities = ["北京", "上海", "广州", "北京", "深圳", "上海", "北京"] counter = {} for city in cities: counter[city] = counter.get(city, 0) + 1 print(counter) # {'北京': 3, '上海': 2, '广州': 1, '深圳': 1}

循环体里只有一行:counter[city] = counter.get(city, 0) + 1。这一行做了三件事:

  1. counter.get(city, 0):从字典里取当前城市的计数值。如果这个城市还是第一次出现,字典里没有这个键,就返回默认值 0。
  2. + 1:在取出来的值基础上加一,得到新的计数值。
  3. counter[city] = ...:把新值写回字典,覆盖原来的计数值。

注意,get和直接用中括号取值的区别在于:counter[city]在键不存在时会抛出KeyError,而counter.get(city, 0)不会抛异常,只会返回一个默认值。这就是能用“一行”完成计数而不需要任何分支的根本原因。

这一行的等价写法其实就是最早的if...else...版本,只是把“判断键是否存在”这件工作交给了get内部的默认值机制。代码短了,但逻辑没有打折。

2.2 三个最常见的实际场景

这个模式可以套进很多需求里。比如统计字符串中每个字符的出现次数:

text = "hello world" char_count = {} for ch in text: char_count[ch] = char_count.get(ch, 0) + 1 print(char_count) # {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}

又比如统计一个列表里每个单词的词频:

words = ["apple", "banana", "apple", "orange", "banana", "apple"] word_count = {} for word in words: word_count[word] = word_count.get(word, 0) + 1 print(word_count) # {'apple': 3, 'banana': 2, 'orange': 1}

再比如统计日志中不同错误级别的条数:

log_levels = ["ERROR", "INFO", "DEBUG", "ERROR", "INFO", "ERROR"] level_count = {} for level in log_levels: level_count[level] = level_count.get(level, 0) + 1 print(level_count) # {'ERROR': 3, 'INFO': 2, 'DEBUG': 1}

同样的套路,换一个数据结构,需求就完成了。不需要if,不需要except,不需要每次循环前先初始化。

2.3 默认值这个参数是整个逻辑的灵魂

dict.get(key, default)的第二个参数很关键。很多人知道get可以传默认值,但用到计数上时容易犯一个错误:忘了传默认值,直接写counter.get(city) + 1。

这种写法会报错。因为当city不存在时,get返回的是None,None + 1直接抛出TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'。

默认值填 0 的语义是:这个键如果不存在,就把它当成“已经出现过 0 次”来处理,然后加上本次的 1 次。这一步想清楚了,整个一行写法就很好理解。

还有一个小细节:如果你要累加的不是 1,而是某个权重,也可以直接改。比如统计一个店铺各商品的销售额:

transactions = [("sku123", 199), ("sku456", 99), ("sku123", 50)] sales = {} for sku, amount in transactions: sales[sku] = sales.get(sku, 0) + amount print(sales) # {'sku123': 249, 'sku456': 99}

这个例子能说明get方案真正的价值:它不是只能“加一”,而是“取旧值加上新值”,所以计数、求和、累加都能用它来写。只要把+ 1换成+ amount就行。

3. 它为什么能这样写:原理与兄弟方法对比

3.1 一次调用背后到底发生了什么

dict.get()是字典对象的内置方法,它的行为可以简单理解为:

  • 第一个参数是键名。
  • 如果键存在于字典中,返回键对应的值。
  • 如果键不存在,且传了第二个参数,返回第二个参数;没传则返回None。

关键在于:get只负责“读”,它不会修改字典本身。不管键存不存在,调用get都不会往字典里写入任何东西。真正产生写入动作的,是那条赋值语句counter[city] = ...。

这个特性让它和setdefault有着本质区别。setdefault是“读不到就写入一个默认值再返回”,它是有副作用的;get是无副作用的。在计数场景里,如果每一轮循环都用setdefault,即使键已经存在,它也会反复构建默认值对象,白白浪费性能。get就不会有这个问题。

还有一个相关方法是pop(key, default),它也是带默认值机制的方法,但它的行为是“取走并删除”。计数的时候千万别用它,否则统计到一半,键被删掉了,数据全丢。

3.2get、setdefault、defaultdict、Counter到底怎么选

写计数的方法不止get一种。我整理了一下这几个方案的对比,方便以后选型:

方案典型写法特点适用场景
dict.getcounter[k] = counter.get(k, 0) + 1无副作用,不额外导入,写法直观通用计数、累加、求和,最推荐
setdefaultcounter.setdefault(k, 0); counter[k] += 1两步操作,且每次都构造默认值需要默认容器时(如列表)会用到
defaultdictcounter = defaultdict(int); counter[k] += 1导入collections,键不存在时自动创建字典里的值就是可变容器时很好用
CounterCounter(iterable)专门为计数设计,自带most_common等方法纯频次统计,不需要自定义逻辑时首选

defaultdict(int)写起来也很短,但它多了一个导入,而且如果后续有人不小心访问了一个不存在的键,字典会默默把这个键创建出来,有时候反而掩盖了代码里的问题。Counter是做纯计数的最专业方案,一行Counter(words)就能完成统计,但当你需要在循环里逐条累加、需要同时维护多个维度计数器时,灵活度不如dict.get()。

我个人的习惯是:如果只是给一个现成的列表做频次统计,直接Counter;如果是循环里逐步更新计数、累加金额、在多个字段上做统计,用dict.get()最顺手。

3.3 性能与可读性的平衡

有人会问:dict.get()这个写法在性能上是不是比if...else...慢?

说实话,影响微乎其微。在 Python 里get本身是 C 语言实现的哈希查找,和in判断加下标取值相比,核心操作都是哈希查找。硬要比较的话,get只做一次哈希查找,if...else...在某些路径下要做两次(先in判断,再counter[city] += 1取值),所以get在“键已存在”的高频场景下反而可能更快一点。当然这种差异在你统计一万个元素的时候根本感觉不出来。

真正值得注意的是另一个极端:千万不要为了“一行”而写这种代码:

[word_count.update({w: word_count.get(w, 0) + 1}) for w in words]

这种用列表推导式的副作用来实现计数的写法,虽然真的只有一行,但它是错的。它生成了一个全是None的临时列表,纯属浪费内存,而且可读性极差。记住,“一行代码”正确的追求是“一个动作只写一行”,不是“整段逻辑挤在一行”。

4. 一行计数还能延伸出哪些变体

4.1 一行完成词频统计并按频次排序

计数只是第一步,统计完往往还要排序。有了get的基础,整条链路也可以写得很紧凑:

words = ["apple", "banana", "apple", "orange", "banana", "apple"] counter = {} for word in words: counter[word] = counter.get(word, 0) + 1 # 按频次从高到低排序 sorted_counter = sorted(counter.items(), key=lambda x: x[1], reverse=True) print(sorted_counter) # [('apple', 3), ('banana', 2), ('orange', 1)]

取出现次数最多的前两个元素:

top2 = sorted(counter.items(), key=lambda x: x[1], reverse=True)[:2] print(top2) # [('apple', 3), ('banana', 2)]

这里的sorted(counter.items(), key=lambda x: x[1])是按字典的值排序,reverse=True表示降序。这类写法在分析日志、统计热词、排行榜场景里非常常用。

除了sorted,还可以用max(counter, key=counter.get)直接拿到出现次数最多的键:

most_common_word = max(counter, key=counter.get) print(most_common_word) # apple

这里max的key参数接收一个函数,counter.get会取出每个键对应的频次,max再根据这个频次找出最大的键。这个用法是dict.get()的另一个经典应用,也值得记下来。

4.2 合并两个字典的计数结果

有时候数据被拆成了两份,比如两天各自的访问统计,你想把它们合并成一份总数。用get写起来也很自然:

day1 = {"u001": 5, "u002": 3} day2 = {"u002": 2, "u003": 4} merged = {k: day1.get(k, 0) + day2.get(k, 0) for k in set(day1) | set(day2)} print(merged) # {'u001': 5, 'u002': 5, 'u003': 4}

set(day1) | set(day2)是取两个字典所有键的并集,然后对每个键分别从两张表里取值,并用默认值 0 兜底。这里的核心技巧依然是get:不管这个键在不在原来的字典里,都能得到一个合理的数字。

如果不用get,写day1[k] + day2[k]会频繁触发KeyError,要么就得写一堆判断。用get直接把合并逻辑压成一行字典推导式,结果清晰又不会报错。

4.3 嵌套字典的分组计数

回到文章开头提到的“统计每个用户的各种行为次数”,这种需求会涉及嵌套字典。先看一个很容易想到的写法:

events = [ ("u001", "login"), ("u002", "page_view"), ("u001", "logout"), ("u001", "login"), ] action_count = {} for user, action in events: if user not in action_count: action_count[user] = {} action_count[user][action] = action_count[user].get(action, 0) + 1 print(action_count) # {'u001': {'login': 2, 'logout': 1}, 'u002': {'page_view': 1}}

这个写法里,外层还保留了一个if,因为我们要用一个空字典作为内层默认值。如果想把这段也简化,可以使用setdefault:

for user, action in events: action_count.setdefault(user, {}) action_count[user][action] = action_count[user].get(action, 0) + 1

或者用defaultdict(dict):

from collections import defaultdict action_count = defaultdict(dict) for user, action in events: action_count[user][action] = action_count[user].get(action, 0) + 1

这三种写法都可以。内层的action_count[user].get(action, 0) + 1依然是我们那套一行计数,外层只是在解决“内层字典还不存在”的问题。真实项目中,像“用户行为统计”“按省份统计各品类销量”这类二维数据,用这个嵌套模式非常顺手。

4.4 列表扁平化与其他场景里的变通用法

dict.get()除了计数,在配置读取、缓存取值等场景也经常出现。比如从配置字典里读取某个参数,找不到就返回默认值:

config = {"host": "localhost", "port": 8080} timeout = config.get("timeout", 30)

这几乎是最常见的一种用法:想要一个“存在就用配置值,不存在就用默认值”的读取逻辑,写的正是config.get(key, default)。这和计数时的思路完全一致,只是返回值不参与加一操作。

再比如把两个列表按元素分组,也可以用get和列表拼接的方式:

data = [("group_a", 1), ("group_b", 2), ("group_a", 3)] grouped = {} for group, value in data: grouped[group] = grouped.get(group, []) + [value] print(grouped) # {'group_a': [1, 3], 'group_b': [2]}

注意这种写法会反复创建新列表,数据量大的时候不如setdefault或defaultdict(list)高效,但胜在代码短且无额外导入。在数据量可控的脚本里用一用完全没问题。

5. 实战中容易踩的坑与排查思路

5.1 默认值不能随便填:可变对象陷阱

get的默认值虽然方便,但第二个参数是一个普通值。假设你写了这么一段代码,想把用户分到不同分组里:

groups = {} for name in user_list: groups[name] = groups.get(name, []) + ["member"]

功能上没问题,但请注意,这里的默认值[]每次调用get时都会被重新创建,且+ ["member"]也是新创建一个列表,整体效率不算高。如果列表很大,会明显浪费内存。

相比之下,setdefault和defaultdict(list)是在原列表上做append,效率更高,更适合这种“默认值是可变容器”的场景。一句话总结:计数加数字用get,默认容器要原地修改时用setdefault或defaultdict。

5.2 忘写第二个参数,直接 TypeError

这是我看到新手甚至老手最容易踩的坑。当键不存在时,get(key)返回None,如果接着做+ 1,就会抛出TypeError。排查的时候看到报错信息,第一反应往往以为是数据类型不匹配,实际上是因为默认值没写。

调试这种问题有一个经验:在计数代码里看到get方法,先检查第二个参数是否写了,再检查返回值参与运算的类型是否一致。把这一条养成习惯,这类报错基本一眼就能定位。

5.3 字符串统计时的隐藏字符

统计一段文本里字符频率的时候,for ch in text会把空格、换行符、制表符也当作普通字符统计进去。你要是对比两份统计结果,明明只改了排版,频率却对不上,十有八九是换行和空格的问题。

如果需要忽略空白字符,可以先用过滤器处理一下:

text = "hello world \n this is a test" char_count = {} for ch in text: if ch.strip(): char_count[ch] = char_count.get(ch, 0) + 1

ch.strip()可以判断当前字符是否为空字符。这个细节在实际处理爬虫抓回来的网页文本时特别重要,因为 HTML 里有大量空白字符会干扰词频分析。

5.4 别在列表推导式里硬塞计数

前面提到过,用列表推导式的副作用做统计是不可取的。除此之外,还有一种看起来“聪明”的写法:

# 反例,不推荐 {words.count(w): w for w in words}

如果只统计不排序,collections.Counter显然更合适。如果你在代码审查里看到某段计数逻辑写得很绕,先帮它简化成counter[k] = counter.get(k, 0) + 1再看看:绝大多数时候,这个简单版本已经够用了。

我后来在项目里养成一个习惯:只要看到“循环里维护字典并做累加”的需求,先写一行dict.get()版本,然后再判断是否需要升级成Counter或defaultdict。用最简单的方式开始,实在不够用了再升级,基本不会有大问题。

平时做日志统计、爬虫去重、用户行为分析这类事情,counter[key] = counter.get(key, 0) + 1已经足够顺手。偶尔需要统计完立刻取最高频项,再用max(counter, key=counter.get)补一行。这两个写法配合起来,足以应付绝大多数计数需求,而且每一行都看得懂,不会给自己留坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询