关于python中“赋值就是建立一个对象的引用”,大家怎么看?Python一切皆为对象又是什么意思?

为什么我觉得好不方便啊... 比如如下的代码: x=[1,2,3,4] y=x y[0]=4 print x >> x = [4 2 3 4]
关注者
147
被浏览
29,657

14 个回答

挺方便的

object.h


而Python中一切皆来源于此,而这两个宏定义为:

其实可发现PyObject_VAR_HEAD也只是PyObject_HEAD加上一个ob_size,于是Python中,每一个对象都拥有相同的对象头部,于是我们只需要用一个PyObject *就可以引用任意的一个对象,而不论该对象实际是一个什么对象,所以,当内存中存在某个Python对象时,该对象的开始的几个字节的含义一定会符合我们的预期,即PyObject_HEAD。而PyObject_HEAD宏定义中的_PyObject_HEAD_EXTRA其实只是指向_object的一个双向链表,

而ob_refcnt则是引用计数的计数器,而struct _typeobject *obtype则是类型,表示对象类型的类型对象:

通过PyObject_VAR_HEAD可以发现类型其实也是一个对象(一切皆对象),而类型对象的类型则是类型对象所关联的类型:PyType_Type:

而即使如简单的int也是来填满这里的东西:

于是,即使是int也是对象

所以即使你使用

i = 47

j = 47

也是对47的一个引用,而

通过id,可以看出其在内存引用位置相同,而为什么相同?因为为了减少频繁调用开销,使用了small int对象池的技术:

于是只要超过257(包括257),则变了:

所以, m,n虽然都是对258对象的引用,却是不同的内存地址了.

剩下的你就可以继续按照这条路线探寻Python的机制了,而你的问题也基本得到解答了,当你疑惑一些问题时,源码是最好的解释。 :-)

P.S. Python版本: 2.7.8

也欢迎关注我的知乎账号 @石溪 ,将持续发布机器学习数学基础及Python数据分析编程应用等方面的精彩内容。

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

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

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}

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

可是如果我不想共享对象引用,而是想实实在在获取对象的一份独立的复制,该怎么办呢?

能想到这一层确实很不错,其实这个很简单,常用的手法有以下几种:

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

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的值。

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

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

{'a': 888, 'b': 2}
{'a': 1, 'b': 2}

第三种:内置函数list可以生成拷贝

L = [1,2,3,4]
C = list(L)
C[0] = 888
print(C)
print(L)

[888, 2, 3, 4]
[1, 2, 3, 4]

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

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

L = [1,2,3,4]
A = [1,2,3,L]
B = A[:]
B[1] = 333
B[3] = ['888','999']
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']
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方法只能进行顶层的赋值。就是在最顶层,如果是数值对象就复制数值,如果是对象引用就直接复制引用,所以仍然存在下一级潜藏的共享引用现象。

如果想实现自顶向下,深层次的将每一个层次的引用都做完整独立的复制,那么就要使用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]

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

没想到简单的赋值还有这么多的坑!最后再来总结总结:普通的=赋值得到的其实仅仅是共享引用;无限条件的分片、字典copy方法和内置函数list这三种方法可以进行顶层对象的拷贝,而deepcopy可以彻底的实现自顶向下的完全拷贝。

关于Python编程和数据分析更全面的内容,欢迎关注我在CSDN上的专栏《python数据分析编程基础》。

有任何问题也可以咨询微信号:zhangyumeng0422