sco*_*awg 5 python garbage-collection destructor
我看到一些代码在__del__
我发现好奇的对象上被明确调用,所以我试着用它来理解它是如何工作的.
我尝试了以下代码(我理解使用__del__
它本身可能很糟糕,但我只想在这里尝试自己):
class A:
def __init__(self, b):
self.a = 123
self.b = b
print "a is {}".format(self.a)
def __del__(self):
self.a = -1
print "a is {}".format(self.a)
self.b.dont_gc_me = self
def foo(self):
self.a = 9999999
print "a is {}".format(self.a)
class Foo:
def __init__(self):
self.a = 800
def __del__(self):
self.a = 0
Run Code Online (Sandbox Code Playgroud)
然后尝试(在IPython控制台中)以下内容:
In [92]: f = Foo()
In [93]: a = A(f)
a is 123
In [94]: a = None
a is -1
In [95]: f.__dict__
Out[95]: {'a': 800, 'dont_gc_me': <__main__.A instance at 0x2456830>}
In [96]: f = None
In [97]:
Run Code Online (Sandbox Code Playgroud)
我看到__del__
只调用一次,即使实例a A
已被实例f保持活着Foo
,当我将后者设置None
为时,我也看不到第二次调用析构函数.
请注意,该方法可以(尽管不推荐!)
__del__()
通过创建对它的新引用来推迟实例的销毁.然后可以在稍后删除该新引用时调用它.无法保证__del__()
在解释器退出时仍然存在的对象调用方法.
这似乎意味着__del__
可以再次调用该方法,但不能保证它会.所以我的问题是:是否有某些情况__del__
会被再次调用?(我认为设置f
到None
上面就可以了,但事实并非如此).还有其他值得注意的细微差别吗?
这是一种方式:
xs = []
n = 0
class A:
def __del__(self):
global n
n += 1
print("time {} calling __del__".format(n))
xs.append(self)
print("creating an A immediately thrown away")
A()
for _ in range(5):
print("popping from xs")
xs.pop()
Run Code Online (Sandbox Code Playgroud)
打印:
creating an A immediately thrown away
time 1 calling __del__
popping from xs
time 2 calling __del__
popping from xs
time 3 calling __del__
popping from xs
time 4 calling __del__
popping from xs
time 5 calling __del__
popping from xs
time 6 calling __del__
Run Code Online (Sandbox Code Playgroud)
因此,简而言之,__del__
可以调用的次数没有限制.但不要依赖于此 - 语言最终可能会改变"规则".
循环使情况复杂化,因为当循环完全是垃圾时,没有可预测的顺序,属于循环的对象将被销毁.因为它是一个周期,每一个对象是从周期每隔一个对象可到达的,因此执行__del__
用于循环中的一个对象可引用一个已经被破坏的对象.那里的主要问题是,CPython只是拒绝收集一个循环,其中至少有一个对象有一个__del__
方法.
但是,如果一个"悬挂"垃圾循环的对象有一个__del__
方法,如果后一个对象本身不在垃圾循环中,则没有问题.例:
class A:
def __del__(self):
print("A is going away")
class C:
def __init__(self):
self.self = self
self.a = A()
Run Code Online (Sandbox Code Playgroud)
然后:
>>> c = C()
>>> import gc
>>> gc.collect() # nothing happens
0
>>> c = None # now c is in a trash self-cycle
>>> gc.collect() # c.a.__del__ *is* called
A is going away
2
Run Code Online (Sandbox Code Playgroud)
这就是这个故事的寓意:如果你有一个"需要"运行析构函数的对象,但可能处于一个循环中,则将其__del__
放入原始对象引用的简单对象中.像这样:
class CriticalResource:
def __init__(self, resource):
self.r = resource
def __del__(self):
self.r.close_nicely() # whatever
class FancyObject:
def __init__(self):
# ...
self.r = CriticalResource(get_some_resource())
# ...
Run Code Online (Sandbox Code Playgroud)
现在a FancyObject
可以在你喜欢的循环中.当它成为垃圾桶,周期不会阻止CriticalResource
的__del__
被调用.
正如@delnan在评论中指出的那样,PEP 442改变了从Python 3.4开始的CPython规则(但这不会被移植到任何以前的CPython版本中). __del__
方法最多只执行一次(通过gc - 当然用户可以显式调用它们任意次),并且具有__del__
方法的对象是否是循环垃圾的一部分将不再重要.
该实现将在循环垃圾中找到的所有对象上运行所有终结器,并在记录其终结器已运行的每个此类对象上设置一个位.它在任何对象被拆除之前执行此操作,因此没有终结器可以在疯狂(部分或完全破坏)状态下访问对象.
实现的缺点是终结器可以以任意方式更改对象图,因此当前循环垃圾收集("gc")运行必须放弃循环垃圾中的任何内容再次可达("复活").这就是为什么终结者被允许最多运行一次的原因:否则gc可能被激活为永远不会通过在循环垃圾中复活死对象的终结者取得进展.
实际上,从3.4开始,CPython对__del__
方法的处理将很像Java对终结器的处理.