流畅的python笔记(八)对象引用、可变性和垃圾回收

本文深入探讨Python中的变量、对象标识、相等性、别名以及拷贝的概念。解释了浅拷贝与深拷贝的区别,并展示了如何通过`copy`模块进行深拷贝。此外,还讨论了函数参数作为引用的影响,强调了避免在函数默认值中使用可变对象的重要性。最后,阐述了`del`操作、垃圾回收以及弱引用的使用和限制。
摘要由CSDN通过智能技术生成

目录

一、变量不是盒子

二、标识、相等性和别名

对象的标识、类型和值

== 和 is的不同

元组的相对不可变性

三、默认做浅拷贝

浅拷贝

深拷贝

四、函数的参数作为引用

python函数默认引用传参

不要使用可变类型作为参数默认值

防御可变参数

五、del和垃圾回收

六、弱引用

弱引用可调用,返回所指对象

WeakValueDictionary

弱引用的局限


一、变量不是盒子

python中变量不是盒子,而是盒子上贴的标签。对象才是盒子。对象一般是指一块存储空间,变量是对象上贴的标签,一个对象可以贴好多个标签,即可以对应很多个变量名。如下例子,把变量a和b分配给了同一个列表对象,类似于C++中的引用概念,python中的变量的本质都是C++中的引用。

 

因为对象在右边,总是先于变量创建,因此正确说法是把某变量分配给某对象,而不是反过来。

二、标识、相等性和别名

对象的标识、类型和值

每个对象都有标识、类型和值。

  • 对象标识,一经对象创建,在对象的生命周期中标识就不会再改变。id()函数返回对象标识的整数表示。
  • 类型,对象对应的类型。
  • 值,两个对象的值可能一样,但是这还是两个不同的对象,因为其标识不同。

== 和 is的不同

== 运算符比较两个对象的值,而 is 比较对象的标识。

        一个特殊的例子,最常使用is检查变量绑定的值是不是None。

  • is运算符比 == 速度快,因为is不能重载,因此python不用寻找并调用特殊方法。
  • 而 a == b 是语法糖,等价于 a.__eq__(b),即调用魔术方法比较a与b。
  • 继承自object的最原始的__eq__方法等价于is,是直接比较id的,但是很多类型都重写了__eq__方法,去考虑值的相等性。

元组的相对不可变性

元组里保存的是元素的引用,但如果引用的元素是可变的,即便元组本身不可变,其元素也依然可以变化。即元组中不可变的是元素的标识,而不是元素的值。

三、默认做浅拷贝

浅拷贝

我们知道python中的变量不是盒子,二是标签,那么当我们要复制一个列表的时候,如果直接用

l2 = l1 则相当于l2和l1是别名,两个对应同一个对象。要复制列表最简单的方式是使用其构造函数。如下面例子中,l1 和 l2的值相同,但是是两个不同的对象。

对于列表和其它可变序列来说,还可以使用 l2 = l1[ : ] 来创建副本。

l1 = [1, 2, 3]
l2 = l1[:] # l2 = list(l1)得到的结果相同
print(l1 == l2)
print(l1 is l2)
print(l1[0] is l2[0])

根据实验结果可看出,用 l2 = l1[:] 创建副本时 l2与l1确实是不同的对象,但是由于列表对象中的元素是引用,而l2直接复制了l1中的引用,因此l1和l2中元素对应的是相同的对象。若所有元素不可变,这样做没有问题且可以节省内存,但如果有可变的元素则这样不对。

        综上,用构造方法或者[ : ]创建副本时做的都是浅拷贝。即只复制了外壳,副本中的元素都是源容器中元素的引用。

        接下来一个例子,对一个包含一个列表和一个元组的列表做浅拷贝。

  1. l2是l1的浅拷贝副本、
  2. 给l1列表加一个元素,100,此时不会影响到l2
  3. 给l1列表的第1个元素(即列表[66, 55, 44])移除元素55,因为l2中的元素是l1中元素的引用,因此此操作会影响到l2
  4. 给l2的第1个元素拼接上[33, 22](可变对象+=是原地操作),同理,l2中与l1中元素互为别名,因此会影响到l1
  5. 给l2的第2个元素拼接上(10, 11)(不可变对象+=会创建一个新对象绑定给原变量),因为元组不可变,因此拼接之后l2中对应位置的元组变成了一个新的元组对象(7,8,9,10,11),但是l1中不会改变,此时l1与l2中对应位置已经是不同的元组对象了。

        上述例子输出如下,符合预期。

深拷贝

深拷贝即副本不共享内部对象的引用

        不管浅拷贝还是深拷贝,拷贝之后的副本与原对象肯定是不同的对象,即标识不同,但是浅拷贝中副本与原对象共享内部元素的引用。copy模块提供的deepcopy和copy函数能为任意对象深拷贝和浅拷贝。下面例子展示deepcopy和copy的用法。

from copy import deepcopy, copy

class Bus:
    
    def __init__(self, passengers=None): # 这里用None不用[]是有原因的,看下边讲解
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy(bus1)
bus3 = deepcopy(bus1)
print("three buses' ids: ", id(bus1), id(bus2), id(bus3))

bus1.drop('Bill')
print('bus1: ', bus1.passengers)
print('bus2: ', bus2.passengers)
print('bus3: ', bus3.passengers)

print("three buses' passengers' ids: ", id(bus1.passengers),id(bus2.passengers),id(bus3.passengers))

通过特殊方法__copy__()和__deepcopy__()可以控制copy和deepcopy的行为。

        含有循环引用的拷贝问题不做讨论。

四、函数的参数作为引用

python函数默认引用传参

C里边默认的传参方式是值传递,而python里边默认的传参方式是引用传递,即形参是实参的别名,因此如果传入的实参是可变对象,则函数体内可能会改变实参。如下例子所示,传入不可变对象时是改变不了实参的,因为 a += b,实际上a已经分配给一个新的对象了,因此不会改变实参。

不要使用可变类型作为参数默认值

函数的有些参数可以设置缺省/默认值,但是这个值不能用可变类型。

上边例子中把HauntedBus类的构造函数中passengers的默认值由None改成了空列表[ ]。带来的改变是某个HauntedBus类的对象的self.passengers属性是__init__函数的参数passengers的别名,而默认情况下,如果在构造的时候没有传参,即默认构造时,passengers时空列表[ ] 的别名。而空列表[ ] 这个时候已经变成了函数__init__的一个属性(python中函数是一等对象,因此空列表[ ]是函数对象__init__的属性这点不难理解。

        因此,当我们构造bus1时,因为传参构造,所以passengers是传入参数的别名,而非函数对象__init__的属性空列表[ ] 的别名,因此正常运行,不会有错。

        但是当我们构造bus2时,因为是默认构造,因此bus2的self.passengers实际上是函数对象__init__的属性空列表 [ ] 的别名,因此对于bus2的self.passengers属性的一切操作都是反应到函数对象__init__的属性上,因此当我们默认构造bus3的时候,实际上这个时候默认的passengers已经不是空列表了,而且bus2和bus3的self.passengers属性是同一个列表对象的别名(这个列表对象即为构造函数中的默认列表参数)。

防御可变参数

如果定义的函数接收可变参数,需要谨慎考虑是否修改传入的参数。

 

还是这个校车类,下面例子中我们传入实参basketball_team,然后经过校车类对象处理后影响到了实参,如果要避免影响到实参,在构造函数中应该给对象的self.passengers属性分配实参的一个副本,而不应该直接跟实参绑定。

 

改造方案如下:

五、del和垃圾回收

del只会删除对象的引用,而不会删除对象,但删除对象的引用可能会导致对象被删除。

python对象被删除有两种情况:

  1. 某个对象的引用计数为零
  2. 一组对象之间全是相互引用,导致组中对象不可获取

        两种情况可以归结于同一种情况,即这个对象不可获取了,那么就会被当作垃圾回收。

weakref.finalize用函数bye来监视被s1绑定的对象,如果该对象被回收,则bye就会被调用,其返回值ender可以看s1绑定对象的状态,一开始的时候肯定是或者的,即 alive == True,当我们用del删除s1的时候,bye并没有被调用,因此对象没有被删除,这是因为对象还有一个引用s2,引用计数并不等于0。但是当我们将s2指向其他对象(即‘spam’)的时候,原先的对象就不可获取了,因此会被回收,也因此bye函数被调用的,而ender.alive也变成False。

        有一点问题是,我们在ender = weakref.finalize中把s1引用传给了finalize函数了,为了监控对象{1, 2,3}和调用回调,理论上是必须要引用{1,2,3}的,那么为什么最终{1,2,3}被销毁了呢?这是因为finalize持有{1,2,3}的弱引用。

六、弱引用

弱引用可调用,返回所指对象

  

 

首先有一点注意,python控制台会自动把 _ 变量绑定到结果不为None的表达式结果上,因此可能会隐式地增加了新的引用。

  1. 创建一个弱引用
  2. 弱引用是可调用对象,返回值即被引用的对象,需要注意的是因为控制台输出了{0,1},因给其此增加了一个新的引用,即 _
  3. 将a_set不再引用{0,1}集合,引用数量减少了
  4. 调用wref()仍然能返回{0,1},这是因为调用的时候其还有一个引用 _,
  5. 但是当调用wref完成后,控制台输出False,_就绑定到了False上,因此{0,1}被回收。
  6. 上一步{0,1}已被回收,因此这里弱引用为None,弱引用不会妨碍所指对象被当作垃圾回收

        weakref.ref类是低级接口,不要直接使用,应该使用weakref集合(WeakKeyDictionary、WeakValueDictionary、WeakSet)和finalize。

WeakValueDictionary

实现一种可变映射,键是对象的某种可散列的属性,值值对象的弱引用。如果对象被回收,则对应的键会从WeakValueDictionary中删除。

 

  1.  stock是WeakValueDictionary实例
  2.  stock把cheese对象中属性,即奶酪的名称映射到cheese对象的弱引用上
  3.  stock中的所有键,即不同奶酪的名称,此时一共四个
  4.  删除catalog,即cheese对象的列表名称后,stock大多数奶酪都不见了,但是还剩下了一个Parmesan,为什么?原因是在for cheese in catalog这一语句中,cheese是一个全局变量,因此其最后一次循环引用了Parmesan,因此del catalog后,前三个奶酪引用计数都归零,只有Parmesan引用计数为1。

弱引用的局限

不是每个python对象都可以作为弱引用的所指对象。

  • list和dict实例不能作为所指对象,但是他们的子类可以。
  • set可以作为所指对象
  • 自定义类型可以作为所指对象
  • int和tuple不能作为所指对象,其子类也不行+

由于弱引用并不保证引用对象不会被回收,因此如果代码中需要用到循环引用,则可以用弱引用来实现,用弱引用可以解决循环引用不能正常垃圾回收的问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值