学了这么多年编程,发现在学校都是浑水摸鱼,从来没有精通过一门语言,一个月熟悉python和算法。不积硅步,无以至千里。
本文笔记来自以下博客,请参考原文。
Python:深拷贝与浅拷贝 - 七落安歌 - 博客园
https://zhuanlan.zhihu.com/p/9312146798
【Python基础】变量?对象?引用?赋值?一个例子解释清楚!_python 对象赋值-CSDN博客
对象:被分配的一块内存,存储所代表的值
在 Python 中,所有的数据都是对象,包括基本数据类型(例如整数、浮点数、字符串等)以及用户自定义的类型(类的实例等)。
什么是对象
- 在Python中,对象是指内存中存储的数据结构。
- 它可以是任何数据类型,如数字、字符串、列表、字典等。
- 当在Python中创建一个变量时,实际上是在创建一个指向对象的引用。
- 每个对象都有一个唯一的标识符(ID)和一个类型。
对象的三大特征
- 类型(Type)决定对象可以进行的操作、决定对象的行为和属性
- 标识(Identity)唯一标识符、使用
id()
函数查看、类似内存地址的概念 - 值(Value)对象存储的实际数据、可以是数字、文本、集合等
引用:自动形成的从变量到对象的指针
- 引用是指向某个对象的指针。你可以把它看作是变量与其对应对象之间的联系。
- Python中的变量并不是直接存储数据,而是存储对象的引用。
- 当你给一个变量赋值时,实际发生的是创建一个对对象的引用。
引用允许多个变量指向同一个对象,这样可以在不复制数据的情况下进行操作,节省内存。
变量-对象-引用的关系
- 变量是对对象的引用,多个变量可以指向同一个对象。
- 变量本身并不存储数据,而是指向内存中的某个对象。
- 对象是存储数据的地方,变量通过引用该对象。
- 引用 是指向某个对象的内存地址或位置。对象 是存储在内存中的数据。
- 变量 存储 引用,而 引用 指向对象(在堆内存中)的存储位置(地址)。
- 对象 本身是存储在 堆内存 中的,值 就是对象存储的具体数据。
- 地址 是堆内存中对象的具体位置,每个对象在内存中都有唯一的地址。
- 引用 是指一个变量指向内存中的某个对象。
- 当你创建一个变量并将它赋值给另一个变量时,
- 实际是在创建一个对相同对象的引用,而不是复制对象的内容。
赋值和引用传递
当你将一个对象赋值给另一个变量时,实际上是将对象的引用传递给了新的变量,而不是复制对象本身。这意味着两个变量将指向同一个对象。
a = 1
b = a
a = a + 1
首先将 1
赋值于 a
,即 a
指向了 1
这个对象。
接着 b = a
则表示让变量 b
也同时指向 1
这个对象。Python 的对象可以被多个变量所指向(引用)。
最后执行 a = a + 1,在这里需要注意的是,Python 的基础数据类型(例如整型(int)、字符串(string)等)是不可变的
所以,a = a + 1,并不是让 a 的值增加 1,而是表示重新创建了一个新的值为 2 的对象,并让 a 指向它。但是 b 仍然不变,仍然指向 1 这个对象。
因此最后的结果是,a 的值变成了 2,而 b的值不变仍然是 1。
简单的赋值 b = a,并不表示重新创建了新对象,只是让同一个对象被多个变量指向或引用。
a = [1, 2]
a[1] = a
print(a[1])
这段代码中创建了一个列表 a
,其中包含两个元素(1 和 2),然后 a[1]
被赋值为整个列表 a
(a[1] = a
),当你打印 a[1]
时,它实际上是指向列表 a
本身。
可变对象和不可变对象
-
不可变对象:一旦创建就不可修改的对象(值内存地址固定后不可以再修改其值),包括字符串、元组、数值类型(整型、浮点型)、布尔类型。该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。修改变量值不会影响其他变量。
-
可变对象:可以修改的对象(值内存地址固定后还可以修改其值),包括列表、字典、集合。该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的地址,通俗点说就是原地改变。修改对象时会影响所有引用该对象的变量。
内存管理和引用计数
python使用 引用计数 来管理内存。这意味着每个对象都有一个与之关联的计数器,记录了有多少个变量引用了该对象。引用计数的工作原理:
- 每当你创建一个对象时,Python会自动为这个对象分配内存并将引用计数设置为 1。
- 如果你将这个对象赋值给另一个变量,引用计数会增加,
- 直到没有任何变量再引用这个对象时,引用计数会减为零。
- 此时,Python会自动将该对象删除,释放内存。
Python 内不可变对象的内存管理方式是引用计数。
import copya = "张小鸡"
b = a
c = copy.copy(a)
d = copy.deepcopy(a)print "赋值:id(b)->>>", id(b)
print "浅拷贝:id(c)->>>", id(c)
print "深拷贝:id(d)->>>", id(c)
#三个都相同
因为我们这里操作的是不可变对象,Python 用引用计数的方式管理它们,所以 Python 不会对值相同的不可变对象,申请单独的内存空间。只会记录它的引用次数。
浅拷贝与深拷贝
拷贝指的是创建一个新对象,这个新对象的内容是源对象的副本。拷贝可以分为 浅拷贝 和 深拷贝,不论对于浅拷贝还是深拷贝对可变对象他们都会生成新的引用,对不可变对象不生成新的引用,它们的区别主要体现在对象中是否包含对其他对象的引用。
浅拷贝只复制了对象本身的引用,而不会复制对象内部的子对象。因此,如果对象内部包含可变对象(如列表、字典),这些子对象依然是共享的。
如果对象内有可变元素,修改这些元素会影响到原始对象。
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a) # 浅拷贝b[2].append(5)
print(a) # 输出 [1, 2, [3, 4, 5]]
print(b) # 输出 [1, 2, [3, 4, 5]]
对于简单的可变数据类型,浅拷贝相当于将原对象的值进行拷贝,需要在内存空间中开辟一块新的内存空间
import copy# 对于可变数据类型的浅拷贝
list1 = [1, 3, 5]
list2 = copy.copy(list1)
print(list1, id(list1)) # [1, 3, 5] 2029785917952
print(list2, id(list2)) # [1, 3, 5] 2029788974656
对于复杂的可变数据类型,浅拷贝只能拷贝可变数据类型的最外层对象,而无法拷贝内层对象,所以只需要为最外层对象开辟内存空间,内层对象拷贝之后的引用关系和原对象保持不变。
import copylist1 = [1, 3, 5, [7, 9]]
list2 = copy.copy(list1)
print(list1, id(list1)) # [1, 3, 5, [7, 9]] 1757227908928
print(list2, id(list2)) # [1, 3, 5, [7, 9]] 1757227822720# 但是对于内层[7.9],这也是一个列表,也需要占用内存地址,那么其拷贝过程的内存是否相同呢
print(id(list1[3])) # 2710364155712
print(id(list2[3])) # 2710364155712
对于简单的不可数据类型,由于不可变数类型地址一旦固定,其值就无法改变了,又由于浅拷贝需要把自身的对象空间赋值给另外一个对象,为了保持数据一致,只能让其指向相同的内存空间(不需要额外开辟内存空间)
import copytuple1 = (1, 3, 5)
tuple2 = copy.copy(tuple1)print(tuple1, id(tuple1)) # (1, 3, 5) 1831160185728
print(tuple2, id(tuple2)) # (1, 3, 5) 1831160185728
对复杂的不可变数据类型,浅拷贝也只能拷贝最外层对象,无法拷贝内层对象
1)当浅复制的值是不可变对象(字符串、元组、数值类型)时和“赋值”的情况一样,对象的id值(id()函数用于获取对象的内存地址)与浅复制原来的值相同。
2)当浅复制的值是可变对象(列表、字典、集合)时会产生一个“不是那么独立的对象”存在。有两种情况:
第一种情况:复制的对象中无复杂子对象,原来值的改变并不会影响浅复制的值,同时浅复制的值改变也并不会影响原来的值。原来值的id值与浅复制原来的值不同。
第二种情况:复制的对象中有复杂子对象(例如列表中的一个子元素是一个列表),如果不改变其中复杂子对象,浅复制的值改变并不会影响原来的值。 但是改变原来的值中的复杂子对象的值会影响浅复制的值。
深拷贝:与浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。深拷贝拷贝出来的对象是一个全新的对象,不再与原来的对象有任何关系。
import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a) # 深拷贝b[2].append(5)
print(a) # 输出 [1, 2, [3, 4]]
print(b) # 输出 [1, 2, [3, 4, 5]]
import copya = [1, 3, 5]
b = copy.deepcopy(a)
print(a, id(a)) # [1, 3, 5] 2434688069312
print(b, id(b)) # [1, 3, 5] 2434690765888
import copya = [1, 3, 5, [7, 9]]
b = copy.deepcopy(a)
print(a, id(a)) # [1, 3, 5, [7, 9]] 2162810741504
print(b, id(b)) # [1, 3, 5, [7, 9]] 2162810727424
print(id(a[3])) # 2162810727040
print(id(b[3])) # 2162810727232
对于可变数据类型的深拷贝,深拷贝拷贝了所有的数据并开辟对应的内存空间储存。
import copya = (1, 3, 5, (7, 9))
b = copy.deepcopy(a)
print(a, id(a)) # (1, 3, 5, (7, 9)) 2029346995712
print(b, id(b)) # (1, 3, 5, (7, 9)) 2029346995712print(id(a[3])) # 2029346562432
print(id(b[3])) # 2029346562432
对于不可变数据类型的深拷贝,不管是简单的还是复杂的,深拷贝都只能对象的引用关系,他们指向了相同的内存空间。
可变嵌套不可变数据类型
外层对象是可变数据类型,所以可以进行完全拷贝(需要生成内存空间),但是内层对象是不可变数据类型,所以只能进行拷贝引用关系
import copya = [1, 3, 5, (7, 9)]
b = copy.copy(a)
c = copy.deepcopy(a)
print(id(a)) # 1186799997824
print(id(b)) # 1186799988352
print(id(c)) # 1186799988672print(id(a[3])) # 1700778811776
print(id(b[3])) # 1700778811776
print(id(c[3])) # 1700778811776
不可变嵌套可变数据类型
如果一个不可变数据类型包含了可变数据类型,浅拷贝的结论与之前一致,都只能拷贝引用关系。但是对于深拷贝而言,这种数据类型整体都可以进行完全拷贝
d = (1, 3, 5, [7, 9])
e = copy.copy(d)
f = copy.deepcopy(d)print(id(d)) # 2193468143712
print(id(e)) # 2193468143712
print(id(f)) # 2193468262576 内存地址不一样了print(id(d[3])) # 2764873530496
print(id(e[3])) # 2764873530496
print(id(f[3])) # 2764873530560
参数传递机制:传值还是传引用?
函数的参数传递机制是基于对象的引用的。因此,参数的传递机制实际上是通过引用传递的,但具体的行为取决于对象的可变性。
不可变数据类型(Immutable Types)
定义:创建后不能被修改的数据类型,每次修改都会创建一个新的对象
可变数据类型(Mutable Types)
定义:创建后可以被修改的数据类型,可以直接在原对象上进行修改
变量
将变量视为一个容器,它用来存放某个值。当你给一个变量赋值时,实际上是将数据存放在这个“盒子”里。这个观点对于基本类型的数据(如整数、浮点数、字符串等)是成立的,因为这些数据是值类型,赋值操作是将数据的副本存储到变量中。
x = 10
y = x # 复制 x 的值给 y
x = 20
print(x) # 输出 20
print(y) # 输出 10,y 仍然是原来的值
当我们讨论引用类型(如列表、字典、对象等)时。在这种情况下,变量存储的不是数据本身,而是数据的引用(即指向数据位置的指针或内存地址)。这意味着,当你将一个变量赋值给另一个变量时,两个变量实际上指向同一个数据对象。
a = [1, 2, 3] # a 是一个列表
b = a # b 是对 a 的引用,它指向同一个列表
b.append(4) # 修改 b,也会影响 a
print(a) # 输出 [1, 2, 3, 4]
print(b) # 输出 [1, 2, 3, 4]
python中变量在内存中的存储方式
python变量的存储原理详解_python变量存储机制-CSDN博客
id比较的是对象的内存地址是否相等
==比较的是对象的值是否相等
is比较的是对象的内存地址和值是否相等
Python中堆里面存放的是具体的对象,在堆中Python会为其分配具体的内存空间,此地址即为此对象在内存中的地址
Python中栈里面存放的是对象的地址,而不是对象本体
需要注意的是在Python中会为匿名列表对象和匿名字典对象以及短字符串创建缓存区,,并且Python自带小对象整数池(-5~256)
1. 针对匿名列表和字典对象(即不存在变量名的引用)
列表和字典这种可变对象当为匿名对象是会向外暴露一个内存地址,不论里面的内容怎么变,该地址不变,所以下面的地址都一样。
id([1,2,3]) == id([4,5,6]) # Trueid([1])==id([1,2,3,4,5,6]) # Trueid({1:1,2:2})==id({3:3,4:4}) # True'''但是如果存在对象的引用,即非匿名对象,上面的结果都是False,如下'''a=[1,2,3];b=[4,5,6];c={1:1,2:};d={3:3,4:4}id(a)==id(b) # Fasleid(c)==id(d) # Fasle
2. 针对短字符串
Python会为短字符串创建内存缓存,即针对相同的短字符串,只会创建一个内存空间,不会为相同的短字符串创建多个内存空间,但是长字符串不会如此
3. 针对小整数
Python会为(-5,256)之间的整数分配独立的内存空间,当有对象调用的时候会直接从里面取地址,不论调用多少次,都不会再为这些对象创建新的内存空间