• 对象引用、可变性和垃圾回收


    《流畅的Python》卢西亚诺·拉马略 第8章 读书笔记

    8.1 变量不是盒子

    为了理解Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。
    因为变量只不过是标注,所以无法阻止为对象贴上多个标注。

    8.2 标识、相等性和别名

    每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。
    ==运算符比较两个对象的值(对象中保存的数据)
    is比较对象的标识,id()函数返回对象标识的整数表示

    最常使用is检查变量绑定的值是否为None
    x is None​​
    x is not None​​

    is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。继承自object的__eq__方法比较两个对象的ID,结果与is一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。

    8.3 默认做浅复制

    构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。

    为任意对象做深复制和浅复制
    有时我们需要的是深复制(即副本不共享内部对象的引用)。copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。为了演示copy() 和deepcopy() 的用法,下例定义了一个简单的类,Bus。这个类表示运载乘客的校车,在途中乘客会上车或下车。
    【例】校车乘客在途中上车和下车
    ​​

    1. class Bus:
    2. def __init__(self, passengers=None):
    3. if passengers is None:
    4. self.passengers = []
    5. else:
    6. self.passengers = list(passengers)
    7. def pick(self, name):
    8. self.passengers.append(name)
    9. def drop(self, name):
    10. self.passengers.remove(name)

    交互式控制台中 创建一个Bus实例和两个副本

    1. >>> import copy
    2. >>> from Bus import Bus
    3. >>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
    4. >>> bus2 = copy.copy(bus1)
    5. >>> bus3 = copy.deepcopy(bus1)
    6. >>> id(bus1), id(bus2), id(bus3)
    7. (140137100109976, 140137100110144, 140137100110312)
    8. >>> bus1.drop('Bill')
    9. >>> bus1.passengers
    10. ['Alice', 'Claire', 'David']
    11. >>> bus2.passengers
    12. ['Alice', 'Claire', 'David']
    13. >>> bus3.passengers
    14. ['Alice', 'Bill', 'Claire', 'David']
    15. >>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
    16. (140137100086024, 140137100086024, 140137136242952)
    17. >>>

    说明:
    line4, bus2是bus1的浅复制
    line5, bus3是bus1的深复制
    line8, bus1中的'Bill'下车
    line12, bus2中也没有'Bill'
    line15, 审查passengers属性后发现,bus1和bus2共享同一个列表对象

    8.4 函数的参数作为引用时

    8.4.1 不要使用可变类型作为参数

    以例1为基础定义一个新类HauntedBus,然后修改__init__方法。passengers的默认值不是None,而是[],这样就不用像之前那样使用if判断了。这个“聪明的举动”会让我们陷入麻烦。
    【例2】一个简单的类,说明可变默认值的危险

    1. class HauntedBus:
    2. """备受幽灵乘客折磨的校车"""
    3. def __init__(self, passengers=[]):
    4. self.passengers = passengers
    5. def pick(self, name):
    6. self.passengers.append(name)
    7. def drop(self, name):
    8. self.passengers.remove(name)

    line3  如果没传入passengers参数,使用默认绑定的列表对象,一开始是空列表。
    line4  这个赋值语句把self.passengers变成passengers的别名,而没有传入passengers参数时,self.passengers是默认列表的别名。
    line6、8  在self.passengers上调用.append()和.remove()方法时,修改的其实是默认列表,它是函数对象的一个属性。
    HauntedBus的诡异行为 --> 

    1. >>> from Bus import HauntedBus
    2. >>> bus1 = HauntedBus(['Alice', 'Bill'])
    3. >>> bus1.passengers
    4. ['Alice', 'Bill']
    5. >>> bus1.pick('Charlie')
    6. >>> bus1.drop('Alice')
    7. >>> bus1.passengers
    8. ['Bill', 'Charlie']
    9. >>>
    10. >>> bus2 = HauntedBus()
    11. >>> bus2.pick('Carrie')
    12. >>> bus2.passengers
    13. ['Carrie']
    14. >>>
    15. >>> bus3 = HauntedBus()
    16. >>> bus3.passengers
    17. ['Carrie']
    18. >>> bus3.pick('Dave')
    19. >>> bus3.passengers
    20. ['Carrie', 'Dave']
    21. >>>
    22. >>> bus2.passengers
    23. ['Carrie', 'Dave']
    24. >>> bus2.passengers is bus3.passengers
    25. True
    26. >>> bus1.passengers
    27. ['Bill', 'Charlie']
    28. >>>

    说明:

    line7 目前没什么问题,bus1没有出现异常。
    line10 一开始,bus2是空的,因此把默认的空列表赋值给self.passengers。
    line15 bus3一开始也是空的,因此还是赋值默认的列表。
    line16 但是默认列表不为空!
    line22 登上bus3的Dave出现在bus2中。
    line24 问题是,bus2.passengers和bus3.passengers指代同一个列表。
    line26 但bus1.passengers是不同的列表。

    问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。
    实例化HauntedBus时,如果传入乘客,会按预期运作。
    但是不为HauntedBus指定初始乘客的话,self.passengers变成了passengers参数默认值的别名。

    审查HauntedBus.__init__对象,看看它的__defaults__属性中的那些幽灵学生:

    1. >>> dir(HauntedBus.__init__)
    2. ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
    3. >>>
    4. >>> HauntedBus.__init__.__defaults__
    5. (['Carrie', 'Dave'],)
    6. >>>

    我们可以验证bus2.passengers是一个别名,它绑定到HauntedBus.__init__.__defaults__属性的第一个元素上:

    1. >>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
    2. True
    3. >>>


    可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值
    在例1中,__init__方法检查passengers参数的值是否为None,如果是就把一个新的空列表赋值给self.passengers。如果passengers不是None,正确的实现会把passengers的副本赋值给self.passengers

    8.4.2 防御可变参数

    在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers

    除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。

    8.5 del和垃圾回收

    del语句删除名称(对象的引用),而不是对象。
    对象才会在内存中存在是因为有引用。当对象的引用数量归零后,垃圾回收程序会把对象销毁。

    仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时,del命令会导致对象被当作垃圾回收。
    重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。

    8.6 弱引用

    有时需要引用对象,而不让对象存在的时间超过所需时间,这经常用在缓存中。
    弱引用不会增加对象的引用数量  ==>  弱引用不会妨碍所指对象被当作垃圾回收

    引用的目标对象称为所指对象(referent),如果所指对象不存在了,返回None

    1. >>> import weakref
    2. >>> a_set = {0, 1}
    3. >>> wref = weakref.ref(a_set) #创建弱引用对象wref
    4. >>> wref
    5. <weakref at 0x7f86f289af98; to 'set' at 0x7f86f0b799e8>
    6. >>> wref() #调用wref()返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以{0, 1}会绑定给_变量
    7. {0, 1}
    8. >>> _
    9. {0, 1}
    10. >>>
    11. >>> a_set = {2, 3, 4} #a_set不再指代{0, 1}集合,因此集合的引用数量减少了。但是_变量仍然指代它。
    12. >>>
    13. >>> _
    14. {0, 1}
    15. >>> wref() #调用wref()依旧返回{0, 1}。
    16. {0, 1}
    17. >>>
    18. >>> wref() is None #计算这个表达式时,{0, 1}存在,因此wref()不是None。
    19. False
    20. >>> _ #随后_绑定到结果值False。现在{0, 1}没有强引用了。
    21. False
    22. >>>
    23. >>> wref() is None #因为{0, 1}对象不存在了,所以wref()返回None。
    24. True
    25. >>>

    说明:多数程序最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用),不要自己动手创建并处理weakref.ref实例。

    8.6.1 WeakValueDictionary简介

    WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其它地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。因此,WeakValueDictionary经常用于缓存。

    1. class Cheese:
    2.     def __init__(self, kind):
    3.         self.kind = kind
    4.     def __repr__(self):
    5.         return 'Cheese(%r)'%self.kind

    把catalog中的各种奶酪载入WeakValueDictionary实现的stock中。删除catalog后,stock中只剩下一种奶酪了。你知道为什么帕尔马干酪(Parmesan)比其他奶酪保存的时间长吗?代码后面的提示中有答案。
    交互式环境测试:

    1. >>> from Cheese import Cheese
    2. >>> import weakref
    3. >>> stock = weakref.WeakValueDictionary()   #stock是WeakValueDictionary实例
    4. >>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),Cheese('Brie'), Cheese('Parmesan')]   
    5. >>> for cheese in catalog:
    6. ...     stock[cheese.kind] = cheese   #stock把奶酪的名称映射到catalog中Cheese实例的弱引用上
    7. >>> sorted(stock.keys())
    8. ['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']   #stock是完整的。
    9. >>> del catalog
    10. >>> sorted(stock.keys())
    11. ['Parmesan']   #删除catalog之后,stock中的大多数奶酪都不见了,这是WeakValueDictionary的预期行为。为什么不是全部呢?
    12. >>>
    13. >>> del cheese
    14. >>> sorted(stock.keys())
    15. []
    16. >>>

    临时变量引用了对象,这可能会导致该变量的存在时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。但上述举例中,for循环中的变量cheese是全局变量,除非显式删除,否则不会消失。

    与WeakValueDictionary对应的是WeakKeyDictionary,后者的键是弱引用。

    8.6.2 弱引用的局限

    不是每个Python对象都可以作为弱引用的目标(或称所指对象)。
    基本的list和dict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题;
    set实例可以作为所指对象,用户定义的类型也没问题;
    int和tuple实例不能作为弱引用的目标,甚至它们的子类也不行。

  • 相关阅读:
    计算机毕业设计 高校实习信息发布网站的设计与实现 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试
    FastReport .NET 2023.3.10 Crack
    cuML机器学习GPU库
    seacms_CNVD-2020-22721_v10.1漏洞分析与复现
    【Spark NLP】第 9 章:信息提取
    SpringBoot使用Maven整合minio实现静态资源和对象的存储;解决与okhttp依赖冲突问题
    ROS 学习笔记4.TF变换
    mybatisPlus不能赋null值
    安卓教材学习
    2022年 - 年中总结
  • 原文地址:https://blog.csdn.net/wy_hhxx/article/details/125594176