社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

这个Python函数调用3次,竟然输出了这样的结果!

A逍遥之路 • 3 天前 • 18 次点击  

一、事件起因

在一个简单的代码中,发现了一个有趣的现象。一位同事写了一个看起来很正常的函数,但运行结果却让所有人都摸不着头脑。

事情是这样的,他想写一个函数来管理学生名单:

二、问题重现

代码展示

def add_student(name, student_list=[]):    student_list.append(name)    return student_list
# 调用函数class1 = add_student("张三")class2 = add_student("李四"class3 = add_student("王五")
print("班级1:", class1)print("班级2:", class2)print("班级3:", class3)

预期结果

大多数人(包括写代码的同事)都认为输出应该是:

班级1: ['张三']
班级2: ['李四']
班级3: ['王五']

这很合理对吧?每次调用函数都应该创建一个新的学生列表。

实际结果

但是实际运行后,结果却是:

班级1: ['张三', '李四', '王五']
班级2: ['张三', '李四', '王五']
班级3: ['张三', '李四', '王五']

什么?! 三个班级的学生名单竟然完全相同?!

如果你也觉得奇怪,那说明你踩到了Python中最经典的陷阱之一。

三、深度解析

3.1 可变默认参数的真相

这个问题的根源在于Python对可变默认参数的处理机制。

关键概念:函数定义时 vs 函数调用时

def add_student(name, student_list=[]):  # ← 这里是关键!    # ...

误区:很多人认为student_list=[]会在每次调用函数时创建一个新列表。

真相默认参数只在函数定义时计算一次!

3.2 内存地址验证

让我们用内存地址来证明这个现象:

def add_student_debug(name, student_list=[]):    print(f"函数被调用,student_list的地址: {id(student_list)}")    student_list.append(name)    return student_list
# 多次调用观察地址result1 = add_student_debug("张三")result2 = add_student_debug("李四")result3 = add_student_debug("王五")

输出结果

函数被调用,student_list的地址: 140712345678912
函数被调用,student_list的地址: 140712345678912
函数被调用,student_list的地址: 140712345678912

看到了吗?三次调用使用的是同一个列表对象!

3.3 执行过程详解

让我们一步步分析执行过程:

步骤1:函数定义

def add_student(name, student_list=[]):    # Python在这里创建了一个空列表对象    # 这个列表对象被存储在函数的默认参数中    pass

步骤2:第一次调用

class1 = add_student("张三")# student_list指向那个唯一的默认列表# 执行append后:student_list = ["张三"]# 返回:["张三"]

步骤3:第二次调用

class2 = add_student("李四")student_list仍然指向同一个列表(现在内容是["张三"])执行append后:student_list = ["张三""李四"]返回:["张三""李四"]

步骤4:第三次调用

class3 = add_student("王五")student_list仍然指向同一个列表(现在内容是["张三""李四"])执行append后:student_list = ["张三""李四""王五"]返回:["张三""李四""王五"]

四、问题的本质

4.1 Python对象模型

在Python中,一切都是对象,包括函数。函数的默认参数作为函数对象的属性被存储。

def func(x, lst=[]):    return lst
# 查看函数的默认参数print(func.__defaults__)  # ([],)
# 修改默认参数并再次查看func()func().append(1)print(func.__defaults__)  # ([1],)

4.2 为什么这样设计?

这种设计有其合理性:

  1. 性能考虑:避免每次调用都重新计算默认值

  2. 一致性:与其他默认参数的行为保持一致

  3. 可预测性:默认值的计算时机是确定的

但同时也带来了陷阱,特别是对于可变对象。

五、解决方案

5.1 方案一:使用None作为默认值(推荐)

def add_student(name, student_list=None):    if student_list is None:        student_list = []  # 每次调用时创建新列表    student_list.append(name)    return student_list
# 测试class1 = add_student("张三")class2 = add_student("李四")class3 = add_student("王五")
print("班级1:", class1)  # ['张三']print("班级2:", class2)  # ['李四']  print("班级3:", class3)  # ['王五']

5.2 方案二:使用工厂函数

def add_student(name, student_list_factory=list):    student_list = student_list_factory()    student_list.append(name)    return student_list
# 或者使用lambdadef add_student(name, student_list_factory=lambda: []):    student_list = student_list_factory()    student_list.append(name)    return student_list

5.3 方案三:利用"陷阱"实现单例模式

有时候,这个"陷阱"也可以被巧妙利用:

def get_cache(cache=[]):    """利用默认参数实现简单的缓存"""    return cache
def add_to_global_cache(item):    cache = get_cache()    cache.append(item)    return cache
# 所有调用共享同一个缓存cache1 = add_to_global_cache("item1")cache2 = add_to_global_cache("item2")print(cache1)  # ['item1', 'item2']print(cache2)  # ['item1', 'item2']print(cache1 is cache2)  # True

六、扩展思考

6.1 哪些类型会有这个问题?

可变类型(会有问题):

  • list - 列表

  • dict - 字典  

  • set - 集合

  • 自定义的可变对象

不可变类型(不会有问题):

  • intfloatstr - 数字和字符串

  • tuple - 元组

  • frozenset - 不可变集合

6.2 验证实验

# 不可变默认参数 - 正常工作def test_immutable(x, num=0):    num += 1    return num
print(test_immutable(1))  # 1print(test_immutable(2))  # 1  print(test_immutable(3))  # 1
# 可变默认参数 - 有"陷阱"def test_mutable(x, lst=[]):    lst.append(x)    return lst
print (test_mutable(1))  # [1]print(test_mutable(2))  # [1, 2]print(test_mutable(3))  # [1, 2, 3]

6.3 检测工具

一些静态代码分析工具可以检测这个问题:

  • pylint: 会发出W0102警告

  • flake8: 使用flake8-mutable插件

  • PyCharm: 内置检测功能

# pylint示例pylint your_file.py# 输出: W0102: Dangerous default value [] as argument

七、实际案例分析

7.1 Web开发中的真实案例

# 错误的写法 - 可能导致用户数据混乱def create_user_session(user_id, permissions=[]):    permissions.append('read')  # 每个用户都会累积权限!    return {        'user_id': user_id,        'permissions': permissions    }
# 正确的写法def create_user_session(user_id, permissions=None):    if permissions is None:        permissions = []    permissions.append('read')    return {        'user_id': user_id,        'permissions': permissions    }

7.2 配置管理中的案例

# 危险:所有实例共享同一个配置!class ConfigManager:    def __init__(self, settings={}):        self.settings = settings
    def set(self, key, value):        self.settings[key] = value
# 正确做法class ConfigManager:    def __init__( self, settings=None):        self.settings = settings or {}
    def set(self, key, value):        self.settings[key] = value

八、最佳实践总结

8.1 编码规范

  1. 永远不要使用可变对象作为默认参数

  2. 使用None作为可变默认参数的标志

  3. 在函数内部创建可变对象

8.2 代码模板

# 推荐的函数模板def my_function(required_param, optional_list=None, optional_dict=None):    # 处理可变默认参数    if optional_list is None:        optional_list = []    if optional_dict is None:        optional_dict = {}
    # 函数逻辑    # ...

九、总结

这个Python可变默认参数的"陷阱"揭示了几个重要概念:

🎯 核心要点

  1. 默认参数只在函数定义时计算一次

  2. 可变对象会在多次调用间保持状态

  3. 使用None是解决这个问题的标准做法

  4. 理解Python对象模型有助于避免类似陷阱

💡 深层思考

这个例子再次证明了一个道理:看似简单的代码背后可能隐藏着复杂的机制。作为Python开发者,我们需要:

  • 深入理解语言特性

  • 养成良好的编码习惯

  • 使用工具帮助检测潜在问题

  • 编写测试验证代码行为

如果这个例子让你感到意外,那说明你对Python的理解还有提升空间。继续深入学习,你会发现Python还有更多有趣的特性等待探索!


💭 思考题:你在实际开发中遇到过类似的"陷阱"吗?欢迎在评论区分享你的经历!

转发、收藏、在看,是对作者最大的鼓励!👏
关注逍遥不迷路,Python知识日日补!






           对Python,AI,自动化办公提效,副业发展等感兴趣的伙伴们,扫码添加逍遥,限免交流群

备注【成长交流】

图片

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/184059
 
18 次点击