Gointerface接口声明实现及作用详解
230
2022-08-25
《流畅的Python》读书笔记——Python对象引用、可变性和垃圾回收
变量是引用
>>> a = [1, 2, 3]>>> b = a>>> a.append(4)>>> b[1, 2, 3, 4]
a和b引用同一个列表。
标识、相等性和别名
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会 变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个 对象的标识;id()函数返回对象标识的整数表示。
在==和is之间选择
== 运算符比较两个对象的值(对象中保存的数据),而is 比较对象的 标识(即判定是否是同一个对象)。
检查x是否为None有两种方法:
In [10]: if not x: ...: print('x is None') ...: x is NoneIn [11]: if x is None: ...: print('x is None') ...: x is None
is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调 用特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同 于 a.__eq__(b)。
继承自 object 的 __eq__ 方法比较两个对象的ID,结果与 is一样。
In [15]: class B(object): ...: def __init__(self,value): ...: self._value = value ...: In [16]: a = B(1) In [17]: b = B(1) In [18]: a == b Out[18]: FalseIn [19]: a is b Out[19]: FalseIn [20]: id(a) Out[20]: 140510725545488In [21]: id(b) Out[21]: 140510722278792In [22]: a is B Out[22]: FalseIn [23]: id(B) Out[23]: 21494392In [24]: type(a) == B Out[24]: True
但是多数内置类型使用更有意义的方式覆盖了__eq__ 方法,会考虑对象属性的值。
元组的相对不可变性
元组保存的是对象的引用。如果引用的元素是可变的,即使元组本身不可变,元素依然可变。
In [25]: t1 = (1, 2, [30, 40]) In [26]: t2 = (1, 2, [30, 40]) In [27]: t1 == t2 Out[27]: TrueIn [28]: id(t1[-1]) #t1[-1]就是最后那个列表元素 Out[28]: 140510930522568In [29]: t1[-1].append(99) In [30]: t1 Out[30]: (1, 2, [30, 40, 99])In [31]: id(t1[-1]) Out[31]: 140510930522568In [32]: t1 ==
这也是有些元组不可散列的原因。
默认做浅复制
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:
In [33]: l1 = [3, [55, 44], (7, 8, 9)] In [34]: l2 = list(l1) # 创建l1的副本 还可以 l2 = l1[:] In [35]: l2 Out[35]: [3, [55, 44], (7, 8, 9)]In [36]: l2 == l1 Out[36]: TrueIn [37]: l2 is l1 Out[37]: False
然而,构造方法或[:]做的是浅复制(即复制的是元素引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。
l1 = [3, [66, 55, 44], (7, 8, 9)]l2 = list(l1) # l2是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)
当执行完第2行代码后,如上所示。l1和l2指向不同的列表,但是两者引用同一个列表[66,55,44]和元组(7,8,9)以及int实例。
l1 = [3, [66, 55, 44], (7, 8, 9)]l2 = list(l1) #print(l1[0] is l2[0]) #Trueprint(l1[1] is l2[1]) #Trueprint(l1[2] is l2[2]) #True
它们三个元素刚开始都引用了同一个对象,但是为啥上图没有体现出来,这里注意一下
把 100 追加到l1 中,对 l2 没有影响。
看到这我明白了,像int这种是字面量就能表示值的不可变类型直接将值放入列表中,没有把内存结构画出来了。
把内部列表l1[1] 中的 55 删除。这对 l2 有影响,因为l2[1] 绑定的列表与 l1[1]是同一个。
对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在l1[1] 中也有体现,因为它是l2[1]的别名。
对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1 和 l2 中最后位置上的元组不是同一个对象。
为任意对象做深复制和浅复制
浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对 象的引用)。copy 模块提供的 deepcopy 和copy函数能为任意对象做深复制和浅复制。
为了演示 copy()和 deepcopy()的用法,定义了一个简单 的类Bus。这个类表示运载乘客的校车,在途中乘客会上车或下车。
# -*- coding: utf-8 -*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)
在交互式控制台中,执行下面代码:
>>> import copy>>> from Bus import Bus>>> bus1 = Bus(['小明','小红','小黄'])>>> bus2 = copy.copy(bus1) #浅复制>>> bus3 = copy.deepcopy(bus1) #深复制>>> id(bus1),id(bus2),id(bus3) #三个不同的Bus实例(2651381816680, 2651381846648, 2651382400280)>>> bus1.drop('小明') #bus1上的小明下车后>>> bus2.passengers #bus2中也没有他了['小红', '小黄']>>> id(bus1.passengers),id(bus2.passengers),id(bus3.passengers) #可以看出,bus1和bus2共享同一个列表对象(2651382392584, 2651382392584, 2651382272072)>>> bus3.passengers #而bus的passengers指向另一个列表['小明', '小红', '小黄']
一般来说,深复制不是件简单的事。如果对象有循环引用,那么 这个朴素的算法会进入无限循环。deepcopy函数会记住已经复制的对 象,因此能优雅地处理循环引。
>>> a = [10, 20]>>> b = [a, 30]>>> a.append(b)>>> a[10, 20, [[...], 30]]>>> from copy import deepcopy>>> c = deepcopy(a)>>> c[10, 20, [[...], 30]]
深复制有时可能太深了。例如,对象可能会引用不该复制的外部 资源或单例值。我们可以实现特殊方法 __copy__() 和__deepcopy__(),控制 copy 和 deepcopy的行为。
当函数的参数作为引用时
Python 唯一支持的参数传递模式是共享传参(call by sharing)。Java 的引 用类型是这样,基本类型按值传参。
共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是 说,函数内部的形参是实参的别名。
这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无 法修改那些对象的标识(即不能把一个对象替换成另一个对象)。
>>> def f(a, b):... a += b... return a... >>> x = 1>>> y = 2>>>> f(x,y)3>>> x,y #数字x没变,无法修改那些对象的标识 方法体内是(x = x + y )(1, 2)>>> a = [1, 2]>>> b = [3, 4]>>> f(a,b)[1, 2, 3, 4]>>> a,b #列表a变了([1, 2, 3, 4], [3, 4])>>> t = (10,20)>>> u = (30,40)>>> f(t, u)(10, 20, 30, 40)>>> t,u #元组t没变((10, 20), (30, 40))
不要使用可变类型作为参数的默认值
我们以上面的的 Bus 类为基础定义一个新类, HauntedBus,然后修改 __init__ 方法。这一次,passengers 的默认值不是None,而是 [],这样就不用像之前那样使用 if判断了。这个“聪明的举动”会让我们陷入麻烦。
class HauntedBus: #主要的问题是传入的参数列表和HauntedBus中的列表会互相影响 def __init__(self, passengers=[]): self.passengers = passengers # 老司机,带带我 def pick(self, name): self.passengers.append(name) # 这不是去幼儿园的车,我要下车 def drop(self, name): self.passengers.remove(name)
控制台测试:
问题在于,没有指定初始乘客的HantedBus实例会共享同一个乘客列表。
这是因为 self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算 (通常在加载模块时),因此默认值变成了函数对象的属性。因此,如 果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受 到影响。
可以查看HauntedBus.__init__对象,看看它的__defaults__属性中的那些幽灵学生:
>>> HauntedBus.__init__.__defaults__(['Carrie', 'Dave'],)
最后,我们可以验证 bus2.passengers是一个别名,它绑定到HauntedBus.__init__.__defaults__属性的第一个元素上:
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengersTrue
可变默认值导致的这个问题说明了为什么通常使用 None 作为接收可变值的参数的默认值。
防御可变参数
如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。
>>> bus4 = HauntedBus(ps) >>> bus4.passengers #和列表中的值一样['jack', 'rose']>>> ps.append('groves') #ps列表新增一个元素>>> bus4.passengers #也在bus4上了['jack', 'rose', 'groves']>>> bus4.drop('rose') #bus4下了一个人>>> ps #ps列表页少了个元素['jack', 'groves']
问题出现的原因是代码self.passengers = passengers
正确的做法是,校车自己维护乘客的列表:
def __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers)#通过构造函数创建副本
除非这个方法确实想修改通过参数传入的对象,否则在类中 直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对 象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦。
del和垃圾回收
del语句删除名称,而不是对象。del命令可能会导致对象被当作垃圾 回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得 到对象时。
为了演示对象生命结束时的情形,下面使用 weakref.finalize 注册一个回调函数,在销毁对象时调用。
>>> import weakref>>> s1 = {1, 2, 3}>>> s2 = s1 #s1和s2是别名,指向同一个集合>>> def bye():... print('Gone with the wind...')... >>> ender = weakref.finalize(s1, bye) #在s1引用的对象上注册回调bye>>> ender.alive #调用finalize对象之前, alive属性的值为TrueTrue>>> del s1 #del不是删除对象,而是删除对象的引用>>> ender.aliveTrue>>> s2 = 'spam' #重新绑定最后一个引用,让{1,2,3}无法获取Gone with the wind... #对象被销毁,调用了bye回调>>> ender.aliveFalse
弱引用
正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后, 垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存 在的时间超过所需时间。这经常用在缓存中。
弱引用不会增加对象的引用数量。引用的目标对象称为所指对象 (referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
下面展示了如何使用 weakref.ref 实例获取所指对象。如果对 象存在,调用弱引用可以获取对象;否则返回 None。
>>> a_set = {0, 1}>>> wref = weakref.ref(a_set) #创建弱引用对象wref>>> wref #a_set的弱引用对象
Python 控制台会自动把 _ 变量绑定到结果不为 None 的表达式结果上。
WeakValueDictionary简介
WeakValueDictionary类实现的是一种可变映射,里面的值是对象 的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应 的键会自动从 WeakValueDictionary中删除。因 此,WeakValueDictionary经常用于缓存。
实现一个简单的类,表示各种奶酪。
# -*- coding: utf-8 -*import weakrefclass Cheese: def __init__(self, kind): self.kind = kind def __repr__(self): return 'Cheese(%r)' % self.kindif __name__ == '__main__': stock = weakref.WeakValueDictionary() #构造WeakValueDictionary 实例 catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')] for cheese in catalog: stock[cheese.kind] = cheese #stock 把奶酪的名称映射到 catalog 中 Cheese 实例的弱引用上 print(cheese) #Cheese('Parmesan') print(sorted(stock.keys())) #['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] del catalog #删除 catalog 之后,stock 中的大多数奶酪都不见了,这是WeakValueDictionary 的预期行为。为什么不是全部呢? print(sorted(stock.keys())) #['Parmesan'] del cheese print(sorted(stock.keys())) #[]
临时变量引用了对象,这可能会导致该变量的存在时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时 会被销毁。但是在上面示例中,for 循环中的变量 cheese 是全局变量,除非显式删除,否则不会消失。
弱引用的局限
不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本 的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地 解决这个问题:
class MyList(list): """list的子类,实例可以作为弱引用的目标""" a_list = MyList(range(10))# a_list可以作为弱引用的目标wref_to_a_list = weakref.ref(a_list)
set 实例可以作为所指对象,用户定义的类型也没问题。但是,int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行。
Python对不可变类型施加的把戏(选读)
对元组t 来说,t[:]不创建副本,而是返回同一个对 象的引用。此外,tuple(t)获得的也是同一个元组的引用。
>>> t1 = (1, 2, 3)>>> t2 = tuple(t1)>>> t2 is t1True>>> t3 = t1[:]>>> t3 is t1True
str、bytes 和 frozenset 实例也有这种行为。注意,frozenset 实例不是序列,因此不能使用fs[:](fs 是一个 frozenset 实 例)。但是,fs.copy()具有相同的效果:它会欺骗你,返回同一个 对象的引用,而不是创建一个副本:
>>> t1 = (1, 2, 3)>>> t3 = (1, 2, 3)>>> t3 is t1False>>> s1 = 'ABC'>>> s2 = 'ABC'>>> s2 is s1 #s1和s2指代同一个字符串True
共享字符串字面量是一种优化措施,称为驻留(interning)(这不和Java中的常量池技术类似吗)。CPython 还会在小的整数上使用这个优化措施,防止重复创建“热门”数字,如0、-1 和 42。(Java中的Integer也会缓存-128到127之间的数值)。
千万不要依赖字符串或整数的驻留!比较字符串或整数是否 相等时,应该使用 ==,而不是 is。驻留是 Python 解释器内部使用 的一个特性。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~