关于python参数传递的理解

Posted by FanHao on 2019-02-20

前言

之前和同学聊到了python关键字del与内存回收相关的问题。通过以下内容可以帮助理解

参数

参数有形参和实参之分。形参也就是形式参数,不在内存中占用内存地址,如def定义函数时括号内的变量就是形参。实参全称为实际参数,在调用函数时提供的值或者变量称作为实际参数,占用内存地址。

1
2
3
4
5
6
7
8
9
10
#下面x,y即为形参
def add(x,y):
return x+y

#1,2为实参
add(1,2)
#以下的x,y为实参
x=2
y=3
add(x,y)

参数的传递

首先我们来看一段python代码

1
2
3
4
5
6
7
if __name__=='__main__':  
a=2
b=a
c=a
del a
del b
print(c)

最终程序得到的结果为2。可能有人会感觉很奇怪,明明a已经被del了,c=a还是可以得到1的结果。
所以我们需要明确概念,del作为python的关键字,不同于C free和C++ delete。del语句只作用于变量,而不作用于数据对象。所以del a只是删除了变量a,而不是数据对象。而python有GC机制也就是垃圾回收机制,将a和b del之后,2的引用计数仍然为1,所以不会被清除。只有当引用次数为0后,数据对象内存地址被自动回收。

什么叫引用

在python中,一切皆对象。python中赋值语句a=2可以把a看成是对象2的一个引用。
文章的下面内容,我们将详细分析

python的GC机制是什么

GC机制也就是垃圾回收机制。当对象2的引用计数为0时,将自动被清除。
具体分析如下:

1
2
3
4
5
6
7
if __name__=='__main__':  
a=2 #2的引用次数=1
b=a #2的引用次数+1,此时为2
c=a #2的引用次数+1,此时为3
del a #引用次数-1,此时为2
del b #引用次数-1,此时为1
print(c) #c的值为2

参数的传递和改变

首先我们学习一个函数id(),它是python自带的函数,函数的解释是返回对象的唯一标识,CPython使用的对象的内存地址。其实总结就是返回对象的内存地址。

1
2
3
4
5
6
7
8
9
10
>>> x=8
>>> id(x)
1617486720
>>> id(2)
1617486720
>>> y='hi'
>>> id(y)
35985280
>>> id('hi')
35985280

从以上代码可以看出,id(x)和id(8)的值是一样的,id(y)和id(‘hi’)的值也是一样的。
在Python中一切皆对象。8,’hi’都是对象,8是一个整型对象,而’hi’是一个字符串对象。上面的x=8,在python中实际的处理过程是这样的:先申请一段内存分配给一个整型对象来存储整型值8,然后让变量x去指向这个对象,实际上就是指向这段内存(类似C语言中的指针)。而id(8)和id(x)的结果一样,说明id函数在作用于变量时,其返回的是变量指向的对象的地址。因为变量也是对象,所以在这里可以将x看成是对象8的一个引用。
下面我们在看几个例子。
例一:

1
2
3
4
5
6
7
8
9
10
a=6
b=6
print(id(a))
print(id(b))
print(id(6))
s='hello'
t=s
print(id('hello'))
print(id(s))
print(id(t))

通过执行以上代码可以发现,id(a)、id(b)、id(6)的结果相同。id(‘hello’),id(s)和id(t)的结果也是相同的。说明a和b指向的是同一对象,而t和s也是指向的同一对象。a=6这句让变量a指向了int类型的对象6,而a=6这句执行时,并不重新为6分配地址空间,而是让a直接指向了已经存在的int类型的对象2。这个很好理解,因为本身只是想给a赋一个值2,而在内存中已经存在了这样一个int类型对象2,所以就直接让y指向了已经存在的对象。这样一来不仅能达到目的,还能节约内存空间。t=s这句变量互相赋值,也相当于是让t指向了已经存在的字符串类型的对象’hello’。
例二:

1
2
3
4
5
x=1
y=2
print(id(1),id(x))
x=3
print(id(3),id(x))

执行以上代码后,前后两次执行id(x)得到的值不同。x=1时,申请一段内存存储对象1,再将变量x只想对象1。执行x=3时,同样是申请一段内存存储对象3,再将变量x重新指向对象3,此时对象1的引用次数变为0。并不是获取x指向对象1的内存地址,再将内存中的值改为1.
例三:

1
2
3
4
5
6
7
8
L=[1,2,3]
x=3
M=L
print(id(M),id(L))
L[0]=3
print(M,L)
print(id(M),id(L))
print(id(x),id(L[0]),id(L[2]))

执行结果如下

1
2
3
4
2269912 2269912
[3, 2, 3] [3, 2, 3]
2269912 2269912
1617486640 1617486640 1617486640

在Python中,复杂元素的对象是允许更改的,比如列表、字典、元组等。Python中变量存储的是对象的引用,对于列表,其id()值返回的是列表第一个子元素L[0]本身的存储地址。上面的例子,L=[1,2,3],这里的L有三个子元素L[0],L[1],L[2]。L[0]、L[1]、L[2]分别指向对象1、2、3。M和L是指向同一个对象,所以在L中子元素的值后,M中子元素的值也相应改变了。但是id(L)和id(M)的值没有改变,因为L[0]=3只是让L[0]指向了对象3,并没有改变L[0]自身的存储地址。(id(L)的值实际等于L[0]自身的存储地址)
分析上面代码的执行结果,可以得出与上面分析一样的结论。
画一个简单的示意图帮助我们进行理解
L —–> | L[0] | —–> 1
| L[1] | —–> 2
| L[2] | —–> 3 <—– x