4.15 向函数传递参数:深入讨论
现在,我们来仔细思考一下如何将参数传递给函数。在许多编程语言中,有两种方法可以传递参数—按值传递和按引用传递(有时分别称为按值调用和按引用调用):
- 按值传递时,被调用的函数会建立一个副本接收参数的值,并在函数中使用该副本。对副本的更改不会影响调用者中原始变量的值。
- 按引用传递时,被调用的函数可以直接访问调用者中的参数,如果这个值可变,则可以修改该值。
在Python中,参数始终通过引用传递,也可以称之为按对象的引用传递,因为“Python中的所有内容都是对象。”[1]当函数调用提供参数时,Python会将参数对象的引用(而不是对象本身)复制到相应的参数中,这样做会对性能的提升产生积极的作用。因为函数会经常操作大型的对象,复制对象本身而不是对象的引用会消耗大量的计算机内存,这样会显著降低程序的性能。
内存地址、引用和指针
当通过引用与对象进行交互时,事实上在后台使用的是对象在计算机内存中的地址(或位置),在某些语言中被称为“指针”。完成如下的赋值之后
变量x
中实际上并不包含7,而是包含一个对含有7
的对象的引用,这个对象存储在内存中的某个位置。可以说x
“指向”(即引用)包含7
的对象,如下图所示:
内置函数id
和对象标识
现在,我们来看一下如何将参数传递给函数。首先,创建上面提到的整数变量x
,后面会将x
作为函数的参数:
现在x
引用(或“指向”)了包含7
的整数对象。两个独立的对象不能驻留在内存中的同一个地址中,因此内存中的每个对象都有一个唯一的地址。虽然我们看不到对象的地址,但可以使用内置id
函数来获取属于这个对象的唯一int
值,这个值仅在该对象驻留在内存中时才识别该对象(当你在自己的计算机中运行该程序时,可能会得到与本例不同的值):
函数id
返回的整数结果称为对象的标识[2]。内存中的两个对象不能具有相同的标识。接下来,我们使用对象标识来说明对象是通过引用传递的。
将对象传递给函数
下面的代码定义了函数cube
,可以显示其参数的标识并返回参数值的立方值:
接下来,使用x
作为参数调用cube
,x
引用了包含7
的整数对象:
结果显示cube
的参数number
的标识为4350477840
,与之前显示的x
的标识相同。这说明当执行函数cube
时,参数x
和参数number
引用的是同一个对象,因为每个对象都具有自己唯一的标识。因此,当函数cube
在其计算中使用参数number
时,它会从调用者中的原始对象获取number
的值。
使用is
运算符测试对象标识
也可以使用is
运算符证明两个参数引用的是同一个对象。如果is
运算符的两个操作数具有相同的标识,则返回True
,如下:
不可变对象作为参数
当函数接收对不可变(不可修改)对象(如int
、float
、string
或tuple
)的引用作为参数时,即使可以直接访问调用者中的原始对象,也无法修改原始对象的值。为了证明这一点,我们在cube
函数中使用增强赋值语句将新对象赋值给参数number
,并显示赋值前后的id(number)
:
当调用cube(x)
时,第一个print
语句显示的最初的id(number)
与代码段[2]
中的id(x)
相同。这说明数值是不可变的,因此声明
实际上会创建一个包含立方值的新对象,然后将该对象的引用赋值给参数number
。根据前面讲过的内容可知,如果没有对原始对象的更多引用,它将被当作垃圾回收。函数cube
的第二个print
语句显示新对象的标识。对象的标识必须是唯一的,因此number
一定引用了不同的对象。为了说明x
未被修改,我们再次显示它的值和id
,如下:
可变对象作为参数
下一章将会介绍将对像列表这样的可变对象的引用作为参数传递给函数时,函数可以修改调用者中的原始对象。
[1] 甚至在本章中定义的函数和在后面几章中定义的类(自定义类型)也都是Python中的对象。
[2] Python文档指出,根据使用的Python工具的不同,对象的标识也可能是对象实际的内存地址。