首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一个空列表让我加了两天班——Python默认参数的隐藏陷阱

一个空列表让我加了两天班——Python默认参数的隐藏陷阱

原创
作者头像
风一样的男子
发布2026-07-10 13:47:59
发布2026-07-10 13:47:59
60
举报
文章被收录于专栏:编程教程编程教程

一次让我怀疑人生的排错经历

上个月我接了个订单统计系统的活儿。需求很简单:给每个商品打标签,如果调用时没给标签列表,就自动新建一个空列表来装。

我三下五除二写完了代码:

代码语言:javascript
复制
def add_tag(item, tags=[]):
    tags.append(item)
    return tags

简单测了一下,完全OK。于是部署上线,开始接收真实数据。

第二天,运营同事跑过来跟我说:"你这系统是不是有问题?我明明只给'电子产品'加了三个标签,为什么'包邮'商品里也出现了'电子产品'的标签?"

我一查数据库,惊了——很多商品的标签都串在一起了。查了半天代码,发现问题就出在那行看似人畜无害的def add_tag(item, tags=[]):上。

一个空列表,让我加了两天班。


怎么回事?Python的默认参数藏着一个坑

先来复现一下这个"灵异"现象:

代码语言:javascript
复制
def add_tag(item, tags=[]):
    tags.append(item)
    return tags

print(add_tag("电子产品"))   # ['电子产品']
print(add_tag("包邮"))       # ['电子产品', '包邮']  ← 意外!
print(add_tag("今日特价"))   # ['电子产品', '包邮', '今日特价']

看到第二次的输出,我整个人都不好了:tags明明是空列表,为什么第一次追加的"电子产品"还在里面?

原因只有一个:Python的默认参数是在函数定义时创建的,而不是在函数调用时创建的。

当Python解释器读到def add_tag(item, tags=[]):这一行时,它创建了一个空列表,然后把这个列表固定下来,作为tags的默认值。以后每次调用add_tag(),如果不传tags参数,用的永远都是同一个列表对象

id()验证一下:

代码语言:javascript
复制
def add_tag(item, tags=[]):
    print(f"tags的内存地址: {id(tags)}")
    tags.append(item)
    return tags

add_tag("A")  # tags的内存地址: 140234567890
add_tag("B")  # tags的内存地址: 140234567890 ← 同一个!

两次调用的内存地址完全一样,说明它们用的是同一个列表对象。这就是"空列表"不断累积的原因。


这个坑会出现在哪些地方?

场景1:Web API的参数累积

代码语言:javascript
复制
def fetch_data(url, params={}):
    params['timestamp'] = time.time()
    response = requests.get(url, params=params)
    return response

第一次调用,params是空字典,加上timestamp后发送请求。 第二次调用,params还是那个字典,里面已经有一个timestamp了,你又加了一个新的——变成两个timestamp参数,API直接懵了。

场景2:类实例化的共享列表

代码语言:javascript
复制
class Order:
    def __init__(self, items=[]):
        self.items = items

order1 = Order()
order1.items.append("手机")

order2 = Order()
print(order2.items)  # ['手机'] —— 不是空列表!

你想让每个订单有独立的商品列表,结果所有订单都共享同一个列表。这个Bug尤其隐蔽——因为代码看起来完全没毛病。

场景3:数据验证函数的错误累积

代码语言:javascript
复制
def validate(data, errors=[]):
    if 'name' not in data:
        errors.append('缺少name字段')
    if 'age' not in data:
        errors.append('缺少age字段')
    return errors

errors1 = validate({'name': '张三'})  # ['缺少age字段']
errors2 = validate({'age': 18})        # ['缺少age字段', '缺少name字段'] —— 混了

第二次调用的错误列表里包含了第一次的错误,因为用的是同一个列表。


为什么Python会这么设计?

你可能会问:Python为什么要把默认参数设计成"定义时创建",而不是"调用时创建"?

这是有意为之的。

第一,性能考虑。 函数定义只执行一次,如果每次调用都重新创建默认参数(即使是空列表),会带来不必要的开销。虽然对空列表来说开销不大,但如果默认参数是一个复杂的对象,这个设计就能节省不少时间。

第二,这确实是一个特性。 在某些场景下,你确实需要函数调用之间保持状态。比如带缓存的函数:

代码语言:javascript
复制
def fibonacci(n, cache={}):
    if n in cache:
        return cache[n]
    if n <= 1:
        return n
    cache[n] = fibonacci(n-1) + fibonacci(n-2)
    return cache[n]

这个代码利用了默认参数的持久性来缓存计算结果,大幅提升了性能。

但问题在于:这个特性对新手不友好,而且在大多数场景下,用默认参数保存状态是过度设计。


标准解法:None占位符

正确的写法很简单,所有Python程序员都公认的"最佳实践":

代码语言:javascript
复制
def add_tag(item, tags=None):
    if tags is None:
        tags = []
    tags.append(item)
    return tags

测试一下:

代码语言:javascript
复制
print(add_tag("电子产品"))   # ['电子产品']
print(add_tag("包邮"))       # ['包邮']
print(add_tag("今日特价"))   # ['今日特价']

这次每次调用都创建了全新的列表,各不干扰。

经验法则:只要默认参数是可变对象(列表、字典、集合、自定义类实例),就用None占位,在函数内部重新创建。

不可变对象(字符串、整数、浮点数、元组、布尔值)不需要这个技巧,因为函数内部无法修改它们。


带类型注解的写法

如果你用类型注解,可以这样写:

代码语言:javascript
复制
from typing import Optional, List

def add_tag(item: str, tags: Optional[List[str]] = None) -> List[str]:
    if tags is None:
        tags = []
    tags.append(item)
    return tags

Python 3.10+可以用更简洁的写法:

代码语言:javascript
复制
def add_tag(item: str, tags: list[str] | None = None) -> list[str]:
    if tags is None:
        tags = []
    tags.append(item)
    return tags

| None明确表示这个参数可以是None,类型检查工具不会报警。


类场景下的正确写法

代码语言:javascript
复制
class Order:
    def __init__(self, items: list | None = None):
        if items is None:
            items = []
        self.items = items

# 现在每个订单的items都是独立的
order1 = Order()
order1.items.append("手机")

order2 = Order()
print(order2.items)  # [] —— 正常了

一张表总结

场景

错误写法

正确写法

默认空列表

def f(x=[]):

def f(x=None): if x is None: x = []

默认空字典

def f(x={}):

def f(x=None): if x is None: x = {}

默认空集合

def f(x=set()):

def f(x=None): if x is None: x = set()

默认自定义对象

def f(x=MyClass()):

def f(x=None): if x is None: x = MyClass()


检查工具能帮你发现这个问题

现代Python工具会自动检测这个陷阱:

  • PylintW0102: dangerous-default-value
  • Flake8(通过插件):检测可变默认参数
  • Pyright/mypy:类型检查能发现不一致

如果你用VSCode + Pylance,默认就有相关提示。看到这个警告,立刻改成None模式。


回到开头

那次加班排查之后,我在公司的代码规范里加了一条:

所有函数的默认参数,如果是可变类型,必须用None占位,在函数内部重新创建。

从那以后,再也没有因为"空列表"加过班。

记住一句话:Python的默认参数在def时创建,不在调用时创建。可变对象做默认参数,数据会跨调用累积。

这个道理很简单,但如果不理解,一个空列表就能让你加两天班。希望你是那个"不用加班"的人。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一次让我怀疑人生的排错经历
  • 怎么回事?Python的默认参数藏着一个坑
  • 这个坑会出现在哪些地方?
    • 场景1:Web API的参数累积
    • 场景2:类实例化的共享列表
    • 场景3:数据验证函数的错误累积
  • 为什么Python会这么设计?
  • 标准解法:None占位符
  • 带类型注解的写法
  • 类场景下的正确写法
  • 一张表总结
  • 检查工具能帮你发现这个问题
  • 回到开头
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档