上个月接手一个订单统计系统,需求是对商品打标签,未传入标签列表时自动新建空列表。我快速写出了类似这样的函数:- def add_tag(item, tags=[]):
- tags.append(item)
- return tags
复制代码 测试通过后部署上线。第二天运营反馈:为“电子产品”添加的三个标签,竟然出现在“包邮”商品的标签列表里。数据库查询后发现,大量商品的标签数据互相串扰。排查两天,罪魁祸首就是那个看起来无害的空列表默认参数。
问题复现:- def add_tag(item, tags=[]):
- tags.append(item)
- return tags
- print(add_tag("电子产品")) # ['电子产品']
- print(add_tag("包邮")) # ['电子产品', '包邮'] ← 异常
- print(add_tag("今日特价")) # ['电子产品', '包邮', '今日特价']
复制代码 第二次调用时,“电子产品”依然留在列表中,因为默认参数列表在整个函数生命周期内是同一个对象。验证内存地址:- def add_tag(item, tags=[]):
- print(f"tags内存地址: {id(tags)}")
- tags.append(item)
- return tags
复制代码 两次调用输出的id相同,证明是同一个列表。
Python的默认参数在函数定义时创建,而非调用时创建。这是有意为之的设计:提升性能,避免每次调用都重新创建复杂对象;同时支持一些状态缓存的场景(如斐波那契缓存)。但这一特性对可变对象(列表、字典、集合、自定义类实例)来说是个陷阱,因为函数内部的修改会累积到同一个默认对象上,导致数据污染。不可变对象(字符串、整数、元组等)不会出现此问题。
常见危害场景:
1. Web API参数累积——默认字典导致timestamp重复添加。
2. 类实例共享列表——多个订单实例持有同一个默认列表。
3. 数据验证错误累积——连续调用的错误列表混淆。
标准解法:使用None作为占位符,在函数内部重新创建可变对象。- def add_tag(item, tags=None):
- if tags is None:
- tags = []
- tags.append(item)
- return tags
复制代码 每次调用都会新建列表,互不干扰。同样的模式适用于字典、集合和自定义类:- def f(x=None):
- if x is None:
- x = {} # 或 set(), MyClass()
- ...
复制代码
如果使用类型注解,推荐写法:- 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+可简化为:- def add_tag(item: str, tags: list[str] | None = None) -> list[str]:
- if tags is None:
- tags = []
- tags.append(item)
- return tags
复制代码
类初始化时的正确写法:- class Order:
- def __init__(self, items: list | None = None):
- if items is None:
- items = []
- self.items = items
复制代码
总结:可变对象做默认参数时,务必用None占位,并在函数体内重建。现代工具如Pylint(W0102)、Flake8插件、Pyright/mypy均可检测此类危险默认值。在VSCode + Pylance中,默认就会给出警告。遵循此规则,可以避免一类非常隐蔽的Python陷阱。 |