刚刚发现 Python 的参数传递机制很有意思,值得专门写个博客记录一下。
在 C/C++ 中,有两种参数传递方式:值传递 (pass by value) 和引用传递 (pass by reference)。值传递:传入函数的实参被复制给该函数的形参。形参是局部变量,函数运行完成后,这些局部变量的内存被释放,不会影响函数外部实参的值。
引用传递:将实参的地址传入函数中,可以在函数内部直接通过这个地址修改实参。
由于值传递会复制传入的参数,因此形参地址和实参地址是不同的,例如:
#include
int f(int a){
printf("In f: &a=%p\n", &a);
}
int main(){
int a = 3;
printf("In main: &a=%p\n", &a);
f(a);
return 0;
}
// 运行结果// In main: &a=0x7ffefede5954// In f: &a=0x7ffefede593c
但在 Python 中,参数传递的规则却不太一样:
def f(a):
print("In f: id(a)={}".format(id(a)))
def main():
a = 3
print("In main: id(a)={}".format(id(a)))
f(a)
if __name__=='__main__':
main()
# 运行结果
# In main: id(a)=94285635591488
# In f: id(a)=94285635591488
f 中的id(a)竟然与 main 中的id(a)是一样的!那在 f 中改变 a 的值,岂不是会影响到 main 中 a 的值吗?(并不会)。
我们进一步发现:当 a 在 f 中没有被改变的时候,f 中的 id(a) 与 main 中的id(a) 相同,而一旦 f 内部改变了 a 的值,这两个 id 就不同了:
def f(a):
print("In f before change: id(a)={}".format(id(a)))
a += 1
print("In f after change: id(a)={}".format(id(a)))
def main():
a = 3
print("In main before call f: id(a)={}".format(id(a)))
f(a)
print("In main after call f: id(a)={}".format(id(a)))
if __name__=='__main__':
main()
# 结果
# In main before call f: id(a)=94870107749696
# In f before change: id(a)=94870107749696
# In f after change: id(a)=94870107749728
# In main after call f: id(a)=94870107749696
这一规则,适用于 python 中的所有不可变对象(immutable objects),例如 int, float, bool, string, unicode, tuple 等等一旦被创建,就无法(在原地址上)被修改的对象:
In [1]: a = "hello world"
In [2]: a[0] = "H"
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in
----> 1 a[0] = "H"
TypeError: 'str' object does not support item assignment
而对于 python 中的可变对象(mutable objects),例如 list, dict, set 以及用户自定义的类,等可以修改其状态的对象,传递规则又有不同:
def f(a):
print("In f before change: id(a)={}".format(id(a)))
a.append(1)
print("In f after change: id(a)={}".format(id(a)))
def main():
a = [1, 2, 3]
print("In main before call f: id(a)={}".format(id(a)))
f(a)
print("In main after call f: id(a)={}".format(id(a)))
if __name__=='__main__':
main()
# 结果
# In main before call f: id(a)=140263104163584
# In f before change: id(a)=140263104163584
# In f after change: id(a)=140263104163584
# In main after call f: id(a)=140263104163584
我们发现,修改了 list 后,它的 id 并不会改变。类似 C/C++ 中的引用传递,此时在 f 中对 a 做的改变,将影响到 main 中的 a。但这和引用传递也有明显区别,例如在 f 中为 a 赋值一个新的 list,main 中的 a 是不会受到影响的:
def f(a):
a = [4, 5]
print("In f: a={}".format(a))
def main():
a = [1, 2, 3]
f(a)
print("In main: a={}".format(a))
if __name__=='__main__':
main()
# 结果
# In f: a=[4, 5]
# In main: a=[1, 2, 3]
原因是,f 中的 a=[4, 5] 相当于把局部变量 a 绑定了一个新的 list 对象,这个 a 所指向的内存,已经不是 main 中的 a 所指向的那块内存了。由于 list 是可变对象, a.append(5) 并不会开辟一块新的内存,而是会在原内存位置的对象上增加一个元素。
综上,不能简单地将“值传递”或者“引用传递”的概念套用到 Python 中,Python 中的参数传递机制,更类似于“内存传递”。当这块内存存储的是可变对象时,可以直接在它上面做修改;当这块内存存储的是不可变对象时,修改某个变量,本质上并没有修改这块内存的数据,而是开辟了一块新的内存,将新内存赋值给该变量,而旧内存则被垃圾回收了。