今天师弟问了一个关于 Python 函数参数的一个问题:
1 2 3 4 5 6 7 8 |
|
为啥第一个函数会把 l 每次调用完的值保留下来?
起初我认为问的是这两个函数使用的时候,为何会保持对传入的参数 l 的修改。从这个方面来讲,是因为 Python 对于数据赋值的处理的原因。
在 Python 中,赋值是传引用的。一个列表,比如 [1, 2, 3],或者一个字符串,’tonychow’,这些对象在创建的时候会在内存中分配一段空间。如果将这些对象赋值给一个变量名,那就会导致在 Python 的命名空间中该变量名指向内存中这个对象。对该变量名的操作就是对内存中这个对象的操作。所以如果尝试直接将一个变量 a 赋值给另外一个变量 b ,导致的后果是,命名空间中,这两个变量名 a 和 b 指向内存中同样一个对象,也就是所谓传引用赋值。对其中任意一个变量的操作,实质是对该对象进行操作,所以同样的操作后结果也会可以在另外一个变量中看到。如下:
1 2 3 4 5 6 7 8 9 |
|
从上面的代码可以看到,在将 a 赋值给 b 之后,对 a 列表调用 pop 方法,导致的是 b 列表也发生了变化。我们还可以通过 Python 内置的 globals 函数和 id 函数来加深这个理解。globals 函数将会返回一个字典,这个字典是当前的全局符号表。而 id 函数则会返回一个对象的标识,可以将其返回值看作是这个对象在内存中的地址。
1 2 3 4 5 6 7 8 9 10 |
|
可以看到,a 和 b 都在当前的全局字符表中,他们的值也都是一致的。此外,id 函数的结果明确地说明了 a 和 b 这两个变量名都是指向了内存中的同一个对象。而在 Python 中,调用函数的时候,传入参数,也是进行传引用的赋值。所以我师弟说的这两个函数都会保留对于传入参数的修改,也就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
题外话,在 Python 内置的数据类型中,有两种不同的数据类型。一种是可变类型(mutable),比如 list , dict 等;另外一种就是不可变类型(immutable),比如字符串或者 tuple。
可是后来师弟贴出了另外一段代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
这下我明白了,师弟说的不是我想到的那个问题,而是命名参数的问题。
说实话这个问题一开始我也没有想到答案。大家在学习 Python 的时候,无论看的是哪本入门书,应该在开始的时候都会看到一句话“ Python 中一切都是对象”。看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
对的,数字1是一个对象,字符串 ‘test’ 也是一个对象,甚至一个函数也是一个类型为 function 的对象,也有一堆的属性和方法。对于 function 对象而言,有一个特殊属性 defaults ,这个属性用一个元组保存了是这个 function 对象的命名参数的缺省值,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
如果一个函数有命名参数,则按顺序保存了命名参数的缺省值。如果这个函数命名参数没有缺省值或者没有命名参数,则为 None 。回到问题,为什么第一个函数中指定缺省值为 [] 会导致随着执行过程中,缺省参数的值会被保留下来呢?代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
其实通过上面的罗嗦一大堆,答案很容易就可以得到了:foo 是一个 function 类型的对象,这个对象中有个 defaults 属性,保存了命名参数 l 的值,而在一次次的调用过程中,因为没有传入参数,所以实际上 foo 函数改变的是命名参数的缺省值。也就是师弟所说的这个函数在一次次调用中保留了对命名参数l的结果的修改。而师弟贴出的第二个函数的命名参数缺省值是 None ,实质上就是没有缺省值,所以l的值修改没有在调用中保留下来。是不是真的这样?我们来看下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
上面这个函数 foo 有一个命名参数 l ,它的命名参数缺省值是一个空的列表,虽然是空列表,可是它确确实实是一个对象,已经在内存给它分配了空间。我们可以通过 id 函数的结果看出来。然后是两次的调用 foo 函数可以看到,因为没有传入参数,所以这两次修改的都是这个缺省的命名参数的值,所以可以得到所谓的对 l 的值的修改保留下来了的感觉。
首先我们应该明白,在 Python 中,一个对象的实例化和初始化是不同的。一个对象实例化调用的是对象的 new 函数,而初始化调用的是 init 函数。所以,要深入地去看在 Python 中,函数在实例化的时候到底发生了什么,我们应该要去看 Python 源码。如下,源码版本为 Python2.7.4。
1 2 3 4 5 6 |
|
Python2.7.4/Include/object.h, L765-L767
1 2 3 |
|
上面第一断代码是 funcobject 的 func_new 中的代码,也就应该是 functions 对象的 new 函数代码。可以看到,如果 defaults 不是 None,也就是说有值,而我们上面也提到 Python 中一切都是对象,所以就会对这个对象进行 Py_INCREF 操作,并且将这个 defaults 值设定为 func_defaults。Py_INCREF 操作是什么?从第二段代码可以看到,这是一个宏定义,将参数 op 的 ob_refcnt 值加一。ob_refcnt 是什么?refcnt——reference count,这样明白了,就是将该对象的引用计数值加一。在执行了函数函数之后,该命名函数的缺省值对象并没有被销毁,而是随着该函数对象的存在而存在。对这个缺省之对象的修改当然也会被保留下来。
-EOF-