流畅的python:对象引用、可变性、垃圾回收-Part1

第八章 对象引用、可变性、垃圾回收-Part1

1、到底什么是变量?

从一开始学python,你可能就知道,python中的一切都是对象,那变量到底跟对象有啥关系?我们先看这样的一个例子:

class Onevar():
    def __init__(self):
        print('Onevar ID:%d' % id(self))


x = Onevar()
print('x ID:', id(x))
y = Onevar() * 10
print('y ID:', id(y))

# 输出:
Onevar ID:4
x ID: 4
Onevar ID:5
Traceback (most recent call last):
TypeError: unsupported operand types for *: 'Onevar' and 'int'

首先我们实例化x,自然类中的地址self赋予了x变量,他们的地址是一样的。好了,这点很好理解,他们的地址都是指向同一个对象,不管是self还是x都是对象在不同场景下的分配。

接下来我们第八行,在这里我们可以分析出到底是先有对象还是变量名!首先我们可以明确 Onevar() * 10是不可能正确执行的,必然会报错,但是此时首先返回一个Onevar ID,我们看当前空间里都有啥,在命令行运行dir()你会发现有很多变量,但是肯定不会有y,因为赋值语句右侧先被执行,而当创建对象时已经发生了错误,那么变量y肯定不会被创建。所以在此我们也对赋值语句有了更深层次的认识:赋值语句右侧创建对象,然后左侧的变量名绑定到该对象上,对其进行了标注,也就是赋值语句先执行右边,后执行左边。

所以变量是对象的绑定或者标注,所以一个对象可以存在多个标注,也就是存在多个变量名,我们把这些变量名叫做别名。

2、标识、相等性和别名

笔者有一个英文名叫做Murphy,有一个笔名叫做不爱锻炼的三石,论坛中我也爱用墨菲(mofei)这个名字,也就是说mofei是我的别名,用python描述这种关系:

murphy = {'name': '不爱锻炼的三石'}
mofei = murphy
mofei is murphy # 输出True
id(mofei), id(murphy) # 输出(3053929972264, 3053929972264)

很好理解,我们创建的是一个dict对象,murphy和mofei都是变量名,两者绑定同一个对象,is 和两者的id可以证明这条结论。如果修改murphy的时候,mofei也会随之改变:

murphy['age'] = 25
mofei['age'] #返回25

但是有一天,我的身份信息遭到了泄露,有一个真名叫jack的人和我拥有相同的身份信息:

jack = {'name': '不爱锻炼的三石', 'age': 25}

但是我们尽管身份信息相同,但是却不是同一个实际对象,所以id不一致,is操作符返回False:

print(jack == murphy) # 输出True
print(jack is murphy) # 输出False
print(id(jack), id(murphy)) # 输出(3053940102952, 3053929972264)

每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变。关键是,可以把标识理解为对象在内存中的地址,对应的ID一定是唯一的数值标注,而且在对象的生命周期中绝不会变。is运算符比较两个对象的标识;id( )函数返回对象标识的整数表示。而==描述的是内容是否一致,与地址没有关系。

is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。

3、元组的相对不可变性

在开始之前,我们先看下面的一段代码:

t1 = (23, 'my', [23, 45])
t1[-1].append(21)

可能之前我们已经讲过这段代码,其实际运行的结果是:t1被修改为(23, ‘my’, [23, 45, 21]),这似乎与前面我们学到的元组的不变性相违背,事实上元组与多数Python集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
以上面的例子来说:原本的t1[2]=[23, 45]其id在笔者电脑上为3053935215112,增加了一个元素21以后,其地址保持不变,所以程序并不会报错。这就说明了元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识,这被称为元组的相对不可变性

那我能改变t1[1]吗?对于不可变的字符串来说,无法进行增删等修改操作,如果直接赋值,其地址必然发生改变,会立即抛出TypeError,对于一般的元组来说:

a = (1, 2, 3)
b = a
a is b # True
b = (2, 3, 4)
a is b # False

b = a相当于对对象(1,2,3)增加了一个别名,所以两者id一致,但是对于第4行又重新创建了一个(2, 3, 4)对象,并用b进行了标注,那么这时候b所指向的对象与a指向的对象肯定是不一样的。

所以在这里大家在明白元组相对不变性的同时,也要记住:不要把可变序列放在元组之中。

4、浅复制

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如下面的这个列表:

a = [2, '3', [4, 5, 5], (7, 4, 3)]

我想经过前面的描述,你在复制列表时不至于傻傻的还用b=a这样的直接赋值,因为这样并不是复制,而是为对象增加一个别名,两者指向的还是同一个对象。想要复制内容而保持无关,就必须创建一个全新的对象,例如使用:

b = list(a) # 或者使用b = a[:]
a is b # False

可是事情往往不那么简单,我现在想要修改a中的一个序列值:

a[2].append(31)
print(b)
# 输出
[2, '3', [4, 5, 5, 31], (7, 4, 3)]

见鬼!b的值又被改变了,我们明明创建了一个新的对象,a和b的id也并不一样。这是怎么回事呢?为了便于说明,推荐一个网站:Python Tutor,可以将我们的代码可视化:

代码可视化

我们可以看到,对于a,b这两个对象来说,确实指向两个不同的列表,对于不可变元素来2,‘3’来说,两个这的对象确实不一致,但是对于list和tuple来说,却指向相同的子对象。这也就说明了为啥a[2]修改后b[2]同样发生了改变。当然元组这样保存是合理的,可以节省内存,而且因为其不可变,变化了就肯定不是同一个对象:

a = [2, '3', [4, 5, 5], (7, 4, 3)]
b = list(a)
a[-1] = (2, 4, 5)
print(b)
元组改变代码可视化

像这样复制了最外层容器,副本中的元素是源容器中元素的引用,我们称之为浅复制,构造方法或[:]做的就是浅复制。如果所有元素都是不可变的,那么这样没有问题,还能节省内存,如果存在可变元素,那么就一定要小心了。

5、深复制

副本不共享内部对象的引用的复制方式就是深复制,通常使用copy模块中的deepcopy进行深复制,而copy.copy为浅复制,还以上一节中的例子为说明:

import copy
a = [2, '3', [4, 5, 5], (7, 4, 3)]
b = copy.copy(a)  # 浅复制
c = copy.deepcopy(a)  # 深复制
深复制

可以看出,深复制对象c的可变子序列是一个新的list对象,而对于不可变元素则是指向相同的对象,在节省内存的同时,也保证了内部子元素不会指向同一对象。

——未完待续——
欢迎关注我的微信公众号
扫码关注公众号

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值