我不理解Python中的__del__行为。

47

有人能解释一下为什么以下代码会表现出它的行为吗:

import types

class Dummy():
    def __init__(self, name):
        self.name = name
    def __del__(self):
        print "delete",self.name

d1 = Dummy("d1")
del d1
d1 = None
print "after d1"

d2 = Dummy("d2")
def func(self):
    print "func called"
d2.func = types.MethodType(func, d2)
d2.func()
del d2
d2 = None
print "after d2"

d3 = Dummy("d3")
def func(self):
    print "func called"
d3.func = types.MethodType(func, d3)
d3.func()
d3.func = None
del d3
d3 = None
print "after d3"

输出结果是这样的(请注意,d2的析构函数从未被调用)(Python 2.7)

delete d1
after d1
func called
after d2
func called
delete d3
after d3
有没有一种方法可以“修复”代码,使析构函数在不删除已添加的方法的情况下被调用?我的意思是,最好将d2.func = None放在析构函数中!
谢谢
[编辑] 根据前几个答案,我想澄清一下,我不是在问使用__del__的优点(或缺点)。我试图创建一个最短的函数来演示我认为非直观的行为。我假设已经创建了循环引用,但我不确定原因。如果可能的话,我想知道如何避免循环引用....

2
是的,types.MethodType(func, d2) 引用了 d2 并将其放在 d2 上,因此你有一个循环引用。这并不奇怪,但如果这不是你想要的,那你为什么要这样做呢? - Jochen Ritzel
1
这里是一个类似的问题和atexit答案的链接:链接 - Brett Stottlemyer
有没有办法“修复”代码,使析构函数在不删除添加的方法的情况下被调用?我认为我们都对这个声明感到困惑。如果您要删除特定对象的实例,并且已经添加了特定方法,那么为什么需要该方法继续存在呢?如果您希望它成为Dummy对象的一部分,那么可以将该方法添加到其中:Dummy.func = types.MethodType(func, Dummy)。然后,这将向所有Dummy实例添加该方法。(这可能不是期望的行为,但这就是您会得到的) - monkut
4
@Brett:你现在正在做一些奇怪的事情(给实例添加函数),这导致了问题,所以你正在寻找更多奇怪的方法来解决它。退一步说,更合理的做法是停下来思考如何以不会带来更多问题的方式解决原始问题。你可以通过描述符来解决这个问题,但我不太想解释 - 这有点复杂,只有比可避免的问题的复杂解决方案更糟糕的事情是复杂的解决方案。 - Jochen Ritzel
@JochenRitzel 描述符在这里没有帮助,因为描述符所做的就是在幕后实例化MethodType对象。 这仍然会创建循环引用。 - Aryeh Leib Taurog
显示剩余4条评论
8个回答

46

不能假设 __del__ 方法会被调用 - 它不是自动释放资源的地方。如果你想确保一个(非内存)资源被释放,应该创建一个 release() 或类似的方法,然后显式地调用它(或在上下文管理器中使用,正如Thanatos在评论中指出的那样)。

至少你应该仔细阅读 __del__文档,然后最好不要尝试使用 __del__。(还可以参考 gc.garbage文档,了解关于 __del__ 的其他坏处)


12
此外,with语句还可以与类似于release()的函数一起使用,以编写更简单、更清晰的代码。 - Thanatos
9
如果“显式”总是一个好主意,为什么还需要垃圾回收呢?“记得调用”释放函数是脆弱的。你可能会忘记调用它 - 或者异常可能在意外的时刻被引发,而你可能永远不会到达释放函数。这篇文章解释了如何保证调用__del__函数。如果可能的话,上下文管理器会更好,但大部分时间都不可能,因为对象的生命周期超越了一个方法或函数。 - Tom Swirly
2
@TomSwirly:你可能能够保证你的对象将有资格进行垃圾回收,但你无法保证何时进行回收。如果垃圾收集器可以自由地收集一个对象,那么它也可以自由地收集它。Raymond Chen的这篇博客文章值得记住。对于给定的GC实现做出太多假设可能会导致今天工作的代码明天神秘地崩溃。 - Nick Bastin
2
@TomSwirly 如果你无法确定某个特定资源所持有和释放的单个范围,那么你应该重新考虑你的代码组织方式。创建一个可以获取和释放资源的单个范围是最好的计划,即使需要进行重大重构。然后,您可以将资源对象传递到函数调用或其他内容中,并根据需要将其向下传递到堆栈。通常,您可以找到一种仅在代码的短段时间内持有资源的方法(在打开文件进行写入之前生成内容,在读取数据后立即关闭连接)。 - jpmc26
1
@NickBastin 对于我的表述不够清晰,我深感抱歉。我的意思是当你发现自己“无法”使用上下文管理器时,请重新组织你的代码,以便你可以使用它。=)我认为我的“获取”和“释放”的提及会被理解为上下文管理器通常执行的函数。我完全同意你的看法。 - jpmc26
显示剩余4条评论

29

我提供自己的答案,因为尽管我感谢避免使用__del__的建议,但我的问题是如何使它在提供的代码示例中正常工作。

简短版本: 下面的代码使用weakref来避免循环引用。我觉得在发布问题之前已经尝试过这个方法,但我想我一定做错了什么。

import types, weakref

class Dummy():
    def __init__(self, name):
        self.name = name
    def __del__(self):
        print "delete",self.name

d2 = Dummy("d2")
def func(self):
    print "func called"
d2.func = types.MethodType(func, weakref.ref(d2)) #This works
#d2.func = func.__get__(weakref.ref(d2), Dummy) #This works too
d2.func()
del d2
d2 = None
print "after d2"

长版: 当我发布问题时,我确实搜索过类似的问题。我知道可以使用with,并且流行的观点是__del__不好的

使用with在某些情况下很有意义。打开文件、读取它和关闭它就是一个很好的例子,其中with是完全可行的解决方案。你已经去过需要对象的特定代码块,并且你想在块结束时清理对象。

数据库连接似乎经常被用作不适合使用with的例子,因为通常需要离开创建连接的代码段,并在更多事件驱动(而不是顺序)的时间框架内关闭连接。

如果with不是正确的解决方案,我看到两个替代方案:

  1. 确保__del__工作正常(请参见此博客以获取关于弱引用用法的更好描述)
  2. 使用atexit模块在程序关闭时运行回调函数。请参见此主题,了解示例。

虽然我尝试提供简化的代码,但我的真正问题更多是事件驱动的,因此with不是一个合适的解决方案(with对于简化的代码来说还可以)。我也想避免使用atexit,因为我的程序可能需要长时间运行,并且我希望能够尽快执行清理操作。

因此,在这种特定情况下,我认为使用weakref并防止会导致__del__无法正常工作的循环引用是最好的解决方案。

这可能是一种例外情况,但在某些用例中,使用weakref__del__是正确的实现方式,我个人认为是这样。


7
记住:没有任何保证__del__方法会被调用,也不知道它何时被调用以及被调用多少次。垃圾回收是由Python自动控制的,发生的时间无法掌控。值得注意的是,只有cPython实现了引用计数并及时回收该类垃圾,而Jython、PyPy(以及我认为的IronPython)只在垃圾回收扫描期间进行回收。过度依赖__del__方法可能会导致程序灾难性后果。 - SingleNegationElimination
1
“显示比隐式更好。”(http://www.python.org/dev/peps/pep-0020/)如果你需要在一个事件处理程序中打开连接,然后在另一个处理程序中关闭它,请明确调用close()!否则,在整个事件循环中使用`with`语句包装。如果需要,使用[contextlib.closing](http://docs.python.org/release/2.7/library/contextlib.html#contextlib.closing)。如果您不知道在哪里关闭连接,则问题更大。请不要通过修改语言来让您的松散代码奇迹般地运行,请找出放置这些close()语句的位置。你会感到高兴的。 - Aryeh Leib Taurog
8
你必须知道如何提问,或至少知道如何接受回答。请参考这个元问题关于XY问题的这个问题。你问了一个关于Python内部机制的合法问题:“为什么__del__似乎不起作用”,你得到了一个合法的回答。而你真正的问题是:“我如何在一个事件处理程序中关闭在另一个事件处理程序中打开的资源”,这个问题的答案与__del__无关。 - Aryeh Leib Taurog
8
@Aryeh - 我不同意。我的问题是如何让Python的一个功能正常工作,并且我提供了一个正确的答案。如果真正的答案是不要使用__del__,那么__del__应该从Python中删除。我认为赞成票给人们一个通常接受的想法,而我的答案并不是。那很好。但当我的答案确实回答了问题(按照提问的方式,而不是你错误地重新陈述问题),你怎么能说我的答案是错的呢? - Brett Stottlemyer
5
就使用数据库连接作为一个 with 不好用的例子,我强烈反对。通常情况下,在程序的某个高级别范围内获取数据库是非常简单的,将其作为参数传递到其他代码部分中,然后在它们返回时释放它。可能在传递参数和返回之间执行了很多代码,但它仍然是一个非常明确定义的范围。与使用全局变量来处理数据库连接并解决这种问题相比,更倾向于使用这种类型的代码。 - jpmc26
显示剩余10条评论

11

您可以使用with操作符,而不是del

http://effbot.org/zone/python-with-statement.htm

就像使用文件类型对象一样,您可以做类似的事情:

with Dummy('d1') as d:
    #stuff
#d's __exit__ method is guaranteed to have been called

5
d 不超出范围。唯一的保证是在由 Dummy('d1') 创建的对象上调用了 __exit__ 方法。 - SingleNegationElimination
1
@TokenMacGuy:好的,你是对的。从技术上讲,它并不超出范围,我想这值得一提。然而,为了使用语法,对象必须实现__enter____exit__方法。因此,就所有意图而言,在with语句之后,它就超出了范围,因为人们可能会将他们的__exit__方法写成析构函数。然而,我没有意识到它实际上并没有超出范围,我想这对各种黑客行为可能很有用。 - Falmarri

9

del不会调用__del__

你所使用的方式中,del是用来删除一个局部变量的。当对象被销毁时,才会调用__del__方法。Python语言对于对象何时被销毁没有任何保证。

作为Python最常见的实现,CPython使用引用计数。因此,del通常会按照你的预期工作。但如果存在引用循环的情况下,它将无法正常工作。

d3 -> d3.func -> d3

Python无法检测到这个问题,因此不能立即清除它。而且这不仅是引用循环的问题。如果抛出异常,可能仍然希望调用析构函数。但是,作为其回溯的一部分,Python通常会保留本地变量。

解决方案不是依赖于__del__方法。相反,请使用上下文管理器。

class Dummy:
   def __enter__(self):
       return self

   def __exit__(self, type, value, traceback):
       print "Destroying", self

with Dummy() as dummy:
    # Do whatever you want with dummy in here
# __exit__ will be called before you get here

这是保证可行的,你甚至可以检查参数,看看是否处理异常并在这种情况下采取不同的措施。


2

在我看来,问题的关键在于:

添加函数是动态的(在运行时进行),而不是预先知道的

我感觉你真正想要的是一种灵活的方式来将不同的功能绑定到表示程序状态的对象上,也称为多态。Python做得很好,不是通过附加/分离方法,而是通过实例化不同的类。我建议您重新审视您的类组织。也许您需要将核心、持久数据对象与短暂状态对象分开。使用“有一个”范例而不是“是一个”范例:每次状态更改时,您要么将核心数据包装在状态对象中,要么将新状态对象分配给核心的属性。

如果您确定不能使用这种Python风格的OOP,您仍然可以通过首先在类中定义所有函数,然后将它们绑定到其他实例属性来解决问题(除非您正在根据用户输入即时编译这些函数):

class LongRunning(object):
    def bark_loudly(self):
        print("WOOF WOOF")

    def bark_softly(self):
        print("woof woof")


while True:
    d = LongRunning()
    d.bark = d.bark_loudly
    d.bark()

    d.bark = d.bark_softly
    d.bark()

1
一个完整的上下文管理器示例。
class Dummy(object):
    def __init__(self, name):
        self.name = name
    def __enter__(self):
        return self
    def __exit__(self, exct_type, exce_value, traceback):
        print 'cleanup:', d
    def __repr__(self):
        return 'Dummy(%r)' % (self.name,)

with Dummy("foo") as d:
    print 'using:', d

print 'later:', d

0

使用weakref的另一种解决方案是,当仅在实例调用时动态绑定函数,通过覆盖类上的__getattr____getattribute__来返回func.__get__(self, type(self))而不是只有func。这就是在类上定义的函数的行为方式。不幸的是(对于某些用例),Python不会对附加到实例本身的函数执行相同的逻辑,但您可以修改它以执行此操作。我曾经遇到过与实例绑定的描述符类似的问题。在这里,性能可能不如使用weakref好,但它是一个选项,将使用仅Python内置透明地工作。

如果您经常这样做,您可能需要一个自定义元类,用于动态绑定实例级别的函数。

另一种选择是直接将函数添加到类中,这样在调用时就可以正确地执行绑定。对于许多用例来说,这可能会有一些麻烦:即正确地命名空间化函数,以避免冲突。虽然实例ID可以用于此,但由于cPython中的ID不能保证在程序的整个生命周期内唯一,因此您需要仔细考虑以确保它适用于您的用例...特别是,您可能需要确保在对象超出范围时删除类函数,从而其ID/内存地址再次可用。__del__非常适合这个问题 :)。或者,您可以在对象创建时清除所有命名空间为该实例的方法(在__init____new__中)。
另一种选择(而不是搞python魔术方法)是显式添加一个用于调用动态绑定函数的方法。这样做的缺点是,用户无法使用正常的python语法调用您的函数:
class MyClass(object):
    def dynamic_func(self, func_name):
        return getattr(self, func_name).__get__(self, type(self))

    def call_dynamic_func(self, func_name, *args, **kwargs):
        return getattr(self, func_name).__get__(self, type(self))(*args, **kwargs)

    """
    Alternate without using descriptor functionality:
    def call_dynamic_func(self, func_name, *args, **kwargs):
        return getattr(self, func_name)(self, *args, **kwargs)
    """

为了让这篇文章更完整,我也会展示你的weakref选项:

import weakref
inst = MyClass()
def func(self):
    print 'My func'
#  You could also use the types modules, but the descriptor method is cleaner IMO
inst.func = func.__get__(weakref.ref(inst), type(inst))

当然,有许多其他方法可以获得类似的功能而不需要循环引用。 - DylanYoung

0

使用 eval()


In [1]: int('25.0')                                                                                                                                                                                                                                                               
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-1-67d52e3d0c17> in <module>
----> 1 int('25.0')

ValueError: invalid literal for int() with base 10: '25.0'

In [2]: int(float('25.0'))                                                                                                                                                                                                                                                        
Out[2]: 25

In [3]: eval('25.0')                                                                                                                                                                                                                                                              
Out[3]: 25.0

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