zoukankan      html  css  js  c++  java
  • python对象引用和垃圾回收

    变量="标签"

    变量a和变量b引用同一个列表:

    >>> a = [1, 2, 3]
    >>> b = a
    >>> a.append(4)
    >>> b
    [1, 2, 3, 4]

    使用"标签"很形象的解释了变量    =========>   列表[1, 2, 3]是一个物品,而a和b都是给这个物品贴上的标签。因此,改变a的内容,b的内容也改变了。

    "is"和"=="

    有一个人叫做李华,1997年生,身体情况工作信息记录为info,有个小名叫"小华"。

    >>> lihua = {'name':'lihua','born':'1997','information':'info'}
    >>> xiaohua = lihua
    >>> xiaohua is lihua
    True
    >>> id(xiaohua),id(lihua)
    (2072419437304, 2072419437304)
    >>> xiaohua['information'] = 'new_info'
    >>> lihua
    {'name': 'lihua', 'born': '1997', 'information': 'new_info'}

    可见xiaohua和lihua指代同一个对象,假如有个冒充者(李华)说他是李华,身份信息一模一样,记为anony。

    >>> anony = {'name': 'lihua', 'born': '1997', 'information': 'new_info'}
    >>> anony == lihua
    True
    >>> anony is lihua
    False

    此时使用"is"和"=="判断结果是不同的。lihua和xiaohua绑定同一个对象,xiaohua是lihua的别名;而lihua和anony绑定不同对象。

    "=="比较的是对象的值,而"is"比较对象的标识。

    在Python中,对象的标识就是id()函数返回值,而is比较的就是这个返回值的整数表示。在Cpython中,id()返回的是对象的内存地址,在其他Python解释器中可能是别的值。最主要的是,id()函数返回值在对象的生命周期中一定不会改变

    写程序是一般关注值,因此==出现频率较高,而在变量和单例值之间比较时应该使用is。除此之外,is运算符比==快,因为它不能重载,解释器不需要寻找并调用特殊方法,直接比较整数id;a==b是语法糖,等同于a.__eq__(b),继承自object的__eq__方法比较两个对象的id,结果与is一样,而覆盖__eq__方法后结果可能就与is结果不同了。

    元组是"可变的"

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

    t1 = (1, 2, [30, 40])
    t2 = (1, 2, [30, 40])
    print(t1 == t2)
    True
    
    print(id(t1[-1]))
    3031106993224                  #标识
    
    t1[-1].append(50)
    print(t1)
    (1, 2, [30, 40, 50])              #修改t1[-1]列表
    
    print(id(t1[-1]))
    3031106993224                   #标识没变
    
    print(t1 == t2)
    False                                    #值改变了,不相等

    浅复制与深复制

    默认作浅复制

    复制列表最简单的方式是使用内置类型的构造方法,以list为例:

    l1 = [3, [40, 50], (6, 7, 8)]
    l2 = list(l1)
    print(l2)
    [3, [40, 50], (6, 7, 8)]
    
    print(l2 == l1)
    True                    #副本与原副本相等
    
    print(l2 is l1)
    False                   #副本与原副本指代不同的对象

    当然,可变序列都可以用 [:] 来复制,而无论是构造方法还是 [:] 复制都是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用),如果元素是可变的,就会出现问题。

    l1 = [3, [40, 50], (6, 7, 8)]
    l2 = list(l1)
    l1.append(99)
    l1[1].remove(50)
    print('l1:', l1)
    print('l2:', l2)
    l2[1] += [22, 33]
    l2[2] += (9, 10)
    print('l1:', l1)
    print('l2:', l2)
    
    
    #结果
    l1: [3, [40], (6, 7, 8), 99]           
    l2: [3, [40], (6, 7, 8)]                #对比l1,由于浅复制,追加99对l2无影响,而对元组l1里面的可变对象[40, 50]执行删除操作却影响到了l1,说明l2和l1绑定同一个列表
    l1: [3, [40, 22, 33], (6, 7, 8), 99]
    l2: [3, [40, 22, 33], (6, 7, 8, 9, 10)]   #+=操作就地修改列表,因此l2与l1同时被修改,而+=对于元组这种不可变对象来说,会重新创建一个元组,重新绑定给l2,修改后,l2中的那个元组与l1中的不是同一个
    
    
    print(id(l1[2]))
    print(id(l2[2]))
    
    print(id(l1[2]))
    print(id(l2[2]))
    
    #替换为打印id,发现l2的元组id最后改变了
    2377750817600
    2377750817600
    2377750817600
    2377749721512

    浅复制容易,但有时会出现不想要也很意外的结果,就需要深复制。

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

    copy模块提供copy用于浅复制和deepcopy用于深复制(副本不共享内部对象的引用)。

    定义一个类bus表示校车,有乘客上车下车方法:

    import 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)
    
    
    bus1 = Bus(['Alice', 'Bob', 'David'])   #原校车
    bus2 = copy.copy(bus1)            #copy方法复制的校车
    bus3 = copy.deepcopy(bus1)        #deepcopy方法复制的校车
    
    print(id(bus1), id(bus2), id(bus3))
    bus1.drop('David')            #bus1的David下车
    print(bus2.passengers)
    
    print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
    print(bus3.passengers)
    
    #结果
    2765338083960 2765338084072 2765338083456   #三个不同的Bus对象
    ['Alice', 'Bob']                      #bus2的David消失了
    2765338349448 2765338349448 2765337978376   #bus1,bus2共享同一个列表对象,bus3则有另一个列表
    ['Alice', 'Bob', 'David']             #bus3没有改变

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

    from copy import deepcopy
    
    a = [10, 20]
    b = [a , 30]
    a.append(b)
    print(a)
    c = deepcopy(a)
    print(c)
    
    
    [10, 20, [[...], 30]]
    [10, 20, [[...], 30]]

    深复制有时处理得太深,对象可能会引用不该复制的外部资源或单例值,此时可以实现特殊方法__copy__()和__deepcopy__(),控制copy和deepcopy的行为。

     函数的参数作为引用

    python唯一支持的方式是共享传参。类似于java的引用传参。它是指函数各个形式参数获得实参中各个引用的副本,即函数内部形参是实参的别名

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

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

    可选参数可以有默认值,但应该避免使用可变对象作为参数默认值。如果使用可变参数,后果见例子:

    定义一辆校车,passenger默认值不用None而用[ ]

    class Bus:
    
        def __init__(self, passengers=[]):
            self.passengers = passengers
    
        def pick(self, name):
            self.passengers.append(name)
    
        def drop(self, name):
            self.passengers.remove(name)
    
    
    bus1 = Bus(['Alice', 'Bob'])      #1车原两人
    print(bus1.passengers)
    bus1.pick('Jane')
    bus1.drop('Alice')
    print(bus1.passengers)             #上一人下一人
    
    bus2 = Bus()                         #2车空车
    bus2.pick('David')
    print(bus2.passengers)           #上一人
    
    bus3 = Bus()                          #3车空车
    bus3.pick('Mike')
    print(bus2.passengers)            #上一人
    
    print(bus2.passengers is bus3.passengers)
    print(bus1.passengers)

    奇怪的现象出现了:

    ['Alice', 'Bob']
    ['Bob', 'Jane']
    ['David']
    ['David', 'Mike']
    True
    ['Bob', 'Jane']

    1车正常行驶,3车出现”幽灵学生“,上二车的David出现在了3车。事实上,可看到bus2和bus3引用的是同一个乘客列表。

    实例化Bus时,如果传入乘客可以正常运作,但是不为Bus指定乘客的话,奇怪的事情发生,这是因为self.passengers变成了passengers参数默认值的别名。默认值在定义函数时计算,因而默认值变为了函数对象的属性,如果默认值是可变对象,那么后续函数调用都会受到影响。审查Bus.__init__对象

    print(Bus.__init__.__defaults__)
    #'David', 'Mike'成为默认乘客
    (['David', 'Mike'],)

    所以,如果定义的函数接受可变参数,应该慎重考虑调用方是否期望修改传入的参数。例如校车写成深复制那一节的样子。

    del和垃圾回收

    del语句删除名称,或者说删除标签。(删除一个物品的标签,而不是删除这个物品)del命令可能导致对象被当作垃圾回收,即满足下列条件之一时:

    1.删除的变量保存的是对象的最后一个引用

    2.无法得到对象

    重新绑定也可能会导致对象的引用数量归零,导致对象销毁。

    python采用引用计数算法来进行垃圾回收,每个对象都会统计有多少个引用指向自己,当引用计数器归零时,对象就立即销毁。python2.0采用分代垃圾回收算法,用于处理循环引用。

    见下例:

    import weakref
    
    s1 = {1, 2, 3}
    s2 = s1
    
    def bye():
        print('bye')
    
    ender = weakref.finalize(s1, bye)   #注册一个回调函数,在{1,2,3}销毁时使用
    print(ender.alive)
    del s1
    print(ender.alive)
    s2 = 'helloworld'
    print(ender.alive)
    
    #结果发现del s1后,对象仍然存活,而s2重新绑定了对象,于是无法获取对象,导致对象被销毁
    True
    True
    bye
    False

    弱引用

    有引用时对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。

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

    python提供weakref模块来控制弱引用。

    weakref.ref

    import weakref
    import sys
    
    set1 = {1, 2}
    print(sys.getrefcount(set1))         #打印引用计数
    wref = weakref.ref(set1)             #创建弱引用
    print(wref)                          #打印弱引用 
    print(sys.getrefcount(wref))
    set2 = wref()                         #!!!弱引用时可调用对象,返回的是被引用的对象,若所指对象不存在则返回None 
    print(set2 is set1)
    print(sys.getrefcount(set1))          
    set1 = None
    set2 = None
    print(wref)  

    结果:

    2
    <weakref at 0x0000024BADFA0408; to 'set' at 0x0000024BADEE99E8>
    2
    True
    3            #调用弱引用返回被引用对象绑定到set2,所以引用显示为3                                           
    <weakref at 0x0000024BADFA0408; dead>      #弱引用失效

    初始引用为2的原因是:当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用

    weakref.WeakValueDictionary

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

    import weakref
    
    class Cheese:
        def __init__(self, kind):
            self.kind = kind
    
        def __repr__(self):
            return 'Cheese(%r)' % self.kind
    
    stock = weakref.WeakValueDictionary()
    catalog = [Cheese('A'), Cheese('B'), Cheese('C'), Cheese('D'), Cheese('E'), Cheese('A')]
    for cheese in catalog:
        stock[cheese.kind] = cheese
    
    print(sorted(stock.keys()))
    del catalog
    print(sorted(stock.keys()))
    del cheese
    print(sorted(stock.keys()))

    结果:

    ['A', 'B', 'C', 'D', 'E']
    ['A']
    []

    删除引用后['A']奶酪还在,是因为临时变量引用了对象,这可能导致该变量存在的时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。示例中是全局变量,需显式删除才会消失。

    Weak模块还有proxy,WeakSet,WeakKeyDictionary等

    //proxy(obj[,callback])函数来创建代理对象。使用代理对象就如同使用对象本身一样,而不需要像ref那样显示调用

    //WeakKeyDictionary的键是弱引用,它的实例可以为应用中其他部分拥有的对象附加元数据,这样就无需为对象添加属性

    //WeakSet类保存元素弱引用的集合类,元素没有强引用时,集合会把它删除

    以上来自《流畅的python》第8章

  • 相关阅读:
    磁盘分区异常占用满了
    平滑升级nginx
    supervisor进程异常挂掉
    datetime值毫秒四舍五入
    docker+tomcat 启动时非常慢原因之JRE /dev/random阻塞
    Tomcat最大连接数问题
    Docker:设置代理proxy
    easy_install和pip安装python库修改默认的源
    zabbix监控mysql之Warning: Using a password on the command line interface can be insecure.
    Mysql忘记密码解决方法
  • 原文地址:https://www.cnblogs.com/lht-record/p/10279939.html
Copyright © 2011-2022 走看看