zoukankan      html  css  js  c++  java
  • 流畅的python——8 对象引用、可变性和垃圾回收

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

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

    每个 Python 对象都有标识、类型和值。只有对象的值会不时变化。

    作者:其实,对象的类型也可以变,方法只有一种:为 __class__ 属性指定其他类。但这是在作恶,我后悔加上这个脚注了。

    对象 ID 的真正意义在不同的实现中有所不同。在 CPython 中,id() 返回对象的内存地址,但是在其他 Python 解释器中可能是别的值。关键是,ID 一定是唯一的数值标注,而且在对象的生命周期中绝不会变。

    其实,编程中很少使用 id() 函数。标识最常使用 is 运算符检查,而不是直接比较 ID。

    == 与 is

    == 比较两个对象保存的数据,is 比较的是对象的标识。

    is None
    is not None
    

    is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同于 a.__eq__(b)。继承自 object 的 __eq__ 方法比较两个对象的 ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 __eq__方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。

    元组的相对不可变性

    元组与多数 python集合一样,保存的是对象的引用。

    如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。元组中不可变得是元素的标识。

    而 str、bytes、array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身。

    元组的相对不可变性,导致了有些元组不可散列。

    默认做浅复制

    In [17]: l1
    Out[17]: [3, [55, 44], (7, 8, 9)]
    
    In [18]: l2 = list(l1)  # 浅复制
    
    In [19]: l2
    Out[19]: [3, [55, 44], (7, 8, 9)]
    
    In [20]: l1 == l2
    Out[20]: True
    
    In [21]: l1 is l2
    Out[21]: False
    
    In [22]: l3 = l1[:]
    
    In [23]: l3
    Out[23]: [3, [55, 44], (7, 8, 9)]
    
    In [24]: l1 is l3
    Out[24]: False
        
    In [28]: l1[1] = 0  # 地址换了!!!
    
    In [29]: l1
    Out[29]: [3, 0, (7, 8, 9)]
    
    In [30]: l2
    Out[30]: [3, [55, 44], (7, 8, 9)]
    
    In [31]: l3
    Out[31]: [3, [55, 44], (7, 8, 9)]
    
    In [33]: l1 = [3, [55, 44], (7, 8, 9)]
    
    In [34]: l2 = list(l1)  # 浅复制
    
    In [35]: l3 = l1[:]  # 浅复制
    
    In [36]: l1[1].append(333)
    
    In [37]: l2
    Out[37]: [3, [55, 44, 333], (7, 8, 9)]
    
    In [38]: l3
    Out[38]: [3, [55, 44, 333], (7, 8, 9)]
    

    可变对象:+= 是就地加

    不可变对象:+= 是生成一个新的对象,将加的结果赋值给新对象

    为任意对象做深复制和浅复制

    深复制:副本不共享内部对象的引用

    copy.deepcopy 深复制

    copy.copy 浅复制

    class Bus:
        def __init__(self, passengers=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)
    
    In [44]: bus1 = Bus(['a','b'])
    
    In [45]: bus1.p_l
    Out[45]: ['a', 'b']
    
    In [46]: import copy
    
    In [47]: bus2 = copy.copy(bus1)
    
    In [48]: bus3 = copy.deepcopy(bus2)
    
    In [49]: bus2.p_l
    Out[49]: ['a', 'b']
    
    In [50]: bus3.p_l
    Out[50]: ['a', 'b']
    
    In [52]: bus1.pick('ccc')
    
    In [53]: bus1.p_l
    Out[53]: ['a', 'b', 'ccc']
    
    In [54]: bus2.p_l
    Out[54]: ['a', 'b', 'ccc']
    
    In [55]: bus3.p_l
    Out[55]: ['a', 'b']
    

    注意,一般来说,深复制不是件简单的事。如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用

    此外,深复制有时可能太深了。例如,对象可能会引用不该复制的外部资源或单例值。我们可以实现特殊方法 __copy__() __deepcopy__(),控制 copy 和 deepcopy 的行为,详情参见 copy 模块的文档(http://docs.python.org/3/library/copy.html)。

    函数的参数作为引用时

    Python 唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言都采用这一模式,包括 Ruby、Smalltalk 和 Java(Java 的引用类型是这样,基本类型按值传参)。

    共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名

    这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。

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

    没有指定初始值,多个对象会使用默认的同一个列表,相互影响。

    出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

    防御可变参数

    如果定义的函数接受可变参数,应该谨慎考虑调用方是否期望修改传入的值。

    最少惊讶原则

    除非这个方法确实想修改原来的参数对象,否则,就要想一下是否应该修改。如果不确定,创建副本。

    del 和 垃圾回收

    del 语句删除对象的引用,而不是对象。

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

    __del__ 特殊方法,但是它不会销毁实例,不应该在代码中调用。即将销毁实例时,Python 解释器会调用 __del__ 方法,给实例最后的机会,释放外部资源。自己编写的代码很少需要实现 __del__ 代码,有些 Python 新手会花时间实现,但却吃力不讨好,因为 __del__ 很难用对。详情参见 Python 语言参考手册中“DataModel”一章中 del 特殊方法的文档(https://docs.python.org/3/reference/datamodel.html#object.del)。

    在 CPython 中,垃圾回收使用的主要算法是引用计数。

    每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用__del__ 方法(如果定义了),然后释放分配给对象的内存。

    A. Jesse Jiryu Davis 写的“PyPy, Garbage Collection, and a Deadlock”一文(https://emptysqua.re/blog/pypy-garbage-collection-and-a-deadlock/)对 __del__ 方法的恰当用法和不当用法做了讨论。

    弱引用

    正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。

    弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。

    示例展示了如何使用 weakref.ref 实例获取所指对象。如果对象存在,调用弱引用可以获取对象;否则返回 None。

    控制台 :变量 :_ 用于接收没有接收的值。

    WeakValueDictionary 简介

    weakref 模块的文档(http://docs.python.org/3/library/weakref.html)指出,weakref.ref 类其实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说,应该使用 WeakKeyDictionary、WeakValueDictionary、WeakSet 和 finalize(在内部使用弱引用),不要自己动手创建并处理 weakref.ref 实例。我们在示例中那么做是希望借助实际使用 weakref.ref 来褪去它的神秘色彩。但是实际上,多数时候 Python 程序都使用 weakref 集合。

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

    In [94]: zzz = 111
    
    In [95]: for zzz in [1,2]:
        ...:     print(zzz)
        ...:
    1
    2
    
    In [96]: zzz
    Out[96]: 2
    

    弱引用的局限

    不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题:

    class MyList(list):
        """list的子类,实例可以作为弱引用的目标"""
        
    a_list = MyList(range(10))
    # a_list可以作为弱引用的目标
    wref_to_a_list = weakref.ref(a_list)
    

    set 实例可以作为所指对象,因此实例 8-17 才使用 set 实例。用户定义的类型也没问题,这就解释了示例 8-19 中为什么使用那个简单的 Cheese 类。但是,int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行。

    这些局限基本上是 CPython 的实现细节,在其他 Python 解释器中情况可能不一样。这些局限是内部优化导致的结果。

    python 对不可变类型施加的把戏

    元组 t 来说,t[:] 不创建副本,而是返回同一个对象的引用。此外,tuple(t) 获得的也是同一个元组的引用

    In [100]: a = [1,2,3]
    
    In [102]: b = a[:]
    
    In [103]: a is b  # 列表是浅拷贝
    Out[103]: False
    
    In [104]: c = (1,2,3)
    
    In [105]: d = tuple(c)
    
    In [106]: d
    Out[106]: (1, 2, 3)
    
    In [107]: c is d  # 元组就是同一个元组的引用
    Out[107]: True
    
    In [108]: d = c[:]
    
    In [109]: d is c
    Out[109]: True
    
    In [110]: d = c[1:]
    
    In [111]: d is c
    Out[111]: False
    

    str、bytes 和 frozenset 实例也有这种行为。注意,frozenset 实例不是序列,因此不能使用 fs[:](fs 是一个 frozenset 实例)。但是,fs.copy() 具有相同的效果:它会欺骗你,返回同一个对象的引用,而不是创建一个副本

    In [112]: a = 'aaa'
    
    In [113]: b = a[:]
    
    In [114]: a is b
    Out[114]: True
    
    In [115]: c = copy.copy(a)
    
    In [116]: c is a
    Out[116]: True
    
    In [117]: d = copy.deepcopy(a)
    
    In [118]: d is a
    Out[118]: True
    

    copy 方法不会复制所有对象,这是一个善意的谎言,为的是接口的兼容性:这使得 frozenset 的兼容性比 set 强。

    两个不可变对象是同一个对象还是副本,反正对最终用户来说没有区别。

    共享字符串字面量是一种优化措施,称为驻留(interning)。CPython 还会在小的整数上使用这个优化措施,防止重复创建“热门”数字,如 0、-1 和 42。注意,CPython 不会驻留所有字符串和整数,驻留的条件是实现细节,而且没有文档说明。

    千万不要依赖字符串或整数的驻留!比较字符串或整数是否相等时,应该使用 ==,而不是 is。驻留是 Python 解释器内部使用的一个特性。

    本节讨论的把戏,包括 frozenset.copy() 的行为,是“善意的谎言”,能节省内存,提升解释器的速度。别担心,它们不会为你带来任何麻烦,因为只有不可变类型会受到影响。或许这些细枝末节的最佳用途是与其他 Python 程序员打赌,提高自己的胜算。

    可以在自己的类中定义 __eq__ 方法,决定 == 如何比较实例。如果不覆盖__eq__ 方法,那么从 object 继承的方法比较对象的 ID,因此这种后备机制认为用户定义的类的各个实例是不同的。

    处理不可变的对象时,变量保存的是真正的对象还是共享对象的引用无关紧要。如果 a == b 成立,而且两个对象都不会变,那么它们就可能是相同的对象。这就是为什么字符串可以安全使用驻留。仅当对象可变时,对象标识才重要。

  • 相关阅读:
    [最优化理论与技术]线性规划
    [吴恩达深度学习]神经网络和深度学习
    Linux系统级性能分析工具perf的介绍与使用
    Oracle表变化趋势追踪记录 & 表 历史统计信息查看
    delete noprompt archivelog
    DEPLOYING ORACLE RAC DATABASE 12C RELEASE 2 ON RED HAT ENTERPRISE LINUX 7
    How to Modify SCAN Setting or SCAN Listener Port after Installation (Doc ID 972500.1)
    How to create a RAC Database Service With Physical Standby Role Option? (Doc ID 1129143.1)
    Oracle级联备库0数据丢失--重建控制文件并应用主库online redo logfile的激活方法
    Handling ORL and SRL (Resize) on Primary and Physical Standby in Data Guard Environment (Doc ID 1532566.1)
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15344595.html
Copyright © 2011-2022 走看看