Python中未预期调用__del__方法的问题

7

我是新手,正在学习Python,并且正在通过Swaroop CH的《Python简明教程》中的例子进行学习。我发现__del__方法的行为让我感到困惑。

基本上,如果我在Python 2.6.2中运行以下脚本:

class Person4:
    '''Represents a person'''
    population = 0

    def __init__(self, name):
        '''Initialize the person's data'''
        self.name = name
        print 'Initializing %s'% self.name

        #When the person is created they increase the population
        Person4.population += 1

    def __del__(self):
        '''I am dying'''
        print '%s says bye' % self.name

        Person4.population -= 1

        if Person4.population == 0:
            print 'I am the last one'
        else:
            print 'There are still %d left' % Person4.population


swaroop = Person4('Swaroop')
kaleem = Person4('Kalem')

在使用Python控制台(或Spyder交互式控制台)时,我看到了以下内容:

execfile(u'C:\1_eric\Python\test1.py')
初始化Swaroop
初始化Kalem

execfile(u'C:\1_eric\Python\test1.py')
初始化Swaroop
Swaroop说再见
我是最后一个
初始化Kalem
Kalem说再见
我是最后一个

为什么第二个运行后__init__方法立刻被调用了?
我猜测因为相同的实例名称('swaroop'和'kaleem')被重复使用,导致原始实例被释放和垃圾回收。但是,这似乎会对当前人口统计数据造成困扰。

这里发生了什么?有什么好的方法可以避免这种混淆?
是避免使用__del__方法?还是在重复使用实例名称之前检查其是否存在?...

谢谢, Eric


2
你不应该依赖于__del__方法在任何特定时间被调用。将来垃圾回收器可能会改变,以便在不同的时间调用__del__ - Georg Schölly
@Soviut(错误地重新标记了此内容):这与Python中的“classmethods”无关。它确实使用类属性,但那不是同一件事。 - Peter Hansen
欢迎来到 Stack Overflow!当您收到一个好的问题答案时,请通过点击答案左侧的复选框轮廓来“接受”最有帮助的答案。这表示您已经收到了一个对您有用的答案。这样做有助于提高您在该网站上的声誉。 - Greg Hewgill
2个回答

19

这里有几件事情发生了。当实例化你的Person4类时,它会将其population类变量初始化为0。从你的交互式控制台来看,你似乎多次运行了你的“test1.py”文件。第二次运行它时,Person4类被重新声明,这使它从技术上讲与第一个类不同 (即使它具有相同的名称)。这意味着它拥有自己独立的population计数。

现在,swaroopkaleem全局变量,在“test1.py”的两个实例之间共享。Python内部对于大部分自动垃圾回收使用引用计数,因此第一个Person4类的原始实例直到第二次赋值给swaroop才被释放。将swaroop赋值会减少第一个实例的引用计数,导致调用__del__,因为引用计数现在为零。但是由于你在__del__()内部通过名称引用了Person4,所以当实例消失时,它会减少新的Person4.population计数,而不是旧的Person4人口数量。

希望这讲得通。我可以理解为什么对于学习Python的人来说可能会令人困惑。同时使用类变量和使用execfile()重新定义Person4类进一步使事情变得混乱。值得一提的是,我编写了很多Python代码,但我认为我从未需要使用过__del__特殊方法。


8

一般建议:不要在Python中使用__del__。它可能会以多种方式破坏垃圾收集,尤其是在对象之间存在循环引用的情况下。

在您的示例中,与使用execfile()相关的各种问题存在,这并不是最佳实践,并且重新定义全局变量。顺便说一句,如果您确实需要创建一个伪析构函数(即每当对象被垃圾回收时调用的代码),请编写一个所谓的“finalizer”函数(它不是真正的析构函数),并使用weakref.ref回调来调用它。它当然不应该是实例方法,并且请记住,lambda实际上创建了一个闭包,因此请务必确保在回调中不泄漏对self的任何引用!如果您需要从已销毁的实例获取数据,请使用func默认参数方法,但请确保永远不要在lambda内部引用'self',否则它将无法工作。

from weakref import ref
from time import sleep

class Person4:
    '''Represents a person'''
    population = 0

    def __init__(self, name):
        '''Initialize the person's data'''
        self.name = name
        print 'Initializing %s'% self.name

        #When the person is created they increase the population
        Person4.population += 1

        self._wr = ref(self, lambda wr, name=self.name: Person4_finalizer(name))

def Person4_finalizer(name):
        '''I am dying'''
        print '%s says bye' % name

        Person4.population -= 1

        if Person4.population == 0:
            print 'I am the last one'
        else:
            print 'There are still %d left' % Person4.population

p1 = Person4("one")
p2 = Person4("two")
p3 = Person4("three")

del p2
del p3
sleep(5)

输出(sleep 帮助查看正在发生的事情):

Initializing one
Initializing two
Initializing three
two says bye
There are still 2 left
three says bye
There are still 1 left
one says bye
I am the last one

感谢Greg和Alan的解释,我现在明白发生了什么,并且使用__del__存在问题。有一件事情仍然困扰着我,那就是如果我“忘记”删除上面脚本中的所有全局变量并连续两次运行脚本(在使用execfile()的Spyder IDE内),我仍然会看到finalizer在initializer之后被调用, Initializing one one says bye I am the last one 即“旧”p1的清理发生在p1被重新分配之后。我想我需要小心重新分配“全局”变量(或使用像“tempVar”这样的临时变量)。 - ELee
2
更好的是,你应该记住Python没有像静态类型语言那样的“变量”概念。只有对象和绑定这些对象的名称 - 一旦一个对象不被任何东西引用,它就会被销毁 - 但是当你改变值时,没有“内存位置”的值会被覆盖 - “=”运算符将把名称__重新绑定__到另一个引用上。 - Alan Franzoni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接