在Python中不应调用__del__方法时却被调用

7 投票
2 回答
3042 浏览
提问于 2025-04-15 17:13

我刚开始学习Python,正在看Swaroop CH的《A Byte of 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说再见
我是最后一个

为什么在第二次运行时,__del__方法会在__init__方法之后立刻被调用呢?
我猜是因为使用了相同的实例名称('swaroop'和'kaleem'),所以它释放了原来的实例并进行了垃圾回收。但这似乎搞乱了当前的实例计数。

这到底是怎么回事呢?
有什么好的方法可以避免这种困惑吗?
是不是应该避免使用__del__
在重用实例名称之前检查一下现有的实例名称?
...

谢谢,
Eric

2 个回答

8

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

在你的例子中,使用execfile()会有很多问题——这并不是一个好的做法——而且重新定义全局变量也会引发问题。顺便说一下,如果你真的需要创建一个伪析构函数(也就是在对象被垃圾回收时调用的代码),可以写一个所谓的“终结器”函数(这并不是真正的析构函数),然后通过weakref.ref的回调来调用它。这个函数当然不能是实例方法,而且要记住,lambda实际上会创建一个闭包,所以一定要确保在回调中不要泄露对self的引用!如果你需要从被销毁的实例中获取数据,可以使用函数的默认参数方法,只要确保绝对不要在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
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__ 这个特殊方法。

撰写回答