[要点抢先看]
1. 函数参数传递的实现过程
2. 可变对象和不可变对象参数传递, 修改的区别
3. 如何避免不可变对象参数传递的本地修改
今天我们再来说说函数中的参数传递问题
[妹子说] 这个看上去自然而然的过程里有什么讲究么?
有很多需要注意的地方, 从这一节开始就来仔细的聊聊.
参数的传递是通过自动将对象赋值给本地变量名来实现的. 在函数运行时, 函数头部的参数名是一个新的, 本地的变量名, 这个变量名是在函数的本地作用域内存在. 参数的传递本质上就是 python 赋值的另一个实例而已.
那么, 这个问题分为可变对象和不可变对象两种情况进行讨论:
在原处改变函数的可变对象参数的值会对调用者有影响. 函数能够就地改变传入的可变对象, 因此其结果会影响调用者, 这其实和前面介绍过的对象赋值原理是一样的;
而不可变对象的引用重新赋值会指向新的对象, 因此不会改变调用者.
先看一个不可变对象的例子
- def f(a):
- a = 99
- print(a)
- b = 88
- f(b)
- print(b)
- 99
- 88
复制代码
在函数中修改 a 对于调用函数的地方没有任何影响, 因为他在函数内部直接把本地变量 a 重置为了一个完全不同的新对象, 所以他不会影响最初的变量 b
而当参数传递像列表和字典这样的可修改对象的时候, 我们还需要注意, 对这样的可变对象的原处修改可能在函数退出后依然有效, 并由此影响到调用者.
- def change(a,b):
- a = 2
- b[0] = 'spam'
- x = 1
- l = [1,2]
- change(x,l)
- print(x,l)
- 1 ['spam', 2]
复制代码
再次对比可以看出, 调用者的不可变变量 x 没有受到影响, 而可变变量 L 在函数内部进行本地修改, 并影响到了自身
可以看出, 对于参数 a, 仅仅把本地变量 a 修改为引用一个完全不同的对象, 并没有改变调用者作用域中的名称 x 的绑定. 而参数 b 被传给了一个可变对象 (在调用者作用域中叫做 L 的列表), 因为第二个赋值是一个在原处发生的对象改变, 对函数中 b[0] 进行赋值的结果会在函数返回后影响 L 的值, 他修改了 b 所引用的对象的一部分, 因为引用共享对象的缘故, L 也被同时改变.
再强调一次, 其实参数传递后的本地修改过程和简单对象赋值后的对象修改, 实质上是一回事, 换句话说就等于下面这个例子所描述的程序过程
- L = [1,2]
- b = L
- b[0] = 'spam'
- print(L)
- ['spam', 2]
复制代码
[妹子说] 那如何避免对可变参数的修改呢?
实际上, 可变参数的原处修改行为并不是一个 bug, 它只是参数传递在 python 中工作的方式. 在 python 中, 默认通过引用进行函数的参数传递, 是因为这通常是我们所想要的: 这意味着不需要创建多个拷贝就可以在我们的程序中传递很大的对象. 如果不想要函数内部在原处的修改影响传递给它的对象, 那么, 我们可以简单的创建一个明确的可变对象的拷贝
- def change(a,b):
- a = 2
- b[0] = 'spam'
- x = 1
- l = [1,2]
- change(x,l[:])
- print(x,l)
- 1 [1, 2]
复制代码
或者在函数内部进行拷贝, 这样可以不改变传入的对象, 函数调用看上去没有变化
- def change(a,b):
- b = b[:]
- a = 2
- b[0] = 'spam'
- x = 1
- l = [1,2]
- change(x,l)
- print(x,l)
- 1 [1, 2]
复制代码
今天就说这么多, 看上去不是很复杂, 不过在程序中可是经常会碰到的, 可不能掉以轻心.
来源: https://juejin.im/post/5b75740ce51d45664153b228