Python对象引用、可变性及垃圾回收

Python通常使用盒子来比喻变量,但是可能使用便利贴更加形象,比如下面的例子:

a = [1, 2, 3]  # 创建一个列表绑定变量a
b = a  # 变量b绑定a引用的值
a.append(4)  # 修改a引用的列表,追加1项
print(b)  # 输出[1, 2, 3, 4]

很明显b=a并不是把a盒子的内容复制给b盒子中,而是在标注为a的对象上再贴一个标注b,即使为对象贴上多个标注也没问题,多出来的标注其实就是别名。

1、同一性、相等性和别名

a = {'A': 11, 'B': 22}
b = a
print(b is a)  # True
print(id(a), id(b))  # 1951990409536 1951990409536
b['C'] = 33
print(a)  # {'A': 11, 'B': 22, 'C': 33}

上面代码is运算符结果为True,a的id和b的id是相等的,b是a的别名。向b里面添加元素相当于向a里面添加元素。但是我们看下面的代码,命名一个a1的变量,值与a一模一样,看结果有什么区别。

a1 = {'A': 11, 'B': 22, 'C': 33}
print(a == a1)  # True
print(a is a1)  # False

比较两个对象,结果相等,这是因为dict里面的__eq__方法就是这样实现的。但是通过is运算符证明,a1不是a的别名,因为他们不是相同的对象。在Python中,对象一旦创建,标识始终不可变。is运算符比较两个对象的标识,id()函数返回对象标识的整数表示。

在==和is之间选择:

==用于比较两个对象的值,而is用于比较两个对象的标识。

在编程时,我们更加关注的是值,而不是表示,因此在代码中使用==的频率要高于is。

然而,在比较一个变量和一个单例时,应该使用is。目前最常使用is检查变量的值是不是None,比如 x is None 或者 x is not None。

is运算符比==快,因为is不能重载,不需要寻找特殊方法而是直接比较对象ID就行。而a==b等同于a.__eq__(b),继承object的__eq__方法比较两个对象ID,结果和is一样。但是,多数内置类型使用更有意义的方式覆盖了__eq__方法。

我们通常更关注对象的相等性而不是同一性,如果你不确定使用==还是is,那就使用==,而且也适用于is,尽管速度没is快。

元组的相对不可变性:

元组存储的是对象的引用,即使说元组是不可变的,但是如果说里面引用项是可变的,项依然还是可以更改的,元组的不可变性是相对的。元组的值会随着引用的可变对象而变,元组中永不可变的是项的标识,比如:

t1 = (1, 2, [3, 4])
t2 = (1, 2, [3, 4])
print(t1 == t2)  # True
print(id(t1[-1]))  # 2300765327680
t1[-1].append(5)
print(t1)  # (1, 2, [3, 4, 5])
print(id(t1[-1]))  # 2300765327680
print(t1 == t2)  # False

即便修改了t1[-1]的列表数据,但是t1[-1]的标识没变,只是值变了。

2、浅拷贝

复制列表最简单的方式是使用内置的构造函数,比如:

l1 = [1, [2, 3], 4, [5, 6]]
l2 = list(l1)
print(l1 == l2)  # True
print(l1 is l2)  # Fasle

从上面代码很明显看出,l2不是l1的别名,而是使用list()创建了l1的副本。对列表或者其他序列来说,还可以使用切片创建副本:l2=l1[:] 

构造函数list()或者[:]是浅拷贝,如果里面的项是不可变的,那没什么问题,而且还能节省内存,但是如果有可变的项,那么可能会引发问题。

l1 = [1, [2, 3], 4, (5, 6)]
l2 = list(l1)
l1.append(100)
l1[1].remove(3)
print(f"l1: {l1}")  # l1: [1, [2], 4, (5, 6), 100]
print(f"l2: {l2}")  # l2: [1, [2], 4, (5, 6)]
l2[1] += [4, 5]
l2[3] += (7, 8)
print(f"l1: {l1}")  # l1: [1, [2, 4, 5], 4, (5, 6), 100]
print(f"l2: {l2}")  # l2: [1, [2, 4, 5], 4, (5, 6, 7, 8)]

把内部l1中的3删除,这对l2有影响,因为l2[1]绑定的l1[1]是同一个对象。对可变对象来说,例如l2[1] += [4, 5],+=就地修改列表,从而会影响l1的值,而对于元组来说,+=相当于创建一个新元组,然后重新绑定给变量,因此不会影响l1里面的元组。

3、函数参数是引用时

Python唯一支持的参数传递模式是共享传参。共享传参指的是函数的形参获得实参引用的副本,即形参是实参的别名。这种方式会导致函数可能修改作为参数传递进来的可变对象,但是无法修改那些对象的标识。

def f(a, b):
    a += b
    return a
x, y = 1, 2
print(f(1, 2))  # 3
print(x, y)  # 1 2
a, b = [1, 2, 3], [4, 5, 6]
print(f(a, b))  # [1, 2, 3, 4, 5, 6]
print(a, b)  # a:[1, 2, 3, 4, 5, 6] b:[4, 5, 6]
w, v = (1, 2, 3), (4, 5, 6)
print(f(w, v))  # (1, 2, 3, 4, 5, 6)
print(w, v)  # w:(1,2,3) v:(4,5,6)

在编写代码时,尽量不要使用可变类型作为参数的默认值。如果你定义的函数接收可变类型,你要谨慎考虑函数里面是否期望修改传入的参数,除非你确实想修改通过参数传入的对象,如果不确定,那就创建副本,免得为程序埋下难以察觉的bug。

del和垃圾回收

del语句删除引用而不是对象。del可能导致对象被当做垃圾回收,但是仅当删除的变量保存的对象是最后一个引用时。重新绑定也可能导致对象的引用数量归零,致使对象被销毁。

a = [1, 2]
b = a
del a
print(b)  # [1, 2]
b = [3]
print(b)  # [3]

上面代码,a,b同时指向[1,2]对象,删除引用a,[1,2]不受影响,因为还有b指向它,把b绑定[3]时,[1,2]最后一个引用随之删除,现在垃圾回收机制可以销毁[1,2]了。

在Cpython中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即销毁:Cpython对象上调用__del__方法,然后释放分配给对象的内存。Cpython2.0增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,那么即使再出色的引用方式也会导致组中的对象不可达。分代垃圾回收算法能把引用循环中不可达的对象销毁。

总结(面试或者认证考试中可能提到):

  • 变量保存的是引用,这一点对Python有实质的影响
  • 简单的赋值不会创建副本
  • 对于+=或*=s增量赋值来说,如果左边的变量绑定的是不可变对象,则创建新对象,如果是可变对象,则就地修改
  • 为现有变量赋予新值,不修改之前绑定的变量。这叫重新绑定:现在变量绑定了其他对象。如果变量是之前那个对象的最后一个引用,则对象被当做垃圾回收。
  • 函数的形参以别名的方式传递,这意味着可能会修改通过实参传入的可变对象。这一行为无法避免,除非在本地创建副本,或者使用不可变对象(例如元组)。
  • 使用可变类型作为参数的默认值有危险,因为如果就地修改了参数,默认值也就变了,这会影响后续使用默认值的调用
  • 在Cpython中,对象引用数归零后,对象立即被销毁。如果除了循环引用之外没有其他引用,则循环引用的两个对象都被销毁

 

 

  • 26
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值