《Python面向对象编程指南》——2.7 __del__()方法

2.7 __del__()方法

__del__()方法有一个让人费解的使用场景。

这个方法的目的是在将一个对象从内存中清除之前,可以有机会做一些清理工作。如果使用上下文管理对象或者with语句来处理这种需求会更加清晰,这也是第5章“可调用对象和上下文的使用”的内容。对于Python的垃圾回收机制而言,创建一个上下文比使用__del__()更加容易预判。

但是,如果一个Python对象包含了一些操作系统的资源,__del__()方法是把资源从程序中释放的最后机会。例如,引用了一个打开的文件、安装好的设备或者子进程的对象,如果我们将资源释放作为__del__()方法的一部分实现,那么我们就可以保证这些资源最后会被释放。

很难预测什么时候__del__()方法会被调用。它并不总是在使用del语句删除对象时被调用,当一个对象因为命名空间被移除而被删除时,它也不一定被调用。Python文档中用不稳定来描述__del__()方法的这种行为,并且提供了额外的关于异常处理的注释:运行期的异常会被忽略,相对地,会使用sys.stderr打印一个警告。

基于上面的这些原因,通常更倾向于使用上下文管理器,而不是实现__del__()。

2.7.1 引用计数和对象销毁

CPython的实现中,对象会包括一个引用计数器。当对象被赋值给一个变量时,这个计数器会递增;当变量被删除时,这个计数器会递减。当引用计数器的值为0时,表示我们的程序不再需要这个对象并且可以销毁这个对象。对于简单对象,当执行删除对象的操作时会调用__del__()方法。

对于包含循环引用的复杂对象,引用计数器有可能永远也不会归零,这样就很难让__del__()被调用。

我们用下面的一个类来看看这个过程中到底发生了什么。

class Noisy:
   def __del__( self ):
     print( "Removing {0}".format(id(self)) )

我们可以像下面这样创建和删除这个对象。

>>> x= Noisy()
>>>del x
Removing 4313946640

我们先创建,然后删除了Noisy对象,几乎是立刻就看到了__del__()方法中输出的消息。这也就是说当变量x被删除后,引用计数器正确地归零了。一旦变量被删除,就没有任何地方引用Noisy实例,所以它也可以被清除。

下面是浅复制中一种常见的情形。

>>> ln = [ Noisy(), Noisy() ]
>>> ln2= ln[:]
>>> del ln

Python没有响应del语句。这说明这些Noisy对象的引用计数器还没有归零,肯定还有其他地方引用了它们,下面的代码验证了这一点。

>>> del ln2
Removing 4313920336
Removing 4313920208

ln2变量是ln列表的一个浅复制。有两个列表引用了Noisy对象,所以在这两个列表被删除并且引用计数器归零之前,Python不会销毁这两个Noisy对象。

还有很多种创建浅复制的方法。下面是其中的一些。

a = b = Noisy()
c = [ Noisy() ] * 2

这里的关键是,由于浅复制在Python中非常普遍,所以我们往往对存在的对象的引用感到非常困惑。

2.7.2 循环引用和垃圾回收

下面是一种常见的循环引用的情形。一个父类包含一个子类的集合,同时集合中的每个子类实例又包含父类的引用。

下面我们用这两个类来看看循环引用。

class Parent:
   def __init__( self, *children ):
     self.children= list(children)
     for child in self.children:
       child.parent= self
   def __del__( self ):
     print( "Removing {__class__.__name__} {id:d}".
format( __class__=self.__class__, id=id(self)) )
class Child:
   def __del__( self ):
     print( "Removing {__class__.__name__} {id:d}".
format( __class__=self.__class__, id=id(self)) )

一个Parent的instance包括一个children的列表。

每一个Child的实例都有一个指向Parent类的引用。当向Parent内部的集合中插入新的Child实例时,这个引用就会被创建。

我们故意把这两个类写得比较复杂,所以下面让我们看看当试图删除对象时,会发生什么。

>>>> p = Parent( Child(), Child() )
>>> id(p)
4313921808
>>> del p

Parent和它的两个初始Child实例都不能被删除,因为它们之间互相引用。

下面,我们创建一个没有Child集合的Parent实例。

>>> p= Parent()
>>> id(p)
4313921744
>>> del p
Removing Parent 4313921744

和我们预期的一样,这个Parent实例成功地被删除了。

由于互相之间有引用存在,因此我们不能从内存中删除Parent实例和它包含的Child实例的集合。如果我们导入垃圾回收器的接口——gc,我们就可以回收和显示这些不能被删除的对象。

下面的代码中,我们使用了gc.collect()方法回收所有定义了__del__()方法但是无法被删除的对象。

>>> import gc
>>> gc.collect()
174
>>> gc.garbage
[<__main__.Parent object at 0x101213910>, <__main__.Child object at 0x101213890>, <__main__.Child object at 0x101213650>, <__main__.Parent object at 0x101213850>, <__main__.Child object at 0x1012130d0>, <__main__.Child object at 0x101219a10>, <__main__.Parent object at 0x101213250>, <__main__.Child object at 0x101213090>, <__main__.Child object at 0x101219810>, <__main__.Parent object at 0x101213050>, <__main__.Child object at 0x101213210>, <__main__.Child object at 0x101219f90>, <__main__.Parent object at 0x101213810>, <__main__.Child object at 0x1012137d0>, <__main__.Child object at 0x101213790>]

可以看到,我们的Parent对象(例如,4313921808的ID = 0x101213910)在不可删除的垃圾对象列表中很突出。为了让引用计数器归零,我们需要删除所有Parent对象中的children列表,或者删除所有Child实例中对Parent的引用。

注意,即使把清理资源的代码放在__del__()方法中,我们也没办法解决循环引用的问题。因为__del__()方法是在循环引用被解除并且引用计数器已经归零之后被调用的。当有循环引用时,我们不能只是简单地依赖于Python中计算引用数量的机制来清理内存中的无用对象。我们必须显式地解除循环引用或者使用可以保证垃圾回收的weakref引用。

2.7.3 循环引用和weakref模块

如果我们需要循环引用,但是又希望将清理资源的代码写在__del__()中,这时候我们可以使用弱引用。循环引用的一个常见场景是互相引用:一个父类中包含了一个集合,集合中的每一个实例也包含了一个指向父类的引用。如果一个Player对象中包含多个Hand实例,那么在每一个Hand对象中都包括一个指向对应的Player类的引用可能会更方便。

默认的对象间的引用可以被称为强引用,但是,叫直接引用可能更好。Python的引用计数机制会直接使用它们,而且如果引用计数无法删除这些对象的话,垃圾回收机器也能及时发现。它们是不可忽略的对象。

对一个对象的强引用就是直接引用,下面是一个例子。

当我们遇到如下语句。

a= B()

变量a直接引用了B类的一个对象。此时B的引用计数至少是1,因为a变量包含了一个指向它的引用。

想要找个一个弱引用相关的对象需要两个步骤。一个弱引用会调用x.parent(),这个函数将弱引用作为一个可调用对象来查找它真正的父对象。这个过程让引用计数器得以归零,垃圾回收器可以回收引用的对象,但是不回收这个弱引用。

weakref定义了一系列使用了弱引用而没有使用强引用的集合。它让我们可以创建一种特殊的字典类型,当这种字典的对象没有用时,可以保证被垃圾回收。

我们可以修改Parent和Child类,在Child指向Parent的引用中使用弱引用,这样就可以简单地保证无用对象会被销毁。

下面是修改后的类,它在Child指向Parent的引用中使用了弱引用。

import weakref
class Parent2:
   def __init__( self, *children ):
     self.children= list(children)
     for child in self.children:
       child.parent= weakref.ref(self)
   def __del__( self ):
     print( "Removing {__class__.__name__} {id:d}".format( __class__= self.__class__, id=id(self)) )

我们将child中的parent引用改为一个weakref对象的引用。

在Child类中,我们必须用上面说的两步操作来定位parent对象:

p = self.parent()
if p is not None:
   # process p, the Parent instance
else:
   # the parent instance was garbage collected.

我们可以显式地确认引用的对象是否已经找到,因为有可能该引用已经变成虚引用。

当我们使用这个新的Parent2类时,可以看到引用计数成功地归零同时对象也被删除了:

>>> p = Parent2( Child(), Child() )
>>> del p
Removing Parent2 4303253584
Removing Child 4303256464
Removing Child 4303043344

当一个weakref引用变成死引用时(因为引用被销毁了),我们有3个可能的方案。

  • 重新创建引用对象,或重新从数据库中加载。
  • 当垃圾回收器在低内存情况下错误地删除了一些对象时,使用warnings模块记录调试信息。
  • 忽略这个问题。

通常,weakref引用变成死引用是因为响应的对象已经被删除了。例如,变量的作用域已经执行结束,一个没有用的命名空间,应用程序正在关闭。对于这个原因,通常我们会采取第3种响应方法。因为试图创建这个引用的对象时很可能马上就会被删除。

2.7.4 __del__()和close()方法

__del__()最常见的用途是确保文件被关闭。

通常,包含文件操作的类都会有类似下面这样的代码。

__del__ = close

这会保证__del__()方法同时也是close()方法。

其他更复杂的情况最好使用上下文管理器。详情请看第5章“可调用对象和上下文的使用”,我们会在第5章提供更多和上下文管理器有关的信息。

时间: 2024-12-06 20:26:44

《Python面向对象编程指南》——2.7 __del__()方法的相关文章

《Python面向对象编程指南》——第1部分 用特殊方法实现Python风格的类 第1章 __init__()方法 1.1 隐式的基类——object

第1部分 用特殊方法实现Python风格的类 init()方法 与Python无缝集成--基本特殊方法 属性访问.特性和修饰符 抽象基类设计的一致性 可调用对象和上下文的使用 创建容器和集合 创建数值类型 装饰器和Mixins--横切方面 用特殊方法实现 Python风格的类 通过重写特殊方法来完成对Python内部机制的调用,在Python中是很普遍的.例如len()函数就可以重写一个类的__len__()方法. 这意味着对于像(len(x))这样的通用公共接口,任何类(例如,声明一个类叫ti

《Python面向对象编程指南》——2.6 比较运算符方法

2.6 比较运算符方法 Python有6个比较运算符.这些运算符分别对应一个特殊方法的实现.根据文档,运算符和特殊方法的对应关系如下所示. x < y调用x.__lt__(y). x <=y调用x.__le__(y). x == y调用x.__eq__(y). x != y调用x.__ne__(y). x > y调用x.__gt__(y). x >= y调用x.__ge__(y). 我们会在第7章"创建数值类型"中再探讨比较运算符. 对于实际上使用了哪个比较运算

《Python面向对象编程指南》——导读

前 言 本书主要介绍Python语言的高级特性,特别是如何编写高质量的Python程序.这通常意味着编写高性能且拥有良好可维护性的程序.同时,我们也会探究不同的设计方案并确定究竟是哪种方案提供了最佳性能.而对于一些正在寻找解决方案的问题,这也是一种很好的方式. 本书的大部分内容将介绍一种给定设计的不同替代方案.一些方案性能更好,另一些方案更加简单或者更加适合于特定领域的问题.最重要的是,找到最好的算法和最优的数据结构,以最少的开销换取最大的价值.时间就是金钱,高效的程序会为它们的用户创造更多的价

《Python面向对象编程指南》——1.5 通过工厂函数调用__init()__

1.5 通过工厂函数调用__init()__ 我们可以使用工厂函数来完成所有Card对象的创建,这比枚举52张牌的方式好很多.在Python中,实现工厂有两种途径. 定义一个函数,返回不同类的对象. 定义一个类,包含了创建对象的方法.这是完整的工厂设计模式,正如设计模式书中提到的.在类似Java这样的语言里,工厂类层次结构是必需的,因为语言本身不支持可以脱离类而单独存在的函数. 在Python里,类定义不是必需的.仅当特别复杂的情形,工厂类才是不错的选择.Python的优势之一是,对于只需要简单

《Python面向对象编程指南》——第2章 与Python无缝集成——基本特殊方法 2.1 __repr__()和__str__()方法

第2章 与Python无缝集成--基本特殊方法 Python中有一些特殊方法,它们允许我们的类和Python更好地集成.在标准库参考(Standard Library Reference)中,它们被称为基本特殊方法,是与Python的其他特性无缝集成的基础. 例如,我们用字符串来表示一个对象的值.Object基类包含了__repr__()和__str__()的默认实现,它们提供了一个对象的字符串描述.遗憾的是,这些默认的实现不够详细.我们几乎总会想重写它们中的一个或两个.我们还会介绍__form

《Python面向对象编程指南》——2.8 __new__()方法和不可变对象

2.8 __new__()方法和不可变对象 __new__方法的一个用途是初始化不可变对象.__new__()方法中允许创建未初始化的对象.这允许我们在__init__()方法被调用之前先设置对象的属性. 由于不可变类的__init__()方法很难重载,因此__new__方法提供了一种扩展这种类的方法. 下面是一个错误定义的类,我们定义了float的一个包含单位信息的版本. class Float_Fail( float ): def __init__( self, value, unit ):

《Python面向对象编程指南》——2.5 __bytes__()方法

2.5 __bytes__()方法 只有很少的情景需要我们把对象转换为字节.在第2部分"持久化和序列化"中,我们会详细探讨这个主题. 通常,应用程序会创建一个字符串,然后使用Python的IO类内置的编码方法将字符串转换为字节.对于大多数情况,这种方法就足够了.只有当我们自定义一种新的字符串时,我们会需要定义这个字符串的编码方法. 依据不同的参数,bytes()函数的行为也不同. bytes(integer):返回一个不可变的字节对象,这个对象包含了给定数量的0x00值. bytes(

《Python面向对象编程指南》——2.2 __format__()方法

2.2 __format__()方法 string.format()和内置的format()函数都使用了__format__()方法.它们都是为了获得给定对象的一个符合要求的字符串表示. 下面是给__format__()传参的两种方式. someobject.__format__(""):当应用程序中出现format(someobject)或者"{0}".format(someobject)时,会默认以这种方式调用__format__().在这些情况下,会传递一个空

《Python面向对象编程指南》——1.3 在基类中实现init()方法

1.3 在基类中实现init()方法 通过实现__init()__方法来初始化一个对象.每当创建一个对象,Python会先创建一个空对象,然后调用该对象的__init()__函数.这个方法提供了对象内部变量以及其他一些一次性过程的初始化操作. 以下是关于一个Card类层次结构定义的一些例子.这里定义了一个基类和3个子类来描述Card类的基本信息.有两个变量是参数直接赋值的,另外两个参数是通过初始化方法计算来完成初始化的. class Card: def __init__( self, rank,