Python 的传参机制是其内存管理和函数设计的重要组成部分,尤其在处理可变和不可变对象时,可能会导致意想不到的行为。本报告将详细探讨 Python 的传参方式(按值还是按引用),并深入分析如何理解“传对象”,涵盖对象引用的概念、可变性对传参的影响,以及开发者如何在实践中管理这些行为。

背景与问题概述

在编程语言中,传参通常分为按值传递(pass by value)和按引用传递(pass by reference)。按值传递意味着函数接收的是参数的副本,修改不会影响原始变量;按引用传递意味着函数接收的是原始变量的引用,修改会影响原始变量。Python 的传参机制与这些传统概念有所不同,官方文档和社区讨论中常提到“按对象引用传递”(call by object reference)或“按赋值传递”(pass by assignment)。

Python 传参的本质:按对象引用传递

根据官方文档和权威资源,Python 的传参方式是按对象引用传递。这意味着当你将参数传递给函数时,函数接收的是指向同一个对象的引用,而不是对象的副本。具体来说:

  • 函数的参数成为函数局部命名空间中的一个新变量,这个变量绑定到与调用者传递的对象相同的对象。
  • 这种绑定是通过赋值完成的,因此也被称为按赋值传递。

为了理解这一点,我们需要回顾 Python 的对象模型:

  • Python 中一切都是对象,变量只是指向对象的引用(reference)。
  • 当你执行 x = 5x 是一个名称,绑定到整数对象 5。
  • 当你调用 func(x),函数 func 的参数绑定到同一个对象 5。

可变与不可变对象的影响

Python 对象的可变性(mutability)对传参行为有显著影响:

  1. 不可变对象(immutable objects):如整数(int)、字符串(str)、元组(tuple)。这些对象一旦创建就不能修改。
    • 如果函数尝试修改不可变对象的参数(例如重新赋值),实际上是创建了一个新对象,并绑定到参数名称上,但这不会影响调用者的原始变量。
    • 例如:
      1
      2
      3
      4
      5
      6
      def modify_num(num):
      num = 10 # 创建新整数对象 10,绑定到 num

      x = 5
      modify_num(x)
      print(x) # 输出 5,原始变量未变
    • 在这种情况下,行为类似于按值传递,因为无法修改原始对象。
  2. 可变对象(mutable objects):如列表(list)、字典(dict)、集合(set)。这些对象可以被修改。
    • 如果函数修改可变对象的状态(例如追加列表元素),这些修改会反映到调用者的原始对象上,因为两者引用的是同一个对象。
    • 例如:
      1
      2
      3
      4
      5
      6
      def modify_list(lst):
      lst.append(4) # 修改列表,影响原始对象

      y = [1, 2, 3]
      modify_list(y)
      print(y) # 输出 [1, 2, 3, 4],原始列表已改变
    • 在这种情况下,行为类似于按引用传递,因为可以修改原始对象。

然而,需要注意的是,如果函数内重新赋值参数(而不是修改对象内容),这不会影响原始变量:

1
2
3
4
5
6
def reassign_list(lst):
lst = [5, 6, 7] # 重新绑定 lst 到新列表,原始对象不变

y = [1, 2, 3]
reassign_list(y)
print(y) # 输出 [1, 2, 3],原始列表未变

这表明,参数的重新赋值只影响函数内的局部命名空间,不会改变调用者的绑定。

如何理解“传对象”

“传对象”意味着函数接收的是对象的引用,而不是对象本身的副本。以下是关键点:

  • Python 中的变量是对象的引用,传递参数时,函数的参数绑定到与调用者相同的对象。
  • 函数可以通过这个引用访问对象的内容,并根据对象的可变性决定是否能修改它。
  • 如果对象是可变的,函数可以修改其状态,影响原始对象;如果对象是不可变的,函数只能创建新对象,原始对象不受影响。

为了更直观地理解,可以将变量想象为指向对象的标签(label)。传递参数时,函数得到的是同一个标签的副本,但标签指向的对象是共享的:

  • 对于可变对象,修改对象内容相当于在同一个对象上操作,所有标签都会看到变化。
  • 对于不可变对象,试图修改会创建新对象,函数内的标签指向新对象,而原始标签仍指向旧对象。

按值、引用、对象传递对比与总结

以下表格对比了按值传递、按引用传递与 Python 传参的差异:

机制 描述 Python 示例 影响原始变量
按值传递(Pass by Value) 函数接收参数的副本,修改不影响原变量 num = 10,函数内赋值新值
按引用传递(Pass by Reference) 函数接收变量的引用,修改影响原变量 C++ 中的指针或引用传递
Python 按对象引用传递 函数接收对象的引用,可变对象可修改 列表追加元素,字典修改键值 是(可变对象,修改内容)

Python 的传参机制结合了按值和按引用的特性,具体行为取决于对象的可变性。这种灵活性适合大多数场景,但需要开发者理解对象模型以避免误用。

思考题

1
2
3
def bad_append(new_item, a_list=[]):
a_list.append(new_item)
return a_list

执行两次 print bad_append('one') 的结果是什么?

1
2
3
def bad_append(new_item, a_list=None):
a_list.append(new_item)
return a_list

执行两次 print bad_append('one') 的结果是什么?

参考