python3[进阶]8.对象引用、可变性和垃圾回收

    xiaoxiao2024-12-12  16

    文章目录

    8.1变量不是盒子8.2 标识,相等性和别名8.2.1 在==和is之间选择8.2.2 元组的相对不可变性 8.3 默认做浅复制(拓展)为任意对象做深复制和浅复制深拷贝和浅拷贝有什么具体的区别呢? 8.4 函数的参数作为引用时8.4.1 不要使用可变类型作为参数的默认值 总结(阅读)8.4.2 防御可变参数 8.5 del和垃圾回收8.6 弱引用

    8.1变量不是盒子

    python变量类似于Java中的引用型变量,因此最好把他们理解为附注在对象上的标注.

    a = [1,2,3] b = a a.append(7) print(b)

    输出为:

    [1, 2, 3, 7] // 可以发现,a和b引用同一个列表,而不是那个列表的副本

    因为变量只不过是标注,所以可以为对象贴上多个标注,贴的多个标注就是别名.

    8.2 标识,相等性和别名

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

    标识最常使用is运算符检查,而不是直接比较ID.

    charles = {'name':'charles', 'born':'1832'} lewis = charles print(lewis is charles) print(id(charles), id(lewis)) lewis['balence'] = 950 print(charles) alex = {'name': 'charles', 'born': '1832', 'balence': 950} print(alex == charles) print(alex is charles)

    输出:

    True 140640659352168 140640659352168 {'name': 'charles', 'born': '1832', 'balence': 950} True //比较两个对象,结果相同,这是因为dic类的__eq__方法就是这样实现的 False //但是他们是不同的对象,标识不同. //可以发现,charles和lewis绑定同一个对象,alex绑定另外一个对象

    8.2.1 在==和is之间选择

    ==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识(标识就是在内存中的位置) 在变量和"单例值"之间比较时,应该使用is.可以使用is检查变量绑定的值是不是None.

    x is None

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

    8.2.2 元组的相对不可变性

    元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。而 str、bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。 元组的不可变性其实是指tuple数据结构的物理内容(保存的引用)不可变,与引用的对象无关. 复制对象时,相等性和一致性之间的区别有更深入的影响。副本与源对象相等,但是ID不同。可是,如果对象中包含其他对象,那么应该复制内部对象吗?可以共享内部对象吗?这些问题没有唯一的答案。

    8.3 默认做浅复制

    l1 = [3, [55, 44], (7, 8, 9)] l2 = list(l1) print(l2) print(l2 == l1) print(l2 is l1) print(l2[2] is l1[2]) l3 =l1[:] print(l3) print(l3 == l1) print(l3 is l1) print(l3[2] is l1[2]) l4 = l1 print(l4 == l1) print(l4 is l1) l1[1].append(66) print(l1) print(l2) print(l3) print(l4)

    输出结果如下: 上图发现:

    l2和l3对应着关于l1的浅拷贝,l4直接将l1起了一个别名,也就是说l4和l1指向了同一个对象。浅拷贝对于内层引用有影响,即内层引用还是指向了同一个对象。[3, [55, 44], (7, 8, 9)] //list(l1)创建l1的副本 True //副本和源列表相等

    对于列表和其他可变序列来说,还可以使用更简洁的l3 = l1[:]语句来创建副本 然而,构造方法或者[:] 做的是浅复制(就是复制了最外层容器,副本中的元素是源容器中元素的引用).如果所有的元素都是不可变的,那么这样没有问题,如果有可变的元素,会出现问题.

    l1 = [3,[66,55,44],(7,8,9)] l2 = list(l1) l1.append(100) l1[1].remove(55) print('l1:',l1) print('l2:',l2) l2[1]+=[33,22] l2[2]+=(10,11) print('l1:',l1) print('l2:',l2)

    输出:

    l1: [3, [66, 44], (7, 8, 9), 100] l2: [3, [66, 44], (7, 8, 9)] l1: [3, [66, 44, 33, 22], (7, 8, 9), 100] l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)] # 对**元组**来说,+=运算符创建一个新元组,然后重新绑定给变量l2[2].现在l1和l2中最后位置上的元组不是同一个对象. # 总结:对+=和×=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。

    如图:

    (拓展)为任意对象做深复制和浅复制

    浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对象的引用)。

    import copy class Bus: 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) bus1 = Bus(['Alice', 'Bill', 'Claire', 'David']) bus2 = copy.copy(bus1) bus3 = copy.deepcopy(bus1) print(id(bus1), id(bus2), id(bus3)) bus1.drop('Bill') print(bus2.passengers) print(id(bus1.passengers), id(bus2.passengers),id(bus3.passengers)) print(bus3.passengers)

    输出:

    140694379943920 140694379943976 140694379944088 ['Alice', 'Claire', 'David'] 140694377362824 140694377362824 140694377336776 ['Alice', 'Bill', 'Claire', 'David']

    结果如图: 使用 copy 和 deepcopy,创建 3 个不同的 Bus 实例。 审查 passengers 属性后发现:

    bus1 和 bus2 共享同一个列表对象,因为 bus2 是bus1 的浅复制副本。bus3 是 bus1 的深复制副本,因此它的 passengers 属性指代另一个列表。 从上面可以发现,深拷贝和浅拷贝都会创建不同的对象,深拷贝是完全拷贝一个新的对象,浅拷贝不会拷贝子对象。

    深拷贝和浅拷贝有什么具体的区别呢?

    import copy a = [1,2,3,['a','b','c']] b = copy.copy(a) c = copy.deepcopy(a) a[3].append('d') print(a) print(b) print(c)

    结果如图: 从上图我们可以发现,

    copy.deepcopy()会完全拷贝一个新的对象出现;copy.copy()不会拷贝其子对象,也就是说,如果原来的对象里面又包含别的对象的引用,则这个新的对象还是会指向这个旧的内层引用。 总结: copy.copy() 浅复制,不会拷贝其子对象,修改子对象,将受影响 . copy.deepcopy() 深复制,将拷贝其子对象,修改子对象,将不受影响.

    8.4 函数的参数作为引用时

    python唯一支持的参数传递模式是共享传参(call by sharing). 共享传参指函数的各个形式参数获得实参中各个引用的副本,也就是说,函数内部的形参是实参的别名。 这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。

    def f(a,b): a += b return a x,y = 1,2 print(f(x, y)) print(x, y) a = [1, 2] b = [3, 4] print(f(a, b)) print(a, b) t = (10, 20) u = (30, 40) print(f(t, u)) print(t, u)

    输出:

    3 1 2 [1, 2, 3, 4] [1, 2, 3, 4] [3, 4] (10, 20, 30, 40) (10, 20) (30, 40)

    我们发现数字x没变,列表a变了,元组t没变

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

    可选参数可以有默认值,这是python函数定义的一个很好的特性.但是我们应该避免使用可变的对象作为参数的默认值.

    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(['Alice','Bill']) print(bus1.passengers) bus1.pick('Charlie') bus1.drop('Alice') print(bus1.passengers) bus2 = HauntedBus() bus2.pick('Carrie') print(bus2.passengers) bus3 = HauntedBus() print(bus3.passengers) bus3.pick('Dive') print(bus2.passengers) print(bus2.passengers is bus3.passengers) print(bus1.passengers)

    输出:

    ['Alice', 'Bill'] ['Bill', 'Charlie'] ['Carrie'] ['Carrie']//bus3一开始是空的,但是默认列表却不为空 ['Carrie', 'Dive'] True ['Bill', 'Charlie']

    问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。 使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也就变了,这样会影响以后使用默认值的调用。 修正的方法很简单:在__init__方法中,传入passengers参数时,应该把参数值的副本赋值给self.passengers,

    def __init__(self,passengers =None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers)

    总结(阅读)

    关于+和extend()方法,+是创建了新对象,extend是就地连接。浅复制分为两类: 第一类:t2 = t1[:]或者 t2 = list(t1) 都是了新的对象t2.但是内层引用还是指向同一个对象。第二类:l2 = copy.copy(l1) 深复制: l3 = copy.deepcopy(l1) 完全拷贝了一个新的对象。关于增量运算符+=和×=,以+=为例:a+=b,若+=前面的a为可变序列(例如list),则就地解决,若a为不可变序列(例如tuple),则会创建新的对象。实例中图片经过:http://www.pythontutor.com/ 生成

    8.4.2 防御可变参数

    如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。 示例 8-15 一个简单的类,说明接受可变参数的风险

    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)

    测试一下

    >>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] >>> bus = TwilightBus(basketball_team) >>> bus.drop('Tina') >>> bus.drop('Pat') >>> basketball_team ['Sue', 'Maya', 'Diana']

    发现:下车的学生从篮球队中消失了! TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”。学生从校车中下车后,她的名字就从篮球队的名单中消失了,这确实让人惊讶。

    这里的问题是,校车为传给构造方法的列表创建了别名。正确的做法是,校车自己维护乘客列表。修正的方法很简单:在 init 中,传入 passengers 参数时,应该把参数值的副本赋值给 self.passengers,像示例 8-8 中那样做(8.3 节)。

    def __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers) ➊

    ➊ 创建 passengers 列表的副本;如果不是列表,就把它转换成列表。在内部像这样处理乘客列表,就不会影响初始化校车时传入的参数了。此外,这种处理方式还更灵活:现在,传给 passengers 参数的值可以是元组或任何其他可迭代对象,例如set 对象,甚至数据库查询结果,因为 list 构造方法接受任何可迭代对象。

    8.5 del和垃圾回收

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

    在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用__del__ 方法(如果定义了),然后释放分配给对象的内存。 CPython 2.0 增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即 使再出色的引用方式也会导致组中的对象不可获取。Python 的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数,这意味着,对象的引用数量为零时可能不会立即调用__del__ 方法。

    8.6 弱引用

    正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。 弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。 弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。

    最新回复(0)