zoukankan      html  css  js  c++  java
  • Fluent_Python_Part4面向对象,08-ob-ref,对象引用、可变性和垃圾回收

    第四部分第8章,对象引用、可变性和垃圾回收

    1. 创建对象之后才会把变量分配给对象

    变量是对象的标注,是对象的别名,是对象的引用,并不是对象存储的地方。

    例子1. 证明赋值语句的右边先执行

    class Gizmo():
        def __init__(self):
            print('Gizmo id: %d' % id(self))
    
    x = Gizmo()
    #这里表明,在尝试求积之前会创建一个新的Gizmo实例。
    y = Gizmo() * 10
    
    print(dir())
    

    2. 标识(id())、相等性(==)和别名

    例子1. 两个变量指向同一个对象

    charles = {'name': 'Charles L. Dodgson', 'born': 1832}
    
    lewis = charles
    
    print(id(charles), id(lewis))
    print(charles is lewis)
    
    lewis['balance'] = 950
    print(charles)
    

    例子2. chrles和lewis绑定同一个对象,alex绑定另外一个具有相同内容的对象

    charles = {'name': 'Charles L. Dodgson', 'born': 1832}
    
    alex = {'name': 'Charles L. Dodgson', 'born': 1832}
    
    print(id(charles), id(alex))
    #dict类的__eq__方法实现了==运算符
    print(charles == alex)
    print(charles is alex)
    

    例子1和例子2中,charles和lewis是别名,即两个变量绑定同一个对象。而alex不是charles的别名,因为二者绑定的是不同的对象。alex和charles绑定的对象具有相同的值(==比较的是值),但它们的标识(id)不同。

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

    3. ==和is

    ==运算符比较两个对象的(对象中保存的数据),is运算符比较对象的标识。
    通常我们关注的是值,不是id,所以Python中出现的频率比is高。
    is运算符比
    速度快。 a == b是语法糖,等同于a.eq(b)。继承自object的__eq__方法比较两个对象的id,结果与is一样。多数内置类型覆盖了__eq__方法,所以相等性测试可能涉及大量处理工作,例如比较大型集合或嵌套层级深的结构时。

    4. 元组tuple的相对不可变性

    1. 元组与多数Python的collections(list、dict、set等等)一样,保存的是对象的引用。
    2. 元组的不可变性是值tuple本身不可变,元素依然可变。
    3. 而str、bytes和array.array等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。

    例子1. 元组中不变的是元素的标识(id),元组的值会随着引用的可变对象的变化而变

    t1 = (1, 2, [30, 40])
    t2 = (1, 2, [30, 40])
    
    #t1和t2是两个不同的对象,标识不同,但是值相同
    print(id(t1), id(t2))
    print(t1 == t2)
    
    #print('id(t1)', id(t1))
    print(id(t1[-1]))
    #t1[-1]为列表的引用,可以修改其中的值
    t1[-1].append(50)
    print(id(t1[-1]))
    print(t1)
    #print('id(t1)', id(t1))
    

    5. 浅复制(shallow copy)

    例子1. 构造方法list和[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)

    l1 = [3, [55,44], (7,8,9)]
    l2 = list(l1)
    
    print(l1 == l2, id(l1), id(l2))
    
    l3 = l2[:]
    print(l3 == l2, id(l3), id(l2))
    

    浅复制,如果所有元素都是不可变的,那么这样没有问题,且节省内存。但是,如果有可变的元素,可能会导致意想不到的问题。

    例子2. 一个包含另外一个列表和一个元组的列表做浅复制,再做些修改,看看影响。

    #可以在Python Tutor网站查看过程
    l1 = [3, [66, 55, 44], (7, 8, 9)]
    # 1.list()构造方法是浅复制
    l2 = list(l1)
    # 2.浅复制复制的是最外层容器,保留源容器中元素的引用,所以把100追加到l1中,对l2没有影响。
    l1.append(100)
    # 3.l1[1]与l2[1]绑定的列表是同一个
    l1[1].remove(55)
    print('l1:', l1)
    print('l2:', l2)
    # 4.对列表来说,+=运算符就地修改列表。
    print('id(l2[1],列表): ', id(l2[1]))
    l2[1] += [33, 22]
    print('id(l2[1],列表): ', id(l2[1]))
    print('id(l2[2],元组): ', id(l2[2]))
    # 5.对元组来说,+=运算符创建一个新的元组,然后重新绑定给变量。
    l2[2] += (10, 11)
    print('id(l2[2],元组): ', id(l2[2]))
    print('l1:', l1)
    print('l2:', l2)
    

    对列表来说,+=运算符就地修改列表。对元组来说,+=运算符创建一个新的元组,然后重新绑定给变量。

    6.深复制(Deep copy)

    1. 有时候我们需要的是深复制(即副本不共享内部对象的引用)
    2. copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。

    例子1. 演示copy()和deepcopy()的用法,定义了Bus类。

    class Bus:
        """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)
    

    创建一个Bus类的实例bus1,一个浅复制副本bus_shallow, 一个深复制副本bus_deep

    class Bus:
        """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)
    
    import copy
    bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
    #copy()浅复制
    bus_shallow = copy.copy(bus1)
    #deepcopy()深复制
    bus_deep = copy.deepcopy(bus1)
    #bus1和bus_shallow元素中passenger是同一个列表的引用,而bus_deep中passengers是指向另一个列表
    bus1.drop('Claire')
    print(bus_shallow.passengers)
    print(bus_deep.passengers)
    
    print(id(bus1.passengers), id(bus_shallow.passengers), id(bus_deep.passengers))
    

    例子2.deepcopy解决循环引用问题
    如果对象有循环引用,那么这个算法会进入无限循环。deepcopy函数会想办法复制对象,解决循环引用。

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

    通过别名共享对象还能解释Python中传递参数的方式,以及使用可变类型作为参数默认值引起的问题。接下来讨论这些问题。

    7. 函数的参数作为引用的时候

    1. 基本类型按值传参,引用类型是共享传参(call by sharing)。
    2. 共享传参指函数的形式参数获得实参中引用的副本。也就是说,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的id(不能把一个对象替换成另外一个对象)。

    例子1. 函数可能会修改接收到的任何可变对象
    本例子用+=运算符,实际传入的实参可能会受到影响

    def f(a, b):
        a += b
        return a
    #数字没变
    x = 1
    y = 2
    print(f(x,y), x, y)
    #列表变了
    a = [1, 2]
    b = [3, 4]
    print(f(a, b), a, b)
    #元组没变
    t = (10, 20)
    u = (30, 40)
    print(f(t, u), t, u)
    

    例子2. 说明可变默认值的危险,类HauntedBus(幽灵巴士),从Bus类而来。
    不要使用可变类型作为参数的默认值
    题外话:可选参数可以有默认值,这是Python函数定义的一个很棒的特性,这样API在进化的同时能保证向后兼容。

    class HauntedBus:
        """备受幽灵乘客折磨的校车"""
    
        def __init__(self, passengers=[]):
            self.passengers = passengers
    
        def pick(self, name):
            self.passengers.append(name)
    
        def drop(self, name):
            self.passengers.remove(name)
    
    #使用默认值的bus1和bus2相互影响
    bus1 = HauntedBus()
    bus1.pick('Bill')
    bus2 = HauntedBus()
    print(bus2.passengers)
    bus2.pick('Allen')
    print(bus1.passengers)
    

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

    例子3. 查看例子2中的函数对象的属性,证明默认值变成了函数对象的属性

    class HauntedBus:
        """备受幽灵乘客折磨的校车"""
    
        def __init__(self, passengers=[]):
            self.passengers = passengers
    
        def pick(self, name):
            self.passengers.append(name)
    
        def drop(self, name):
            self.passengers.remove(name)
    
    bus1 = HauntedBus()
    bus1.pick('Bill')
    bus2 = HauntedBus()
    print(bus2.passengers)
    bus2.pick('Allen')
    print(bus1.passengers)
    
    print(dir(HauntedBus.__init__))
    #输出(['Bill', 'Allen'])
    print(HauntedBus.__init__.__defaults__)
    
    #bus2.passengers是一个别名,绑定到HauntedBus.__init__.__defaults__属性的第一个元素上
    print(HauntedBus.__init__.__defaults__[0] is bus2.passengers)
    

    可变默认值导致的问题说明了为什么通常使用None作为接受可变值的参数的默认值。

    例子4. TwilightBus,下车的学生从篮球队中消失了。

    class TwilightBus:
        """让乘客销声匿迹的校车"""
    
        def __init__(self, passengers=None):
            if passengers is None:
                self.passengers = []
            else:
                self.passengers = passengers
    
        def pick(self, name):
            self.passengers.append(name)
    
        def drop(self, name):
            self.passengers.remove(name)
    

    这里的问题是,校车为传给构造方法的列表创建了别名,即self.passengers = passengers。正确的做法是,校车自己维护乘客列表。 方法是在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers,即self.passengers = list(passengers)代替self.passengers = passengers。

    def __init__(self, passengers):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)
    
    1. list()构造函数创建了另外一个对象,维护相同值,这个就是副本。注意,tuple(t)获得的是同一个对象。这些是CPython实现的细节。
    2. 更加灵活。传给passengers参数的值可以是元组或者其他可迭代对象。

    8. del和垃圾回收

    1. del语句删除名称,而不是对象。
    2. del命令可能会导致对象被当作垃圾回收,仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。
    3. 重新绑定也可能导致对象的引用计数归零,导致对象被回收。
    4. 有个__del__特殊方法。即将销毁实例时,Python解释器会调用__del__方法,给实例最后的机会,释放资源(如回收内存)。自己很少需要实现__del__方法,因为很难用对。
    5. 在CPython中,垃圾回收使用的主要算法是引用计数。但Python的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数。这意味着,对象的引用数为0时可能不会立即调用__del__方法。

    例子1. 演示对象生命结束时的情形。并说明del不会删除对象。对象不可获取,从而被删除。

    import weakref
    s1 = {1, 2, 3}
    s2 = s1
    
    def bye():
        print('Gone with the wind')
    
    wek_ref = weakref.finalize(s1, bye)
    print(wek_ref.alive)
    del s1
    print(wek_ref.alive)
    
    s2 = 'spam'
    print(wek_ref.alive)
    

    我们把s1引用传给finalize函数了,虽然s1,s2都不指向{1, 2, 3}了,为什么{1, 2, 3}对象被销毁了?
    这是因为finalize持有{1, 2, 3}的弱引用,为了监控对象和调用回调。

    9. 弱引用

    弱引用不会增加对象的引用数量。
    弱引用不会妨碍所指对象被当做垃圾回收。
    如果弱引用指向的对象死亡,弱引用就返回None。
    弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。

    例子1. 弱引用是可调用对象,返回的是被引用的对象;如果所指对象不存在了,返回None

    Note: 这里隐式赋值给_, _是强引用。
    例子1的wearef.ref是底层接口,这里为了做演示。多数程序最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用),不要自己手动创建并处理weakref.ref实例。
    WeakValueDictionary简介:中文电子书P370

    弱引用的局限:中文电子书P372

    10. Python对不可变类型施加的把戏。

    这是CPython的实现细节。中文电子书P374。

    1. list(l1),l1[:]返回的都是一个新的对象,tuple(t1)得到同一个元组。
    2. str、bytes、fronzenset实例也有这种行为。
    3. 注意,frozenset实例不是序列,因此不能使用fs[:]。但是,fs.copy()是善意的谎言,返回同一个对象的引用而不是创建一个副本。为的是接口的兼容性,节省内存,提高解释器的速度,使得fronzenset的兼容性比set强。
    4. 两个不可变对象是同一个对象还是副本,对用户来说没什么区别。

    另外,

    总结:

    变量保存的是引用,这一点对Python编程有很多实际的影响。

    1. 简单的赋值不创建副本。
    2. 对+=或*=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,就原地修改。
    3. 为现有的变量赋予新值,不会修改之前绑定的变量。这就重新绑定:变量绑定了其他的对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
    4. 函数的参数以引用的形式传递。这意味着,函数可能会修改通过参数传入的可变对象。解决与否看需求。如果不想被修改,在函数本地创建副本,或者传入的参数使用不可变对象。(例如传入元组而不是列表)。
    5. 使用可变类型作为函数参数的默认值有危险,因为如果使用默认值初始化实例,在实例中修改了参数,会影响到以后再使用默认值初始化的实例的调用。
    6. 在CPython中,对象的引用数量归零后和除了循环引用之外没有其他引用,两个对象都会被销毁。
    7. 某些情况下,可能需要保存对象的引用,但不保存对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一种底层机制,是weakref模块中WeakValueDictionary、WeakKeyDictionary和WeakSet等有用的集合类以及weakref.finalize函数的底层支持。
    8. 在Python中比较的是对象的值, is才是比较对象的引用或者标识(id)。而在Java中,比较的是对象(不是基本类型)的引用。
    9. 当然,自己可以在类中定义__eq__方法,决定==如何比较对象。如果不覆盖__eq__方法,从object几成的方法就是比较对象的ID。
    10. Python支持重载运算符,不支持函数重载。
    11. 处理不可变的对象时,变量保存的是真正的底下那个还是共享对象的引用无关紧要。如果a == b成立,而且两个对象都不会变,那么它们就可能是相同的对象。这就是为什么字符串可以安全使用驻留(intering)。仅当对象可变时,对象标识(id)才重要。
    12. 可变对象是导致多线程编程难以处理的主要原因,因为某个线程改动对象后,如果不正确地同步,那就会损坏数据。但是过度同步又会导致死锁。
    13. Python没有直接销毁对象的机制,这其实是一个好的特性:如果随时可以销毁对象,那么指向对象的强引用怎么办?
    14. 在CPython中, 垃圾回收依靠引用计数。
    open('test.txt', 'wt', encoding='utf-8').write('1,2,3
    ')
    

    这段代码在CPython是安全的,一旦引用数量归零,就立即销毁对象,因为文件对象的引用数量会在write方法返回后归零,Python在销毁内存中表示文件的对象之前,会立即关闭文件。然而在JPython和IronPython中却不安全,因为它们使用的是宿主运行时(Java VM和.NET CLR)中的垃圾回收程序,不依靠引用计数,更加复杂。所以,在任何情况下,应该改为以下的代码:

    with open('test.txt', 'wt', encoding='utf-8') as fp:
        fp.write('1,2,3')
    
  • 相关阅读:
    10-12
    8-10
    5.2-5.3
    四则运算 测试与封装
    第5-7章
    汉堡包
    1-5章
    实验二
    实验一
    Controller方法是如何与请求匹配上的及参数如何填充的
  • 原文地址:https://www.cnblogs.com/allen2333/p/8850138.html
Copyright © 2011-2022 走看看