Skip to content

Python函数中的可变对象传递:新手避坑指南

在Python中,数据类型分为可变类型和不可变类型。 理解它们在函数中的行为对于编写清晰、可预测的代码至关重要。本节主要探讨可变对象(如listdict)作为参数传递给函数时,会发生什么。

简单来说,当我们将可变对象传递给函数时,函数会获得对该对象引用的副本。这意味着函数内部对该对象的修改会影响到原始对象。这就是所谓“原地修改”。 因此,在使用可变对象作为函数参数时,务必小心谨慎。

什么是可变对象和不可变对象?

在深入函数传递之前,我们先来明确一下可变对象和不可变对象的概念。

  • 可变对象:其值可以在创建后被修改。 常见的可变对象包括:
    • 列表 ( list )
    • 字典 ( dict )
    • 集合 ( set )
  • 不可变对象:其值在创建后不能被修改。 常见的不可变对象包括:
    • 整数 ( int )
    • 浮点数 ( float )
    • 字符串 ( str )
    • 元组 ( tuple )

记住,不可变对象的值虽然不能直接修改,但可以通过重新赋值来改变变量的指向。

列表的传递与修改

让我们通过一个例子来详细说明列表作为函数参数时的行为。

代码示例:

python
def double_list_elements(my_list):
    """将列表中的每个元素乘以 2 (原地修改)"""
    for i in range(len(my_list)):
        my_list[i] = my_list[i] * 2
    print("函数内部修改后:", my_list)


original_list = [1, 2, 3]
print("调用函数前:", original_list)
double_list_elements(original_list)
print("调用函数后:", original_list)

输出结果:

调用函数前: [1, 2, 3]
函数内部修改后: [2, 4, 6]
调用函数后: [2, 4, 6]

代码解析:

  1. 我们定义了一个名为 double_list_elements 的函数,它接受一个列表作为参数。
  2. 函数内部,我们遍历列表的每个元素,并将它们乘以 2。 关键在于,我们直接修改了传入的列表 my_list
  3. 在函数外部,我们创建了一个名为 original_list 的列表,并将其传递给函数。
  4. 运行结果表明,函数内部对 my_list 的修改,影响到了函数外部的 original_list。 这就是因为列表是可变对象,函数传递的是对列表的引用。

重要提示: 函数 double_list_elements 修改了原始列表。 如果你的目的是保留原始列表,可以考虑在函数内部创建一个列表的副本,并在副本上进行操作。

字典的传递与修改

字典的行为与列表类似。 让我们看一个字典的例子。

代码示例:

python
def double_dict_values(my_dict):
    """将字典中每个值乘以 2 (原地修改)"""
    for key in my_dict:
        my_dict[key] = my_dict[key] * 2
    print("函数内部修改后:", my_dict)


original_dict = {'a': 1, 'b': 2, 'c': 3}
print("调用函数前:", original_dict)
double_dict_values(original_dict)
print("调用函数后:", original_dict)

输出结果:

调用函数前: {'a': 1, 'b': 2, 'c': 3}
函数内部修改后: {'a': 2, 'b': 4, 'c': 6}
调用函数后: {'a': 2, 'b': 4, 'c': 6}

代码解析:

  1. double_dict_values 函数遍历字典的键,并将每个键对应的值乘以 2。
  2. 同样,函数内部对字典 my_dict 的修改,反映到了函数外部的 original_dict

如何避免修改原始对象?

如果你不希望函数修改原始的可变对象,可以创建该对象的一个副本,并在函数内部操作该副本。 Python 提供了多种创建副本的方法:

  • 列表:
    • 使用 list() 构造函数: new_list = list(original_list)
    • 使用切片: new_list = original_list[:]
    • 使用 copy 模块: import copy; new_list = copy.copy(original_list) (浅拷贝)或者 new_list = copy.deepcopy(original_list) (深拷贝)
  • 字典:
    • 使用 dict() 构造函数: new_dict = dict(original_dict)
    • 使用 copy 方法: new_dict = original_dict.copy()
    • 使用 copy 模块: import copy; new_dict = copy.copy(original_dict) (浅拷贝)或者 new_dict = copy.deepcopy(original_dict) (深拷贝)

代码示例(列表副本):

python
import copy

def modify_list(my_list):
    """修改列表,但不影响原始列表"""
    new_list = copy.copy(my_list) # 创建列表的浅拷贝
    for i in range(len(new_list)):
        new_list[i] = new_list[i] * 2
    print("函数内部修改后:", new_list)

original_list = [1, 2, 3]
print("调用函数前:", original_list)
modify_list(original_list)
print("调用函数后:", original_list) # 原始列表未被修改

输出结果:

调用函数前: [1, 2, 3]
函数内部修改后: [2, 4, 6]
调用函数后: [1, 2, 3]

浅拷贝与深拷贝:

  • 浅拷贝: 创建一个新的对象,但新对象中的元素是对原始对象中元素的引用。如果原始对象中的元素是可变对象,修改其中一个对象的元素,另一个对象也会受到影响。
  • 深拷贝: 创建一个全新的对象,包括所有嵌套的子对象。修改其中一个对象,完全不会影响到另一个对象。

对于只包含不可变对象的列表或字典,浅拷贝已经足够。 但如果你的可变对象中包含了其他可变对象(例如,列表的列表),则需要使用深拷贝来确保完全独立。

  • 当可变对象(列表、字典等)作为参数传递给函数时,函数会获得对该对象引用的副本。
  • 在函数内部修改这个引用指向的对象,会影响到原始对象。
  • 如果不想修改原始对象,请在函数内部创建该对象的副本,并在副本上进行操作。
  • 理解浅拷贝和深拷贝的区别,并根据需要选择合适的方法来创建副本。

掌握可变对象在函数中的行为,可以帮助你编写出更健壮、更易于维护的Python代码。 避免因意外修改原始对象而导致的bug。 记住,在处理可变对象时要格外小心!