Python对象的故事

论坛 期权论坛 期权     
识途马老   2019-7-21 15:19   2690   0
【Python开脚第三步】Python中的变量就是标签,标签贴在什么对象上,它的类型(type)就是什么。而且这个标签可以无损地撕下来再贴到别的对象上。这次识途马老来说说Python中的对象的故事。因为不是教科书,就不讲究什么系统不系统了。

第1个故事:可变对象与不可变对象

对象(Objects)是Pyhton对数据(data)的抽象。Python程序中的所有数据都由对象来表示。

每个对象都有一个身份标识(identity),一个类型(type)和一个值(value)。如果变量标签贴到对象上了,那么这个标签也可以称为对象的名字(name)。

对象一旦创建,身份标识永不改变。id(x)返回标签x所贴的对象的身份标识(用一个整数表示), a is b 是对比两个对象的身份标识。你可以把一个对象的身份标识想象成对象的内存地址。Python是语言规范,语言规范可以有不同的实现版本,在CPython实现版本中,id(x)就是x标签所贴的对象的内存地址。

对象一旦创建,其类型也是不可改变的(某些条件下对象的类型在某些场合下确实可以改变,但最好别这样做)。类型决定了对象能取哪些值,能进行哪些操作或运算。不要把变量理解成对象本身,比如,x=24, 后面也可以x="ok!"。所谓“类型不可改变”,指的是整数24和字符串"ok!"的类型不可改变,x本身只是一个标签。我们后面说“对象x”,应该理解成“标签x所贴的对象”,是一种简化说法。因此,你也不要狡辩说,“创建了x=24,然后x的类型不能改变,后面x="ok!"改变了x的类型,所以就错了。”

对象创建后,它的值是否能改变,就要分两种情况了,一种是:mutable,这种对象的值可以改变,另一种就是:immutable,这种对象一旦创建,其值不能改变。对象是否可变,要看对象的类型。有些不可变容器对象(immutable container object )里面可能包含了可变对象(比如y),那么这个可变对象(y)的值还是可变的,但对象(y)本身就不能换成别的对象(比如z)了。所以,对象的不可变性有时候是很微妙的,要小心处理。反过来说,如果是可变容器对象(mutable container object )里面包含了不可变对象(比如s),你可以把s对象从容器中换掉,但s对象本身的值不能换。

下面识途马老制作了一张图片,从中可以清楚地看出Python中,哪些内建的数据类型是可变的,哪些是不可变的。注意,其中只包含的内建的数据类型,没有包含由其他模块扩展的数据类型。



图1. Python3内建数据类型的可变性与不可变性


第2个故事:赋值、浅拷贝、深拷贝

在Python中,变量赋值就是贴标签。而像Tuple或List类型的对象,可以看成是组织起来的一组标签,它们需要一个总标签,这个总标签就是变量名。
现在运行下面这个简单程序,看看输出结果。



图2. list对象的内存结构

通过对输出的解读就能让我们知道List的内存结构。按惯例,当然是先创造了列表对象(这里就是[555, 6, 777, 888, 999]),变量lista就是总标签,总标签下面是一组小标签(lista[0], lista[1], ......), 指向列表对象(这里就是[555, 6, 777, 888, 999])的各个元素。



图3. list对象的内存结构图解

那么,把lista赋值给另一个变量listb,listb=lista, 是啥效果呢?这个效果如下图所示,同样的内存地址上又贴上了一个标签。这会导致一个有趣的现象了,你把listb[1]所指的对象改为666的话( listb[1]=666 ), 则lista和listb的值都会变成[555, 666, 777, 888, 999]了,哪怕从代码上看,你并没有对lista做这样的操作。




图4. list对象的赋值操作

怎么理解这一点呢?这就像某人名叫“张三丰”,小名叫“君宝”。某天张三丰不小心把手指头切掉了。李四在旁边看到,赶快打电话给张三丰的爹,说君宝手指头切掉了,正送往医院。张三丰受伤,也就是君宝受伤,反过来,君宝受伤,也就是张三丰受伤。这里的张三丰和君宝就是lista和listb了。
但是,我们再进行另一波操作:listc=lista[:],则情况就会发生变化了。listc会建立自己的总标签( listc )和分标签( listc[0], listc[1]...... 等等 ),只是分标签的指向就是lista中的对应对象元素。这就是所谓的浅拷贝浅复制。Python有一个专门的模块copy,就提供了浅拷贝功能。



图5. list的浅复制或浅拷贝

与浅拷贝相对应的就是深拷贝了,也是由copy模块提供的。比如,我们想将lista深拷贝给listc,代码就是这样的:listc=copy.deepcopy( lista )。为了搞清楚深拷贝的细节,我们加深一下lista的层级,在lista里面放一个列表元素 ( list1 ),一个元组元素 ( tup1 )。程序和运行结构如图6. 不想看代码的可以直接跳过图6往下读。



图6. 深拷贝演示程序

图7是改造过后的lista的内存结构,lista总标签下,还是5个小标签,分别指向了 [555, 6, list1, tup1, 999] 五个元素。list1里面有两个标签,指向 [ 'yes',  'good' ] 两个对象。tup1元组的两个标签指向 ( 456, 'good python' ) 两个元素(图7中没有列出这两个元素)。


图7.多层次的list列表结构

执行深拷贝语句 listc=copy.deepcopy( lista )之后,listc是啥样的呢?首先,与浅拷贝一样,listc有自己的总标签和分标签,但分标签的指向与浅复制的时候是不一样的。lista中的五个元素 [555, 6, list1, tup1, 999] ,其中的555,6,tup1, 999 都属于不可变对象,listc对应的分标签指向不变,但元素 list1 是可变对象,深拷贝就将 list1 克隆了一整套,并且给这个克隆贴上了一个标签 listc[2] ,如图8所示。为使图示简明清晰,指向不变的标签在图8中没有画出来。



图8. 深拷贝的后果

今天的故事讲完了。其中有一些细节因为过于繁琐,本文一律略去了。有兴趣的读者可以自己去钻研。识途马老之所以纠缠这些对象的内存结构及其变化动作,就是因为在编程的世界里,只有知道了代码的准确运作方式,才能确保编出的代码按程序员的预想运行。我们无法想象一个作曲家不知道音符怎么发音,却能够写出优美的音乐曲谱。这个道理在编程的世界里也是一样的。

长按二维码,关注“识途马老”公众号


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:
帖子:
精华:
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP