Python的默认参数把我坑惨了,原来可变对象和不可变对象的区别这么大
一个让我排查了三个小时的Bug去年写了一个用户行为追踪模块需要对每个访问用户记录一系列操作日志def log_user_action(action, history[]): history.append(action) print(f当前操作: {action}) print(f操作历史: {history}) return history看起来平平无奇对吧一个函数记录用户操作默认参数history[]让每次调用都从空列表开始。测试的时候单独跑一切正常log_user_action(登录) # 当前操作: 登录 # 操作历史: [登录]上线之后奇怪的事情发生了。运维同事跑过来跟我说“日志系统出问题了用户A的日志里全是B用户的操作。”我赶紧去查代码模拟了两个用户交替调用# 用户A log_user_action(A登录) log_user_action(A浏览商品) # 用户B log_user_action(B登录)猜猜输出是什么当前操作: A登录 操作历史: [A登录] 当前操作: A浏览商品 操作历史: [A登录, A浏览商品] 当前操作: B登录 操作历史: [A登录, A浏览商品, B登录] # B的列表里有A的数据用户B的操作历史里赫然出现了用户A的操作记录。两个用户的数据串在一起完全乱套了。我盯着代码看了半天history[]明明每次都传了个空列表怎么会累积呢那天下午我把Python的可变和不可变对象、默认参数的求值时机彻底研究了一遍。今天把这些坑讲清楚希望你别重蹈我的覆辙。第一步先搞清楚可变和不可变对象Python里的对象分两种不可变对象一旦创建值就不能改。数字、字符串、元组、布尔值、None都属于这一类。a 1 b a a 2 # 这不是改1是让a指向2 print(b) # 还是1 —— b指向的对象没变当你以为你在“改”一个不可变对象时实际上是创建了一个新对象然后让变量指向新对象。s hello print(id(s)) # 假设是 140234567890 s s world print(id(s)) # 新的id —— 创建了新字符串可变对象创建之后可以原地修改内容。列表、字典、集合、自定义类的实例都属于这一类。lst [1, 2, 3] print(id(lst)) # 假设是 140234567890 lst.append(4) print(id(lst)) # 还是 140234567890 —— 对象没变内容变了可变对象就像一块白板你可以在上面擦擦写写白板还是那块白板。不可变对象就像刻在石头上的字想改只能换块石头。第二步Python默认参数的一个冷门特性搞清楚可变和不可变之后现在说关键问题Python函数的默认参数在函数定义时就被求值了而不是在函数调用时。什么意思def add(x, y[]): y.append(x) return y当Python解释器读到def add(x, y[]):这一行的时候它就执行了[]创建了一个空列表对象然后把这个对象“焊死”在函数对象上。之后的每一次调用用的都是这同一个列表对象。每次调用add(1)都是往那个同一个列表里追加元素。这就是为什么用户B的日志里会出现用户A的数据——他们用的是同一个history列表。验证一下def test_default(arg[]): arg.append(1) print(id(arg)) # 每次打印的id都一样 test_default() # 打印某个id test_default() # 打印同样的id test_default() # 还是同样的id三调用的arg指向的是同一个列表对象。但如果是不可变对象作为默认参数def test_default(arg1): arg arg 1 print(id(arg)) # 每次打印的id都不同 test_default() # 某个id test_default() # 不同的id test_default() # 又是不同的id每次对不可变对象进行“修改”实际上是创建了新对象原来的默认参数对象纹丝不动。第三步为什么会有这种设计你可能会问为啥Python要这么设计这不是坑人吗其实这个设计背后的逻辑是Python的函数是对象默认参数是函数对象的属性。def foo(x, y10): pass print(foo.__defaults__) # (10,)默认参数在函数定义时被求值作为元组保存在__defaults__属性里。这样每次调用函数的时候直接从这个属性里读默认值不需要重新求值——提高了执行效率。这个设计对不可变对象来说完全没问题。10就是10谁来了它都不变。但问题是Python不限制默认参数的类型。如果你用了可变对象列表、字典这个对象也是定义时创建一次然后被所有调用共享。这就出事了。这算不算设计缺陷Python之父Guido van Rossum在2012年的邮件里承认“这个问题让每个人都踩过坑。如果时光倒流我会让默认参数在函数调用时求值。”但作为一门已经跑了几十年的语言这种兼容性问题改不了只能靠开发者自己注意。第四步正确的写法既然知道了问题怎么改用None做哨兵值在函数内部创建新对象# 错误写法 def log_user_action(action, history[]): history.append(action) return history # 正确写法 def log_user_action(action, historyNone): if history is None: history [] history.append(action) return history现在每次调用如果没传history都会创建一个全新的空列表。用户之间不再互相污染log_user_action(A登录) # [A登录] log_user_action(A浏览) # [A登录, A浏览] log_user_action(B登录) # [B登录] —— 干净的新列表None是不可变对象作为默认参数绝对安全。用None做哨兵在函数内部判断并创建可变对象这是Python社区公认的标准写法。同样的套路适用于所有可变对象# 字典 def process_data(data, configNone): if config is None: config {} # ... # 集合 def track_tags(tags, tag_setNone): if tag_set is None: tag_set set() # ... # 自定义对象 def create_user(name, profileNone): if profile is None: profile UserProfile() # ...第五步还有一些更隐蔽的坑坑一类属性里的可变对象class User: permissions [] # 类属性所有实例共享 alice User() bob User() alice.permissions.append(admin) print(bob.permissions) # [admin] —— Bob也被加了权限正确写法是在__init__里创建实例属性class User: def __init__(self): self.permissions [] # 每个实例自己的一份坑二函数默认参数依赖另一个可变默认参数# 极其危险的写法 def build_url(base, params{}): # ... def make_request(urlbuild_url(), headers{}): # ...默认参数之间如果有依赖关系一个变了另一个也跟着变调试的时候能把人逼疯。坑三缓存陷阱有人想用默认参数做缓存def expensive_calc(key, cache{}): if key in cache: return cache[key] result really_slow_calculation(key) cache[key] result return result这代码看起来聪明实际上有问题——cache是全局共享的不会被清理内存会一直涨。而且测试之间会互相污染。正确的做法是用functools.lru_cache。第六步一个常见的面试题这个坑太经典了面试官特别喜欢考def func(a, b[]): b.append(a) return b print(func(1)) print(func(2)) print(func(3, [])) # 注意这里传了参数 print(func(4))输出是什么func(1)[1]func(2)[1, 2]—— 同一个列表func(3, [])[3]—— 传了新列表不受默认参数影响func(4)[1, 2, 4]—— 又回到了那个累积的列表这道题考的就是你对默认参数求值时机和可变对象共享的理解。能答上来说明你已经避开了这个坑。总结一句话记住默认参数在定义时只创建一次可变对象会被所有调用共享不可变对象安全可变对象危险。用我那个日志系统的例子来总结**用history[]**像是给所有用户发了一本共同的日记本A写的内容B也能看到**用historyNone**像是每个用户发了一本全新的空白日记本各写各的现在我写函数但凡默认参数是可变的列表、字典、集合一定用None做哨兵在函数内部创建。这个习惯救了我无数次希望也能救你。有一个简单的记忆方法默认参数用不可变可变对象内部造。每次写完函数看一眼默认参数如果是[]或{}立马改成None。想清楚再动手少排查几小时Bug。

相关新闻

上下文工程:提升AI对话连贯性与计算效率的关键技术

上下文工程:提升AI对话连贯性与计算效率的关键技术

1. 上下文工程:AI应用体验优化的核心技术在AI应用开发领域,工程师们常常面临一个关键挑战:如何让AI系统保持对话连贯性、理解复杂任务背景,同时控制计算成本?这正是上下文工程(Context Engineering&#xf…

2026/7/23 7:43:44阅读更多 →
备受国自然评审青睐的蛋白互作研究利器:PLA 邻位连接技术

备受国自然评审青睐的蛋白互作研究利器:PLA 邻位连接技术

邻位连接技术(Proximity Ligation Assay,PLA)是一类整合抗体特异性识别与核酸信号放大能力的高精准蛋白质分析技术,无需基因修饰即可在单分子水平原位解析蛋白质相互作用、翻译后修饰事件,是蛋白功能机制研究中认可度极…

2026/7/23 7:41:43阅读更多 →
Agent SaaS:从工具到劳动力的范式转变

Agent SaaS:从工具到劳动力的范式转变

1. Agent作为新一代SaaS的范式转移当Slang AI的餐厅预订系统能在电话铃响三声内完成90%的客户咨询时,传统SaaS的商业模式正在被重新定义。这不是简单的技术升级,而是从"工具提供商"到"劳动力供应商"的根本性转变。我最近帮三家本地餐…

2026/7/23 7:41:43阅读更多 →
Nacos一致性协议解析:AP与CP模式的设计与实践

Nacos一致性协议解析:AP与CP模式的设计与实践

1. Nacos一致性协议的本质解析 Nacos作为阿里巴巴开源的动态服务发现、配置管理和服务管理平台,其核心设计理念中关于一致性协议的选择一直是开发者关注的焦点。要理解Nacos的AP/CP特性,我们需要从分布式系统的基础理论入手。 1.1 CAP理论在Nacos中的体…

2026/7/23 9:08:11阅读更多 →
依连山易推演:古巫语分化的两条文明路径

依连山易推演:古巫语分化的两条文明路径

序言 自上古连山易立极定道,阴阳爻象便构筑起华夏文明天人共振的思想框架。依托后世语言学资料与上古石刻遗存,循易理推想:人类早期的原生巫语,或是连山易音卦合一的承载形式 —— 符号对应爻象,发音契合卦理&#xf…

2026/7/23 9:08:11阅读更多 →
【无人机求援】任务实用程序的多无人机灾难响应框架协助自然灾害期间的救援行动【含Matlab源码 15895期】

【无人机求援】任务实用程序的多无人机灾难响应框架协助自然灾害期间的救援行动【含Matlab源码 15895期】

💥💥💥💥💥💥💥💥💞💞💞💞💞💞💞💞💞Matlab武动乾坤博客之家💞…

2026/7/23 9:08:11阅读更多 →
寒门学子|从国家励志奖学金到国家奖学金:普通人逆袭,核心是搭建属于自己的成长思维体系

寒门学子|从国家励志奖学金到国家奖学金:普通人逆袭,核心是搭建属于自己的成长思维体系

家境只能决定起点,持续学习、独立思维、长远规划,才能决定人生终点 出身普通的孩子,没有先天优势可以依靠。真正拉开人与人差距的,从来不是原生家境,而是持续学习的习惯、独立思考的能力,以及永不言弃的极…

2026/7/23 9:08:11阅读更多 →
2026年GEO服务商市场格局与AI认知优化策略

2026年GEO服务商市场格局与AI认知优化策略

1. 2026年GEO服务商市场格局解析 当企业官网流量连续三个季度下滑15%时,我们终于意识到问题的严重性——传统SEO策略在生成式AI时代正在失效。最近帮某B2B科技企业做诊断时发现,虽然其官网SEO评分高达92分,但在ChatGPT、Kimi等AI平台关于&quo…

2026/7/23 9:08:11阅读更多 →
Hive sql 进阶题 03

Hive sql 进阶题 03

用户注册、登录、下单综合统计从用户登录明细表(user_login_detail)和订单信息表(order_info)中查询每个用户的注册日期(首次登录日期)、总登录次数以及其在2021年的登录次数、订单数和订单总额。select t1…

2026/7/23 9:06:11阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/23 0:56:31阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/23 0:56:31阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 0:56:31阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:00:28阅读更多 →
从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:28阅读更多 →
油泥处理设备哪里能买到

油泥处理设备哪里能买到

油泥处理设备哪里有?这是许多从事油田、炼化、清罐业务的从业者最关心的问题。根据河南三丰环保设备有限公司的行业经验,选购油泥处理设备的核心在于设备能否适配当地环保法规与原料特性,而非单纯看价格。该公司总经理王钦田先生指出&#xf…

2026/7/23 0:00:28阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/22 22:56:18阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/22 18:55:50阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/22 18:55:50阅读更多 →