python中变量在内存中的存储与地址关系解析、浅度/深度copy、值传递、引用传递

python中变量在内存中的存储与地址关系解析

1.变量、地址

变量的实现方式有:引用语义、值语义

python语言中变量的实现方式就是引用语义,在变量里面保存的是值(对象)的引用(值所在处内存空间的地址)。采用这种方式,变量所需的存储空间大小一致,因为其中只需要保存一个引用。而有些语言(例如c)采用的不是这种方式,它们把变量直接保存在变量的存储区里,这种方式就称为值语义。这样的话,一个整数类型的变量就需要保存一个整数所需要的空间(例如c语言中int类型占用4个字节大小,所能表示的数的最大值为2^32,2147483647)。

python中变量与对象的引用关系类似于c语言的指针变量与指向地址之间的关系。

在python的数据结构中,对象分为可变对象和不可变对象。基本数据类型如int、float,元祖tuple、str是不可变对象;list(列表)、dict(字典)、set(集合)是可变对象,可变对象存储的元素的引用其实是没有改变的,改变的是其引用指向的值。

采用引用语义存储的只是一个变量的值所在的内存地址,而不是这个变量的值本身。
python中变量在内存中的存储与地址关系解析、浅度/深度copy、值传递、引用传递_第1张图片
如上所示,变量中存储的是值的引用,也就是指所在内存空间的地址。

2、id函数
id函数(python的内置函数,用来查看对象的身份,也就是内存地址)

num_ = 2
string_ = "ABCDE"
list_ = [1, 2, 3, 4]
print(id(num_))
print(id(string_))
print(id(list_))

运行结果

269640896
16424928
2700792

对于给变量赋值时,每一次的赋值都会产生一个新的地址空间,将新内容的地址赋值给变量。如果是不可变对象(如数字),赋相同的值,地址不发生变化;但是对于可变对象(如列表),赋相同的值,地址发生变化。
  原因是因为python中相同的值的不同对象,相当于内存中对于相同值的对象保存了对份。但是对于不可变数据类型,内存中只能有一个相同值的对象。同时要看是否产生新的对象。下面看代码吧:

例一:

num_ = 2#把对象2的地址赋值给变量num_来存储
print(id(num_))
num_ = 3#把对象3的地址赋值给变量num_来存储,即num_中存的是新对象的地址,所以地址变了
print(id(num_))

运行结果

260138176
260138192

例二:

string_1 = "ABCDEF"  #把不可变对象"ABCDEF"字符串的地址赋值给变量string_1来存储
print(id(string_1))
string_2 = "ABCDEF" #又把不可变对象"ABCDEF"字符串的地址赋值给变量string_1来存储。
print(id(string_1))

#如上代码:因为是同一个对象"ABCDEF"字符串,所以string_1和string_2的地址相同

运行结果

36756992
36756992

例三:

list_1 = [1,3,5,'a'] #把可变对象[1,3,5,a]序列的地址赋值给变量list_1来存储
print(id(list_1))
list_2 = [1,3,5,'a'] #把另一个可变对象[1,3,5,a]序列的地址赋值给变量list_2来存储
print(id(list_2))
#如上代码:因为[1,3,5,a]和[1,3,5,a]是两个不同对象,虽然他们的值的一样的,所以list_1和list_2的地址不相同


#再如下:list_1[0]和list_2[0]分别示列表list_1和list_2的第一个元素,即数字1,它都是不可变对象,所以地址是相同的,都是对象1的地址
print("list_1[0]  addr:%d" % id(list_1[0]))
print("list_2[0]  addr:%d" % id(list_2[0]))
print("1  addr:%d" % id(1))

运行结果


4404728
4405888
list_1[0]  addr:261317808
list_2[0]  addr:261317808
1  addr:261317808

3、可变对象发生改变

对复杂的数据类型(列表、集合、字典),如果添加某一项元素,或者添加几个元素,不会改变其本身的地址,只会改变其内部元素的地址引用,但是如果对其重新赋值时,就会重新赋予地址覆盖就地址,这时地址就会发生改变。

list_ = [1,2,3,4]
print(list_, id(list_))
list_.append(5)
print(list_, id(list_))
#如上代码,因为append前后的list_仍然是同一个对象,只是对象的值发了改变,所以地址不变。

#再如下面的代码
print(list_, id(list_), id(list_[1]))#打印列表、列表的地址、第二个元素的地址
list_[1] = 'aaa'   #修改列表
print(list_, id(list_), id(list_[1]))#打印列表、列表的地址、第二个元素的地址
#不难发发现:列表变了、列表的地址没有变、列表内部元素变了、列表内部元素的地址变了

运行结果

[1, 2, 3, 4] 2700792
[1, 2, 3, 4, 5] 2700792
[1, 2, 3, 4, 5] 2700792 259941568
[1, 'aaa', 3, 4, 5] 2700792 5824096

4、浅拷贝和深拷贝

首先需要import copy
copy.copy():浅拷贝,不管多复杂的数据结构,浅拷贝都只会copy一层。

copy.deepcopy():深拷贝,会完全复制原变量相关的所有数据,到最后一层(自身包含的所有子列表)。在内存之中生成一套完全一样的内容。

import copy
list_ = [1,2,3]

list_1 = ['a', 'b', 'c', list_]
list_2 = copy.copy(list_1)#浅拷贝
list_3 = copy.deepcopy(list_1)#深拷贝
print(list_1, list_2, list_3)#通过打印发现拷贝后各列表内部值相同
print(id(list_1), id(list_2), id(list_3))#通过打印发现,3个列表的地址不同,说明通过拷贝时生成新的对象
print(id(list_1[3]), id(list_2[3]), id(list_3[3]))#通过打印发现,浅拷贝对于二层可变对象不拷贝,仍然是使用源二层可变对象,即list_2的第三个元素和list_和list_1的list_是同一个对象都是list_ 。
#但是深度拷贝,则是分配新地址空间,生成新的对象

list_[1] = 'OK'
print(list_1, list_2, list_3)#因为是同一个对象,所以修改了list_[1], list_1和list_2都受影响

运行结果

['a', 'b', 'c', [1, 2, 3]] ['a', 'b', 'c', [1, 2, 3]] ['a', 'b', 'c', [1, 2, 3]]
7706864 7707744 7706984
7613336 7613336 7832560
['a', 'b', 'c', [1, 'OK', 3]] ['a', 'b', 'c', [1, 'OK', 3]] ['a', 'b', 'c', [1, 2, 3]]

可以看出,浅拷贝,只拷贝一层,因此当list_重新赋值以后,浅拷贝后list_2包含的子列表发生了变化,而深拷贝以后的列表list_3所包含的字列表并没有发生改变。深拷贝等于完全复制并重新开辟新的内存空间,和原列表两者之间互不影响。

总结:

  • 直接赋值:其实就是对象的引用(别名)。
  • 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。
  • 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。

1、b = a: 赋值引用,a 和 b 都指向同一个对象。
python中变量在内存中的存储与地址关系解析、浅度/深度copy、值传递、引用传递_第2张图片
2、b = a.copy(): 浅拷贝, a 和 b 是一个独立的对象,但他们的子对象还是指向统一对象(是引用)。
python中变量在内存中的存储与地址关系解析、浅度/深度copy、值传递、引用传递_第3张图片
b = copy.deepcopy(a): 深度拷贝, a 和 b 完全拷贝了父对象及其子对象,两者是完全独立的。
python中变量在内存中的存储与地址关系解析、浅度/深度copy、值传递、引用传递_第4张图片

5.值传递与引用传递:
可变对象为引用传递(函数传地址),不可变对象为值传递(函数传值)。

值传递:被调函数在执行时,首先对收到的参数对象生成一个副本,在执行过程中,是对参数副本的操作,并不会对原参数产生改变。也就是在堆栈中开辟内存空间存放由主调函数传进来的实参对应的副本值。特点:函数对收到的参数的任何操作,不会对原参数(实参变量)产生影响。

def add_fun(num):
    num +=1
    print(num, id(num))
    return


num = 3
add_fun(num)
print(num, id(num))

运行结果

4 268788960
3 268788944

引用传递:当传递列表或者字典时,如果改变引用的值,就改变了原始对象。(引用传递直接传的是地址,是对原始对象的直接操作。)

def list_append_fun(list_t):
    list_t.append("abc")
    print(list_t, id(list_t))
    return


list_t = [1,2,3]
list_append_fun(list_t)
print(list_t, id(list_t))

运行结果

[1, 2, 3, 'abc'] 31208952
[1, 2, 3, 'abc'] 31208952

由上面程序可以看出,引用传递,函数修改的直接是实参的值。但是,在函数体中不能直接修改整个列表或者字典的值,这样做,也等于创建实参的副本,并不会对实参本身产生影响。如下程序所示。

def list_append_fun(list_t):
    list_t = ['a', 'b', 'c']
    print(list_t, id(list_t))
    return


list_t = [1,2,3]
list_append_fun(list_t)
print(list_t, id(list_t))

运行结果

['a', 'b', 'c'] 6175360
[1, 2, 3] 6174200

你可能感兴趣的:(Python)