
1. 先把结论说清楚这个写法在 Python 里不合法dict.popitem(key)这句话估计是从某道练习题或者自己的速查笔记里流出来的。我先直接说结论标准 Python 里并没有这种用法dict.popitem()不接受任何参数。你想着“按 key 返回内容并删除”真正要调用的方法叫dict.pop(key)而它返回的是 key 对应的value不是整个键值对。我第一次在项目里看到同事写data.popitem(user_id)时代码一跑就是TypeError: popitem() takes no arguments (1 given)。后来我翻了不少网上的帖子发现这种错误还挺普遍原因是大家对字典的几个删除方法印象模糊尤其是看到“pop”和“item”这两个词拼在一起就容易脑补成“pop 掉某个 item”然后顺手把 key 传进去。所以这篇文章就从这句话本身说起把 Python 字典里“返回并删除”相关的方法彻底拆开讲。不管你是刚学 Python 的小白还是被这道题困扰过一阵的开发者看完后应该不会再弄混 pop、popitem、del 这三兄弟。1.1 官方签名和实际的报错信息先看官方文档对dict.popitem的定义dict.popitem()从 Python 3.7 开始字典会保留元素的插入顺序popitem()会从字典的末尾移除并返回一个键值对。它不接收 key 参数返回的类型是元组形如(key, value)。我写个小例子d {name: 张三, age: 18, city: 上海} item d.popitem() print(item) print(d)运行结果(city, 上海) {name: 张三, age: 18}可以看到popitem()弹出的是最后插入的那个键值对。这个行为在 Python 3.7 以后是稳定的而在更早版本里它只是“随机弹出一个键值对”所以那时不少人拿它做去重或者乱序处理效果并不保证。如果你错误地写成d.popitem(name)解释器会直接给你类似这样的报错TypeError: popitem() takes no arguments (1 given)这个报错有两个信息点第一popitem()这个方法是“不接受参数”的第二传进去的name被当成一个多余参数Python 根本没有打算帮你把它当 key 去查。1.2 真正按 key 删除应该用 dict.pop(key)如果你想根据 key 删除字典中的一个键值对同时拿到它对应的 value标准写法是d {name: 张三, age: 18, city: 上海} name d.pop(name) print(name) # 张三 print(d) # {age: 18, city: 上海}如果你误以为“返回词条的内容”是指返回{name: 张三}这种完整的小字典那又偏了一步。pop(key)返回的是 key 对应的 value不是一个小字典。上面的例子中d.pop(name)返回张三。如果你需要同时拿到 key 和 value通常有两种做法# 先用 key 取出 value再删除 if name in d: value d.pop(name) print(fkeyname, value{value}) # 用 popitem 处理的情况是你不关心具体是哪个 key大多数业务代码里你都是知道要删哪个 key 的所以pop(key)会更常用。它才是“按 key 返回并删除”的正主。1.3 del、clear、pop、popitem 的差异为了让你一眼看清这几个操作的区别我整理成一个表操作写法能否指定 key返回值key 不存在时popd.pop(key)能key 对应的 value抛 KeyErrorpop 带默认值d.pop(key, None)能key 对应的 value否则返回默认值返回默认值popitemd.popitem()不能(key, value)元组空字典抛 KeyErrordeldel d[key]能无抛 KeyErrorcleard.clear()不能无永不会因 key 错误它会清空全部看到这里你应该明白标题里“返回字典中关键字为 key 的词条的内容并删除该词条”如果把它改写成一句话应该是dict.pop(key)返回字典中 key 对应的 value并删除这个键值对。而popitem()是另一件事它不需要 key而是从末尾弹出一个完整的键值对。2. 为什么“popitem”这个名字很容易让人理解错很多人不理解既然 popitem 叫“弹出 item”那 item 应该就是指某个键值对为什么我不能指定哪个 item这和 Python 里字典的底层实现有关理解清楚后会容易很多。2.1 字典底层是一张哈希表Python 的dict本质上是一个哈希表实现。所谓“哈希表”你可以把它想象成一张有大量抽屉的柜子每个抽屉都有自己的编号。当你要保存一个键值对时Python 会先对 key 做一次哈希运算hash(name)算出一个整数然后通过这个整数决定把这个键值对放到哪个“桶”里。当你要查找某个 key 时解释器再次计算 key 的哈希值直接跳到对应桶里去取数据。整个过程平均时间复杂度是 O(1)也就是跟字典里有多少个键值对没有太大关系。既然 key 和存储位置之间是靠哈希值计算的那么“你有没有指定 key”这件事在底层就表现为“Python 要不要先做一次 key 定位”。d.pop(name)是先定位到name对应的桶然后把桶里的 value 拿出来再清除这个槽位。而popitem()的设计目标不是“按 key 查找”而是“从字典的顺序结构中取一个元素出来”它要做的只是取下当前顺序的末尾项。所以从设计意图上说popitem()更像是一个“带返回值的尾部删除”操作而不是“按 key 删除”操作。这两个方法一个面向 key 查询一个面向整体容器的顺序维护名字相似只是历史遗留的巧合。2.2 CPython 里的“紧凑哈希表”和顺序存储再往下说一点底层Python 3.6 之后CPython 里的字典在结构上做了一个重要优化把“哈希索引”和“键值对数组”分开存储。每个键值对实际保存在连续的数组里同时另一个稀疏索引数组保存哈希值对应的偏移量。这样做带来的效果是字典本身在迭代时会按照“键值对插入的顺序”来遍历。而当你想移除最后一个元素时只需要获取键值对数组末尾那个元素把它和索引关系断开即可。popitem()正是利用了这种结构。所以它在 Python 3.7 的稳定表现是 LIFO也就是后进先出。你可以把它当成一个有限容量的“撤销栈”来用后加入的配置、后产生的数据先被弹出处理。2.3 可变对象为什么不能作为 key既然说到 key再帮你补一个经常踩的坑不是任何对象都能当字典的 key。哈希表要求 key 对象必须“可哈希”也就是说对象一旦创建哈希值不应该变化。列表、字典、集合都属于可变对象它们内部内容一变哈希值就可能变因此不能作为 key# 下面这行会报 TypeError: unhashable type: list d {[a]: 1}而字符串、数字、元组元组内元素也必须不可变通常可以用作 key。在pop(key)这个操作中底层同样需要先对 key 求哈希值。如果你的程序里 key 本身是可变类型通常在你执行的早期就会报错而不是等到删除时才暴露。2.4 为什么不应把“key”理解成 API Key 之类的密钥聊到 key很多项目的上下文里还有另一类 key比如 API Key、密钥、token 等。它们可能会被存在字典、配置对象、环境变量里。Python 字典里的 key 只是“键值对的关键字”两者是两码事。你从配置文件读出一堆键值对之后常常需要把某些临时键弹出来使用config {host: 127.0.0.1, port: 8080, api_key: xxx} api_key config.pop(api_key)这里pop(api_key)的值是一个字符串。但在真实项目中敏感信息的读取有更规范的姿势一般建议从环境变量里读取而不是硬编码在代码或字典里。用完之后及时清理也算一种防御习惯。3. 实操按 key 返回并删除的几种标准写法和场景下面从最简单、最常用的方案讲起逐步到“按固定顺序弹出”的进阶玩法。3.1 推荐写法dict.pop(key, default)日常开发中90% 的情况用pop(key, default)就够了。第二个参数 default 是可选参数作用是在 key 不存在时返回一个兜底值避免抛异常。user_settings { theme: dark, font_size: 14, language: zh_CN } # 拿 theme 并删除该配置 theme user_settings.pop(theme, None) print(theme) print(user_settings)运行后得到dark {font_size: 14, language: zh_CN}这里最实用的点是即使user_settings里没有theme程序也不会因为 KeyError 中断而是让 theme 变成 None。这种写法的语义是你要读取并删除这个 key但如果它不在就按缺少处理。如果你不希望静默地返回默认值而希望明确知道 key 缺失那就可以省略 defaulttheme user_settings.pop(theme)这样当theme不存在时Python 会抛出 KeyError。二者没有绝对的好坏关键看业务上能不能容忍缺失。3.2 只删不取del dict[key]有些时候你删除一个键纯粹是为了清理字典根本不需要拿返回值。那没必要用pop直接del更清晰cache {a: 1, b: 2, temp: 3} del cache[temp] print(cache) # 输出{a: 1, b: 2}这个操作的执行效率与pop基本一致但代码表达更直接我不关心它原来的值是什么清掉就行了。del在 key 不存在时同样会抛 KeyError所以如果是不确定是否存在可以先用if key in cache判断或者写成if temp in cache: del cache[temp]需要注意del没有返回值如果你写result del cache[temp]Python 会直接报语法错误。3.3 从尾部弹出键值对dict.popitem()再来仔细演示popitem()。它最常见的用途之一是把字典当成一个简易的“待处理任务栈”tasks { task_1: {cmd: ping, timeout: 3}, task_2: {cmd: nslookup, timeout: 5}, task_3: {cmd: curl, timeout: 8}, } while tasks: key, detail tasks.popitem() print(f处理 {key}: {detail})输出顺序是 task_3、task_2、task_1类似后进先出。这里我习惯用两个变量去接收返回值因为popitem()返回的是元组拆包后更直观。有些资料里会说“popitem 随机弹出一个元素”这在 Python 3.6 及以前是可观察到的行为。但如果你现阶段用的是 Python 3.8、3.10、3.11、3.12 等等它已经统一成“弹最后一个插入的元素”。所以不要再依赖随机性要做随机删除的话先random.choice(list(d.keys()))再pop更直接。3.4 想要先进先出FIFO怎么办普通dict虽然按插入顺序迭代但popitem()只能从尾部弹。如果你想从头部弹有个很直接的方案是list(d)拿到 key 列表再循环 popd {a: 1, b: 2, c: 3} for key in list(d.keys()): value d.pop(key) print(key, value)这样做确实能从头部到尾部逐个处理但每次循环都要先生成一个包含全部 key 的列表如果字典很大会额外占用一些内存。更工整的做法是用collections.OrderedDictfrom collections import OrderedDict od OrderedDict([ (a, 1), (b, 2), (c, 3), ]) first_key, first_value od.popitem(lastFalse) print(first_key, first_value) print(od)OrderedDict.popitem(lastFalse)会从头部弹出元素。普通dict没有这个参数所以如果你确实需要一个先进先出的“词典队列”用OrderedDict是兼容性更稳的选择。4. 从题目场景出发画出“返回并删除”的完整用例光会写方法还不够一个方法好不好用要在具体业务场景里看。下面我用几个高频场景帮你把知识点串起来。4.1 场景一一次性配置项的读取和清理写脚本时经常有这种需求读入一份配置字典一部分配置项要在初始化时取出来取完以后就不再占用内存了。举个例子你有一个配置字典settings { mode: debug, log_path: ./logs, db_host: 127.0.0.1, db_port: 3306, }程序启动后你可能要把 mode 取出来并且不希望它继续留在字典里被后续逻辑扫描到mode settings.pop(mode, release) db { host: settings.pop(db_host), port: settings.pop(db_port), }把这类只初始化使用一次的参数从字典里弹出去字典会从“完整配置”慢慢变成“剩余配置”后续如果做全量校验、序列化、日志输出就不会把已经消费过的字段再次带出去。4.2 场景二用字典模拟任务队列有时候你不想引入复杂的数据结构只需要快速处理一批任务而且后加入的任务优先级更高。那可以直接用 dict 加 popitem。一个典型的例子是目录扫描你发现新目录希望先把最新发现的目录处理掉而不是从老目录开始pending { /project/src: [a.py, b.py], /project/tests: [test_a.py], /project/docs: [readme.md], } while pending: current_dir, files pending.popitem() for file in files: print(f处理 {current_dir}/{file})这种实现代码很短而且因为是 dict天然支持去重。同一个目录如果被加入两次后一次会覆盖前一次不会保留两份相同 key 的任务。4.3 场景三API 参数白名单清理如果你写了一个函数接收关键字参数**kwargs但你只想把某些参数单独弹出来使用剩下的继续传给下游函数pop会非常顺手。def create_connection(**kwargs): host kwargs.pop(host, 127.0.0.1) port kwargs.pop(port, 5432) # kwargs 里可能还剩下 timeout、retry 等参数 return connect_to_db(hosthost, portport, **kwargs)这样设计的好处是函数对外接收的参数类型可以非常灵活内部通过 pop 把已经识别并消费掉的参数剔除掉剩下的参数还能继续作为一个整体往下传。这比用kwargs.get(host)再把值改成 None 干净很多。4.4 场景四不要把 pop 当成只读访问用pop有一个副作用它会修改原字典。新手最容易犯的错是想取某个 key 的值但不想删结果发现原来的字典少了一项d {name: 张三, age: 18} name d.pop(name) # 这里是会删除 name 的 print(d) # {age: 18}如果你只想取 key 对应的 value 而不删除应该用d.get(name)或d[name]。这两个方法不会修改原字典。用get还有一层好处当 key 不存在时可以返回一个默认值不会抛异常。使用索引d[name]时如果 key 缺失会抛 KeyError。把这三个操作放在一起比较需求推荐方法返回值是否修改原字典key 缺失表现只取值不删d.get(key)value 或 None否返回 None强制取值不删d[key]value否抛 KeyError取值且删除d.pop(key)value是抛 KeyError 或返回默认值这个表建议收藏写 Python 时经常会在它们之间摇摆。4.5 场景五批量清理一批 key如果要从一个字典中删除多个 key而且每个 key 对应值都需要保留到一个结果字典里可以写一个简单的循环source {a: 1, b: 2, c: 3, d: 4, e: 5} to_process [a, c, d] result {} for key in to_process: if key in source: result[key] source.pop(key) print(result) # {a: 1, c: 3, d: 4} print(source) # {b: 2, e: 5}如果你想检查并删除的同时还希望避免重复判断也可以直接利用默认值value source.pop(key, None)但这里有个隐藏坑如果字典里某个 key 对应的 value 本身就是 None你无法判断这个 key 到底存不存在。不过绝大多数业务里值不会是 None所以问题不大。如果真的需要做严格判断还是要用key in source。5. 常见误区和排查清单这一节我把自己踩过以及帮别人排查过的问题汇总一下方便你遇到类似报错时快速定位。5.1 KeyError 应该怎么避免pop和del在 key 不存在时会抛 KeyError。特别是在做用户输入映射、外部接口字段解析时字典内容往往是不确定的比较容易触发。规避方式通常有两种。第一种直接让缺省值兜底name data.pop(name, 未知用户)第二种先用if判断存在性不存在就走另一个分支if name in data: name data.pop(name) else: name 未知用户第一种代码更短适合“缺失时当作默认值处理”的场景第二种代码更明确适合“缺失时需要执行其他分支逻辑”的场景。两者没有冲突按实际场景选择。5.2 遍历字典时直接删除会报 RuntimeError很多人在处理字典时想边遍历边删除d {a: 1, b: 2, c: 3} for k in d: if k b: d.pop(k)这段代码在 Python 3 里通常会出现RuntimeError: dictionary changed size during iteration原因是你在遍历过程中改变了字典大小导致迭代器失效。正确处理方式有三种# 方式一迭代 list(d.keys()) for k in list(d.keys()): if k b: d.pop(k) # 方式二先找出要删的 key循环结束后再删 to_delete [k for k in d if k b] for k in to_delete: del d[k] # 方式三不断地 popitem 直到满足条件 while d: k, v d.popitem() if should_keep(k): d[k] v方式一最常用牺牲一点内存换稳定方式三适合你想清空字典同时只保留一部分内容的场景。5.3 popitem 在空字典上调用会怎样如果你执行{}.popitem()结果是KeyError: popitem(): dictionary is empty它和pop缺失 key 的 KeyError 不一样。在写“任务处理循环”时一定要保证循环条件能够判断字典是否为空。最常见的写法是while tasks: key, value tasks.popitem()这样字典一旦为空循环自然结束不会触发空字典异常。如果是在不确定的场景里调用popitem可以先判断一下if tasks: key, value tasks.popitem() else: key, value None, None5.4 字典复制时影响原字典的问题如果你需要保留一份原始字典然后再从某个副本里弹值original {name: 张三, age: 18} copy_data dict(original) copy_data.pop(name) print(original) # 仍然是 {name: 张三, age: 18}用dict(original)或original.copy()得到的都是浅拷贝。只删除最外层的 key 没有问题但如果 value 本身是可变对象比如列表那浅拷贝内外层仍然可能共享同一个列表。这时候可以用copy.deepcopy不过大多数“弹出键值对”的场景不涉及双层修改浅拷贝够用。5.5 用类型注解和 linter 帮助避免错误调用现代 Python 开发基本都离不开类型提示。给 dict 写好类型注解后很多错误调用在 IDE 里就会先暴露一次user_map: dict[str, str] {name: 张三} # IDE 会提示 popitem 不接受参数 item user_map.popitem(name)再配合 Ruff、Pyright、Mypy 这类静态检查工具错误的测试代码在运行前就能得到警告。说到底很多“dict.popitem(key)”的错误是因为缺少权威参照所以建议遇到不确定的方法签名时直接在 REPL 里看help(dict.popitem) help(dict.pop)5.6 官方文档与八股文的偏差有些二手资料会把“popitem 随机返回一个键值对”写进面试题标准答案。严格来说在 Python 3.7 之后的语义里不应该再用“随机”来描述它。如果你面试时被问“popitem 是随机删除吗”可以回答Python 3.7 之前没有顺序保证3.7 之后是 LIFO也就是删除并返回最后插入的键值对。这样既体现了版本意识也不会被老旧的笔记误导。6. 我在项目中实际使用 pop/popitem 的几点体会最后说点个人经验不一定每条都能直接用到代码里但多多少少能帮你养出更好的手感。6.1 我习惯用 pop 做配置收敛一个工具脚本里config 字典可能包含二十多个字段但前端逻辑只关心其中几个。我经常会在函数入口做一层“配置剥离”def run(config: dict): name config.pop(name) retries config.pop(retries, 3) verbose config.pop(verbose, False) # 剩下的 config 如果非空说明存在未识别的配置项 if config: raise ValueError(f未知配置: {list(config.keys())})这个模式的好处是当用户错误传入了不支持的配置项你能第一时间知道而不是让多余的字段悄悄沉默到最后。比起直接用config[name]去读最后再一个个手动删除pop的收敛效果更明显。6.2 用一个小脚本验证“返回并删除”的记忆点如果你和我一样也经常担心自己记混pop和popitem可以直接在终端里跑一小段“自问自答”脚本d {a: 1, b: 2, c: 3} # 场景 A我想要按 keyb 取出值并删除 result d.pop(b) print(pop(b) -, result, 剩余:, d) # 场景 B我想要从尾部取出一整对并删除 key, value d.popitem() print(popitem() -, key, value, 剩余:, d)这段脚本的输出会告诉你最核心的区别pop(b) - 2 剩余: {a: 1, c: 3} popitem() - c 3 剩余: {a: 1}看到结果后基本不容易再写错。6.3 敏感数据的“弹出”和日志脱敏如果字典中存储了密码、token、密钥之类的敏感信息我从一开始就不建议把它们长时间放在普通字典里。即使你写的是 demo也要注意日志打印之前先弹出或用掩码处理request_data { username: user01, password: 123456, platform: web, } password request_data.pop(password) print(request_data) # 不会带出 password现实中数据库密码、API Key、加密私钥之类的内容最好从环境变量或密钥管理服务中读取用完尽量不要留在内存里。Python 的pop只是帮助你从字典结构中移除数据它解决的是“数据结构该不该继续保留这个字段”的问题不能代替完整的安全流程。6.4 把“返回并删除”扩展成自己的工具函数如果你在大量代码里都要执行“按 key 返回整个键值对并把它包装成一个小字典”这种特殊需求可以自己封装一个函数def pop_as_dict(source: dict, key: str, defaultNone): if key in source: value source.pop(key) return {key: value} return default它不是 Python 内置 API但当你真正需要完整“词条”而不是 value 时它比反复写{key: source.pop(key)}少一层重复。这也算是在理解 dict 原生活法之上做一点面向业务的自定义扩展。回到文章最开头那句“dict.popitem(key) 返回字典中关键字为 key 的词条的内容并删除该词条”。正确理解成dict.pop(key)后这个需求本身没有任何问题。现在我更愿意说先分清你手里拿的是 key 还是位置再决定用 pop 还是 popitem。字典不是一锅粥它是带编号抽屉的柜子你以为在按标签取物实际却从一个出口往外拿出错也就不奇怪了。我在实际调试代码时最深的体会是类似这种一两个词的 API 混淆看起来是小事一旦在数据处理、参数清理、任务分发这种关键路径上跑错排查成本一点也不低。希望这篇能把你的字典操作基础再夯一层下次再看到popitem你应该能条件反射地想到别传 key它不是干这个的。