Py学习  »  Python

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

A逍遥之路 • 1 周前 • 25 次点击  

一、事件起因

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

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

二、问题重现

代码展示

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
 
25 次点击