Last Updated on

参数通过赋值传递。其背后的理由是双重的:

  1. 传入的参数实际上是对象的引用(但引用是按值传递的)
  2. 有些数据类型是可变的,但有些不是

所以:

  • 如果将可变对象传递给方法,则该方法将获得对该对象的引用,并且可以对其进行突变,但是如果您将该引用重新绑定到该方法中,则外部作用域对此一无所知完成后,外部参考仍将指向原始对象。
  • 如果将不可变对象传递给方法,则仍然无法重新绑定外部引用,甚至无法使对象发生突变。

为了更加清楚,让我们看一下下面的例子:

a = 'old'
def fun(a):
    print("1:", a, id(a))
    a = 'new'
    print("2:", a, id(a))
print("before:", a, id(a))
fun(a)
print("after:", a, id(a))

# 输出结果:
before: old 4403148984
1: old 4403148984
2: new 4403148928
after: old 4403148984

可以发现,在函数中,当传入的a为字符串时,在函数中的变化,对外部的变量不影响的。在执行完a = 'new' 之后,a引用中保存的值,即内存地址发生变化,由原来 'old' 对象的所在的地址变成了 ' new' 这个实体对象的内存地址。

a = ['old']
def fun(a):
    print("1:", a, id(a))
    a.append('new')
    print("2:", a, id(a))
print("before:", a, id(a))
fun(a)
print("after:", a, id(a))

# 输出结果:
before: ['old'] 4346265992
1: ['old'] 4346265992
2: ['old', 'new'] 4346265992
after: ['old', 'new'] 4346265992

但当出入的参数为列表时,函数中对其的修改,会改变外部变量。a引用的内存值也没有发生改变。

我们再看一个例子:

a = ['old']
def fun(a):
    print("1:", a, id(a))
    a = ['new']
    print("2:", a, id(a))
print("before:", a, id(a))
fun(a)
print("after:", a, id(a))


# 输出结果:
before: ['old'] 4463616392
1: ['old'] 4463616392
2: ['new'] 4463616264
after: ['old'] 4463616392

当我们使用可变对象列表的时候,对象的引用是传递到了函数中,但是当我们在方法中重新将该引用绑定到新的值,则外部作用域对此依然一无所知,外部参考仍将指向原始对象。

所以,现在好理解了吧,函数穿参时会将参数的引用传递到函数中,但是如果在函数中将参数重新绑定新的引用,此不会影响到外部作用域,但是如果只是对传入的参数进行修改突变,这需要传入的参数为可变参数,那么参数的修改将会影响到外部作用域。

在python中,strings, tuples, 和numbers是不可更改的对象,而 list, dict, set 等则是可以修改的对象。

那么在实际写代码中,就需要注意函数的参数传递与操作是否会影响到外部作用域,导致一些不知名的错误,那么我们一般怎么解决这个问题呢:

1,在操作不可变对象时,使用return返回新值

def return_a_whole_new_string(the_string):
    new_string = something_to_do_with_the_old_string(the_string)
    return new_string

# then you could call it like
my_string = return_a_whole_new_string(my_string)

2,在操作可变对象时,若不想影响到外部作用域时,在函数中可以使用深copy复制一个新的内存地址的对象,再对此对象进行操作修改,以保证外部作用域的对象不会发生改变。

import copy
a = ['old']
def change_list(a):
    b = copy.deepcopy(a)
    b.append('new')
    return b

至于深copy,浅copy等的区别与理解,详情请看我的另一篇博文:

Python 深浅拷贝