Python中赋值、引用、深浅拷贝的区别和联系

一、对象的唯一id

python中的所有对象都有自己的唯一id,id在创建对象时就已经分配给对象,id是对象的内存地址,并且在每次运行程序时都不相同(除了某些具有恒定唯一id的对象,比如-5~256之间的整数)。

id():返回对象的唯一id,适用于python中的任何对象,如变量、字符串、列表、字典、元胞等。

a = 5
b = 'hello'
c = (1, 2, 3)

print('>>> id(a):', id(a))
print('>>> id(b):', id(b))
print('>>> id(c):', id(c))

运行三次的结果:

在这里插入图片描述

从上图可以看出,整数5的id一直不变,但另外两个变量id的每次重新运行程序的结果都不一样。

二、赋值

python中的赋值语句总是建立对象的引用值,而不是简单的复制对象。因此python变量更像是指针,而不是数据存储区域。如下例子:

import numpy as np

a = [1, 2, 3, 4, 5]
def fun(data_in):
    print(data_in, '  >>>id=', id(data_in))

    b = data_in
    print(b, '  >>>id=', id(b))

    b[0] = 99
    print(b, ' >>>id=', id(b))
    print(data_in, ' >>>id=', id(data_in))

    return b


print(a, '  >>>id=', id(a))
b = fun(a)
print(a, ' >>>id=', id(a))
print(b, ' >>>id=', id(b))

运行结果:

在这里插入图片描述

【解释】:在fun()函数中直接用等号赋值语句对b进行操作,本质上是将data_in的地址赋给b,因此主函数中的变量a和fun()函数中的变量b同时指向了同一个地址,因此在fun()函数中对变量b的所有操作都影响到了主函数中的变量a!

在这里插入图片描述

三、可变对象和不可变对象

  • 可变对象包括:列表(list)、集合(set)、字典(dict);

  • 不可变对象:整数(int)、浮点型(float)、字符串(str)、布尔型(bool)、元胞(tuple)。

判断一个对象是否是可变对象,关键是看操作对象前后的内存地址是否发生变化!如果对某个变量进行了操作,但操作前后的内存地址不变,说明这个变量是可变对象;否则这个变量是不可变对象。

可变与不可变的关键是对象内容能否被修改,而不是对象的指向能否被修改!如下例子:

a = 1
print(a, '>>>id=', id(a))
a = 2
print(a, '>>>id=', id(a))

运行结果:

在这里插入图片描述

【解释】:a首先被赋值一个整数,然后再被赋值为另一个整数,到这里很多人会说了“int是可变对象”,那可就大错特错了!注意看可以发现两次输出的a变量的内存地址是不同的,说明第一次给a赋值为一个整数1,a指向了第一个内存地址address1,第二次再给a赋值为另一个整数2,a指向了第二个内存地址address2,但“address1和address2中存放的内容分别是1和2”这个事实是不变的,所以先后两次赋值并不是改变了对象的内容,只是第二次赋值时创建了一个新对象,a指向了这个新对象而已。也就是说,变量a改变的只是指向,而不是内容,所以a是不可变对象。

在这里插入图片描述

四、函数的参数传递

  • 值传递:指在调用函数时将实际参数复制一份传递到函数中,在函数中对参数进行修改不会到影响实际参数;

  • 引用传递:只在调用函数时将实际参数的地址传递到函数中,在函数中对参数进行的修改将影响到实际参数。

python既支持值传递,也支持引用传递。解释器会查看对象引用(即对象的内存地址)指向的变量的类型,如果变量是不可变对象,那么函数参数作为值传递;如果变量是可变对象,那么函数参数作为引用传递。对于值传递的传参方式,函数结束之后主函数中该变量值不发生变化;对于引用传递的传参方式,函数之后主函数中该变量值会发生变化。如下例子:

def fun(p1, p2):
    p1 = 1
    p2.append(2)
    return

a, b = 0, [1]
print(a, b)
fun(a, b)
print(a, b)

运行结果:

【解释】:fun()函数的参数p1的传入为a(是整数),是不可变对象,所以p1是值传递,函数fun()运行结束后主函数中的a不发生变化;参数p2的传入为b(是列表),是可变对象,所以p2是引用传递,p2指向变量b指向的内存地址,所以当p2发生变化时同时会改变变量b的取值,函数fun()运行结束后b发生改变。

不要使用可变对象作为函数默认参数!!!比如,下面例子:

def fun(a, b=[]):
    b.append(a)
    return b

print(fun(0))
print(fun(0))
print(fun(0))

运行结果:

【解释】:因为函数fun()的第二个参数是可变对象,所以并不是每次调用fun()函数时参数b的传入都是空列表。

为了避免此类问题,可以使用如下代码代替:

def fun(a, b=None):
    if b is None:
        b = []
    b.append(a)
    return b

print(fun(0))
print(fun(0))
print(fun(0))

五、深拷贝和浅拷贝

.copy()是浅拷贝,.deepcopy()是深拷贝。

相同点:两个操作都会创建一个新的对象,新对象的id都和原始对象的id不同;

本质区别:拷贝出来的对象的id不同,即内存地址不同。

import copy

a = [1, 2, 3, 4, 5]
b = a  # 直接用等号进行复制,相当于引用
c = a.copy()
d = copy.copy(a)
e = copy.deepcopy(a)

print(a, '>>>id=', id(a))
print(b, '>>>id=', id(b))
print(c, '>>>id=', id(c))
print(d, '>>>id=', id(d))
print(e, '>>>id=', id(e))
print()

print(a[1], '>>>id=', id(a[1]))
print(b[1], '>>>id=', id(b[1]))
print(c[1], '>>>id=', id(c[1]))
print(d[1], '>>>id=', id(d[1]))
print(e[1], '>>>id=', id(e[1]))

运行结果:

import copy

a = [1, {
    
    }, 3, 4, 5]
b = a  # 直接用等号进行复制,相当于引用
c = a.copy()
d = copy.copy(a)
e = copy.deepcopy(a)

print(a, '>>>id=', id(a))
print(b, '>>>id=', id(b))
print(c, '>>>id=', id(c))
print(d, '>>>id=', id(d))
print(e, '>>>id=', id(e))
print()

print(a[1], '>>>id=', id(a[1]))
print(b[1], '>>>id=', id(b[1]))
print(c[1], '>>>id=', id(c[1]))
print(d[1], '>>>id=', id(d[1]))
print(e[1], '>>>id=', id(e[1]))

运行结果:

【解释】:

  1. b是由a直接赋值得来的,所以是引用,b的所有属性都和a完全一致,所以a和b的id一致,且a[1]和b[1]的id一致;

  2. c和d是由a浅拷贝得来,所以c, d和a的id不同,但c, d的子对象和a的子对象的id相同;

  3. e是由a深拷贝得来,所以e和a已经完全没有关系,对象e和对象a的id不同且两个对象的每个可变子对象的id也不同;

  4. 当a[1]=2时,a[1]是不可变对象,所以不管是深拷贝还是浅拷贝,拷贝得来的对象的第一个元素的id都和a[1]相同;

  5. 当a[1]={}时,a[1]是可变对象,所以深拷贝对象e的子对象e[1]的id和a[1]的id不同。

六、举个栗子

6.1 不可变对象的拷贝

import copy

a = (1, 2, 3)
b = a
c = copy.copy(a)
d = copy.deepcopy(a)

print(a, '>>>id(a)=', id(a))
print()
print('-------赋值/引用-------')
print(b, '>>>id(a)=', id(b))
print()
print('-------浅拷贝-------')
print(c, '>>>id(a)=', id(c))
print()
print('-------深拷贝-------')
print(d, '>>>id(a)=', id(d))

运行结果:

由于a是不可变对象,那么赋值、深浅拷贝之后的内存地址都和原对象相同,即使是被重新赋值,也只是新开辟了一块内存并让a对象指向了新赋值元素,并不改变原有地址内的内容。

6.2 可变对象的拷贝

import copy

a = [1, 2, 3]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)

print(a, '>>>id(a)=', id(a))
print()
print('-------赋值/引用-------')
print(b, '>>>id(b)=', id(b))
print()
print('-------浅拷贝-------')
print(c, '>>>id(c)=', id(c))
print()
print('-------深拷贝-------')
print(d, '>>>id(d)=', id(d))

运行结果:

Python任何时候的赋值都相当于引用,所以b和a的id相同;由于a是可变对象,所以深浅拷贝得到的新对象c和d的id和a不同。

6.3 可变对象改变外层元素

import copy

a = [1, 2, [3, 4]]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
a.append(5)

print(a, '>>>id(a)=', id(a))
print()
print('-------赋值/引用-------')
print(b, '>>>id(b)=', id(b))
print()
print('-------浅拷贝-------')
print(c, '>>>id(c)=', id(c))
print()
print('-------深拷贝-------')
print(d, '>>>id(d)=', id(d))

运行结果:

Python任何时候的赋值都相当于引用,所以b和a的元素和id完全相同;由于a是可变对象,因此深浅拷贝之后id都发生变化;由于改变的是a外层元素,而深浅拷贝都拷贝了外层对象,所以改变a的外层元素不影响c和d的id。

6.4 可变对象改变内层元素

import copy

a = [1, 2, [3, 4]]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
a[2].append(5)

print(a, '>>>id(a)=', id(a))
print()
print('-------赋值/引用-------')
print(b, '>>>id(b)=', id(b))
print()
print('-------浅拷贝-------')
print(c, '>>>id(c)=', id(c))
print()
print('-------深拷贝-------')
print(d, '>>>id(d)=', id(d))

运行结果:

Python任何时候的赋值都相当于引用,所以b和a的元素和id完全相同;由于a是可变对象,所以c和d的id和a不相同;由于浅拷贝只拷贝外部对象,对于内部对象只拷贝了元素引用,所以当a的内部对象a[2]发生改变时,c[2]的元素也会对应发生改变。

七、总结

  1. Python中的赋值即引用,进行赋值时不会开辟新的内存空间,也不会产生一个新的变量单独存在,只是在原有数据块上打上了一个新标签。当数据块的任意一个标签发生变化时,本质是这个数据块发生变化,那么指向这个数据块的任意标签都会发生变化。
  2. 浅拷贝常见的形式:切片a=a[:]、工厂函数a=list(a)、copy函数a=a.copy()或a=copy.copy(a)。浅拷贝只拷贝了最外层的对象,子对象只是被拷贝了元素的引用(即对象内的元素没有被拷贝);
  3. 深拷贝只有一种实现形式:a=copy.deepcopy(a)。深拷贝既拷贝了对象,也拷贝了多层的嵌套子元素,深拷贝得到的对象是一个完全全新的对象,和原对象不再有任何关联。

    
    

    (本文完整的pdf请关注“张张学算法”,并回复“015”获取~)

    

猜你喜欢

转载自blog.csdn.net/weixin_40583722/article/details/129412682