开张名义: Python 函数参数采用基于对象的引用传递.
在 python
的世界上,所有的数据,包括函数/类全都是对象.在函数传递参数时,只是让两个变量标识相同的对象.
那么以上是什么意思?
一.Python 的赋值机制
x = 666
y = x
x = x + 1
执行以上三句,在 python 中意味着什么, 实际发生了什么?
-
x = 666
会在内存中创建一个整型对象, 然后变量x
相当于一个标签,贴在此对象上. -
y = x
将y
也作为这个整型对象的标签,而不是重新在内存中创建一个对象. -
x = x+1
将x
的值加1
,然后在内存中创建另一个整型对象667
,将x
贴在这个对象上,而不是原来的666
上.
下文中,我会用贴向这个词表示一个变量与其对应的对象的关系.
在python中如何表示一个对象与其他对象不同呢? 不能使用==,而是要使用is操作符.
x = 666
y = x
x is y
# True
is 比较的是两个对象的内存地址,因为x和y都是贴向相同,所以判断为True.那么为什么不能用==呢,==只是比较对象的值,不能判断两个变量贴向的对象是否相同.
接下来引申一下,由于 python
的赋值机制所带来的问题
- 当我们使用 del 删除变量时,我们到底做了什么?
一句话 : 删除贴在对象上的标签,而不是真的删除内存中的对象.
那么我们就无法删除内存中的对象,然后节省内存吗?
可以,但是需要通过 python
的垃圾回收机制.简单来说,就是贴在对象上的标签数量为 0
时,会被 python
自动回收.
如果想要了解更多细节,可以看看我的 这篇文章
2. 当我们将多个变量贴在同一个对象上,其中一个变量改变其对应的对象,其他变量所对应的对象还是刚才的那个对象吗?
一句话 : 只要不是显式的修改变量的贴向(哈哈,临时造的词),变量都不会改变对应的对象,但是对象本身可能会发生一些改变.
首先做一些基础操作
x = [1,2,3]
y = x
我们可视化一下结果
- 如果是显示的改变其中一个变量的贴向,对其他变量没影响
x = [1,2,3]
y = x
x = x + [1]
实际上我们新创建了一个列表对象为[1,2,3,4]
,并且将 x
贴在它上. 当然,这一步的操作对于 y
是没有影响的. y
还是指向 [1,2,3]
.
我们可视化这一结果
- 对象本身发生一些改变,此操作只限于 python 中的可变对象
x = [1,2,3]
y = x
x.append(5)
因为 x 是可变对象,所以第三句执行之后,列表会变为 [1,2,3,4]
.又因为 y
和 x
都是贴在同一个列表对象上,所以 y
还是贴在原来的对象上,只不过这个对象本身发生了改变.
我们可视化这一结果
3.小整数池问题
首先来看一个比较容易搞混的问题
# 第一组
x = 666
y = x
# 第二组
a = 456
b = 456
注意第一组和第二组的区别
- 第一组 : 仅仅在内存中创建一个整型对象
666
,x
和y
都是这个整型对象的标签 - 第二组 : 在内存中创建两个整型对象
456
,分别用a
和b
作为标签
重点来了, python
为了提高缓存效率,内部有一个小整数池.什么意思?
m = 3
n = 3
对于m
和n
这样小的且常用的整型值,python
在内存中并不是创建两个对象,而是一个对象.此时m
和n
都是整型对象3
的标签
那么这个小整数池的范围是多少呢?
-5 ~ 256
如何验证?
a = 333
b = 333
a is b
# False
a = 1
b = 1
a is b
# True
类似小整数值,字符串也有自己的缓存机制。
a = 'hello'
b = 'hello'
a is b
# True
4.浅拷贝和深拷贝
先进行解释,然后用例子说明.注意拷贝不等用于创建一个原对象的标签.
浅拷贝
:浅拷贝中的元素,是对原对象中子对象的引用.此时如果原对象中某一子对象是可变的,改变后会影响拷贝后的对象,存在副作用.一不小心就会触发很大的问题.
深拷贝
:深拷贝则会递归的拷贝原对象中的每一个子对象,拷贝之后的对象与原对象没有关系.
接下来我们用例子解释
如何进行浅拷贝
- 集合自己的工厂函数,比如
list
-
copy
模块copy
函数
接下来举个例子
import copy
l = [[1,2],[3,4]]
lc = copy.copy(l)
# lc = list(l)
我们接着运行一个操作
l[-1].append(5)
可视化一下结果
所以最后l
和lc
都是贴向 [[1,2],[3,4,5]]
,lc
躺枪了,它贴向的对象本身改变了.
如何进行深拷贝
-
copy
模块的deepcopy
函数
import copy
l = [[1,2],[3,4]]
lc = copy.deepcopy(l)
l[-1].append(5)
用深拷贝运行以上代码,可视化结果为
所以,深拷贝之后,原对象与拷贝没有任何关系.
二.Python 的参数传递机制
到了这一步已经很明了, 对应开头的阐释.只是函数的参数变量与传递进来的实参变量贴向的对象是同一个对象.
以后所有的操作都是以此为基础的, 结果也应该是不言自明的.
为了加深印象和理解,我们举一个例子
def func(d):
d['a'] = 10
d['b'] = 20
d = {'a': 1, 'b': 2}
d = {} # 1
func(d) # 2
print(d)
########打印结果########
{'a': 10, 'b': 20}
想一想, 最后的结果为什么还是{'a': 10, 'b': 20}
?
我们思考一下执行过程.
- 首先在全局创建一个空字典,并将
d
贴上
- 将
d
传入到函数func
中,在函数中局部的形参变量也为d
,它同样贴在空字典对象上
- 在函数中前两句,为字典赋值.因为字典是可变的,这一操作对全局的
d
也会产生同样的影响.因为此时全局的d
与函数内部的d
贴向的是同一个对象
- 函数最后一句,本质上是将函数内部的
d
贴向另外一个字典对象,全局的d
当然还是贴向原来的字典对象.
- 函数结束,函数内部的
d
被回收,而且最后打印结果如下所示
三.参考资料
<流畅的python>
<极客时间-python核心技术与实战>