python动态类型与对象拷贝

                                       python动态类型与对象拷贝

文章开始把我喜欢的这句话送个大家:这个世界上还有什么比自己写的代码运行在一亿人的电脑上更酷的事情吗,如果有那就是让这个数字再扩大十倍。


1.动态类型和对象引用

在Python中,我们要明确一个概念:变量名和对象是划分开的, 变量名永远没有任何关联的类型信息,类型是和对象关联的,而不存在于变量名中。一个变量名当第一次被赋值的时候被创建,而当新的赋值表达式出现时,他会马上被当前新引用的对象所代替。这就是Python所谓的 动态类型机制

具体看一个例子:
代码片段:

a = 'abcde'
print(a)
a = [1,2,3,4,5]
print(a)
运行结果:

abcde
[1, 2, 3, 4, 5]
结合上面这个简单的例子,我们再来从头仔细理一理:

1、创建了一个字符串对象’abcde’,然后创建了一个变量a,将变量a和字符串对象 ‘abcde’相连接,
2、之后又创建了一个列表对象[1,2,3,4,5],然后又将他和a相连接。

这种从变量到对象的连接,我们称之为引用,以内存中的指针形式实现。因此直白的说,在内部,变量事实上是到对象内存空间的一个指针,而且指向的对象可以随着程序赋值语句而不断变化。

总结一下:变量名没有类型,只有对象才有类型,变量只是引用了不同类型的对象而已。每一个对象都包含了两个头部信息,一个是类型标志符,标识这个对象的类型,以及一个引用的计数器,用来表示这个对象被多少个变量名所引用,如果此时没有变量引用他,那么就可以回收这个对象。

2.Python垃圾收集机制

基于上面谈到的引用机制,我们再说说Python的垃圾收集机制

还是上面那个例子,每当一个变量名被赋予了一个新的对象,那么之前的那个对象占用的空间就会被回收,前提是如果他没有被其他变量名或者对象引用。这种自动回收对象空间的机制叫做垃圾收集机制。

即当a被赋值给列表对象[1,2,3,4,5]时, 字符串对象的内存空间就被自动回收(前提是如果他没有被别的变量引用

具体的内部机制是这样的:Python在每个对象中保存了一个计数器,计数器记录了当前指向该对象的引用的数目。一旦这个计数器被设置为0,这个对象的内存空间就会自动回收。当a被赋值给列表对象后,原来的字符串对象‘abcde’的引用计数器就会变为0,导致他的空间被回收。 这就使得我们不必像C++那样需要专门编写释放内存空间的代码了

3.共享引用机制

我们接着再说说共享引用的内容,如下所示,多个变量名引用了同一个对象,称为共享引用:
代码片段:

a = 'abcde'
b = a
print(a)
print(b)
运行结果:

abcde
abcde
此时字符串对象’abcde’的引用计数是2,我们进一步往下看如果我们此时对变量a重新赋值呢?
代码片段:

a = 'abcde'
b = a
a = [1,2,3,4]
print(a)
print(b)
运行结果:

[1, 2, 3, 4]
Abcde
结果是显而易见的,变量a变成了列表对象的引用,而变量b依然是字符串对象’abcde’的引用,并且字符串对象的引用计数为由2变为1.

如果此时再对b进行重新赋值,字符串对象‘abcde’的引用计数就会变为0,然后这个对象就被垃圾回收了。

我们今天的话题要从“可变对象的原处修改”这里引入,这是一个值得注意的问题。

4.可变对象的原处修改

正如刚才我们谈到的,赋值操作总是存储对象的引用,而不是这些对象的拷贝。由于在这个过程中赋值操作会产生相同对象的多个引用,因此我们需要意识到“可变对象”在这里可能存在的问题:在原处修改可变对象可能会影响程序中其他引用该对象的变量。如果你不想看到这种情景,则你需要明确的拷贝一个对象,而不是简单赋值。
代码片段:

X = [1,2,3,4,5]
L = ['a', X, 'b']
D = {'x':X, 'y':2}

print(L)
print(D)
运行结果:

['a', [1, 2, 3, 4, 5], 'b']
{'y': 2, 'x': [1, 2, 3, 4, 5]}
在这个例子中,我们可以看到列表[1,2,3,4,5]有三个引用,被变量X引用、被列表L内部元素引用、被字典D内部元素引用。那么利用这三个引用中的任意一个去修改列表[1,2,3,4,5],也会同时改变另外两个引用的对象,例如我利用L来改变[1,2,3,4,5]的第二个元素,运行的结果就非常明显。
代码片段:

X = [1,2,3,4,5]
L = ['a', X, 'b']
D = {'x':X, 'y':2}

L[1][2] = 'changed'
print(X)
print(L)
print(D)
运行结果:

[1, 2, 'changed', 4, 5]
['a', [1, 2, 'changed', 4, 5], 'b']
{'x': [1, 2, 'changed', 4, 5], 'y': 2}
我不得不说,有坑请绕行,在这些地方还真的挺容易犯错的。

引用是其他语言中指针的更高层的模拟。他可以帮助你在程序范围内任何地方传递大型对象而不必在途中产生拷贝,起到优化程序的作用。

5.获取对象的独立拷贝

但是,如果我不想共享对象引用,而是想实实在在获取对象的一份独立的复制,该怎么办呢?这个需求在实际的编程中也很常见,常用的手法有以下几种:

5.1.分片返回新的对象拷贝

第一种方法:分片表达式能返回一个新的对象拷贝,没有限制条件的分片表达式能够完全复制列表
代码片段:

L = [1,2,3,4,5]
C = L[1:3]
C[0] = 8
print(C)
print(L)
运行结果:

[8, 3]
[1, 2, 3, 4, 5]
代码片段:

L = [1,2,3,4,5]
C = L[:]
C[0] = 8
print(C)
print(L)
运行结果:

[8, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
可以看出,用分片表达式得到了新的列表拷贝C,对这个列表进行修改,不会改变原始列表L的值。

5.2.字典的copy方法可获取独立拷贝

第二种方法:字典的copy方法也能够实现字典的完全复制:
代码片段:

D = {'a':1, 'b':2}
B = D.copy()
B['a'] = 888
print(B)
print(D)
运行结果:

{'a': 888, 'b': 2}
{'a': 1, 'b': 2}
5.3.内置函数list可以生成独立拷贝

第三种:内置函数list可以生成拷贝
代码片段:

L = [1,2,3,4]
C = list(L)
C[0] = 888
print(C)
print(L)
运行结果:

[888, 2, 3, 4]
[1, 2, 3, 4]
5.4.应用举例

最后我们看一个复杂一些的例子

B通过无限制条件的分片操作得到了A列表的拷贝,B对列表内元素本身的修改,不会影响到A,例如修改数值,例如把引用换成别的列表引用:
代码片段:

L = [1,2,3,4]
A = [1,2,3,L]
B = A[:]
B[1] = 333
B[3] = ['888','999']//没有引用L直接修改的B[3]
print(B)
print(A)
print(L)
运行结果:

[1, 333, 3, ['888', '999']]
[1, 2, 3, [1, 2, 3, 4]]
[1, 2, 3, 4]
但是如果是这种场景呢?
代码片段:

L = [1,2,3,4]
A = [1,2,3,L]
B = A[:]
B[1] = 333
B[3][1] = ['changed']//引用了L
print(B)
print(A)
print(L)
运行结果:

[1, 333, 3, [1, ['changed'], 3, 4]]
[1, 2, 3, [1, ['changed'], 3, 4]]
[1, ['changed'], 3, 4] 
  因为B的最后一个元素也是列表L的引用(可以看做获取了L的地址),因此通过这个引用对所含列表对象元素进行进一步的修改,也会影响到A,以及L本身

所以说,无限制条件的分片操作以及字典的copy方法只能进行顶层的赋值。就是在最顶层,如果是数值对象就复制数值,如果是对象引用就直接复制引用,所以仍然存在下一级潜藏的共享引用现象。
 
 5.5.deepcopy自顶向下递归独立复制

如果想实现自顶向下,深层次的将每一个层次的引用都做完整独立的复制,那么就要使用copy模块的deepcopy方法。
代码片段:

import copy

L = [1,2,3,4]
A = [1,2,3,L]
B = copy.deepcopy(A)

B[3][1] = ['changed']
print(B)
print(A)
print(L)
运行结果:

[1, 2, 3, [1, ['changed'], 3, 4]]
[1, 2, 3, [1, 2, 3, 4]]
[1, 2, 3, 4]
这样,就实现了递归的遍历对象来复制他所有的组成成分,实现了完完全全的拷贝,彼此之间再无瓜葛。

加油吧,程序员!

猜你喜欢

转载自blog.csdn.net/weixin_42248302/article/details/80810157