注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

__del__()方法

__del__()方法有一个毫不起眼的用例。

其目的是在对象从内存中被移除之前给对象一个机会做一些清理或终结工作。这个用例通过上下文管理对象和with语句会处理得更清晰。这是第五章《可调用和上下文的使用》的主要内容。创建上下文比用__del__()和Python垃圾收集算法处理起来更可预知。

如果Python对象有一个相关的操作系统资源,__del__()方法就是那个最后的机会,干净利落地从Python应用程序中彻底解决资源。例如,Python对象隐藏的一个打开的文件、安装好的设备、子进程,或许都能因为资源释放是__del__()处理的一部分而受益。

__del__()方法并不在任何容易预知的时刻调用。对象通过del语句删除时不会调用,也不因为名称空间被移除删除对象时而调用。文档中描述__del__()方法环境不稳定且在异常处理时需额外注意:异常在执行时会被忽略,替而代之的是由sys.stderr打印一个警告。

由于这些原因,上下文管理器通常更适合用来实现__del__()

1、引用计数和析构

对于CPython的实现,对象有一个引用计数。当对象赋给一个变量时计数增加,当变量被移除时减少。当引用计数为零,则不再需要该对象且可以销毁。对于简单的对象,__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

这个del语句没有响应。Noisy对象没有将它们的引用计数设为零;因为它们还被引用到其他地方,如下代码片段所示:

>>> del ln2
Removing 4313920336
Removing 4313920208

ln2变量是ln的浅拷贝列表。Noisy对象被两个列表引用。直到两个列表都被删除,引用计数减少为零之前它们是不能被销毁的。

有许多其他方法来创建浅拷贝。以下是创建对象浅拷贝的方法:

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

这里的重点是,我们经常被对象的引用数量所迷惑,因为浅拷贝在Python中普遍存在。

2、循环引用和垃圾收集

涉及到循环是一种常见的情况。一个Parent类,包含一组孩子。每个Child实例包含了一个Parent的引用。

我们将使用这两个类来看看循环引用:

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实例有一组简单list的孩子。

每个Child实例都有一个Parent类的引用。在初始化期间,孩子们被插入到父母的内部集合中,引用被创建。

我们创建的两个类相当于Noisy,当对象被删除时我们可以看到如下情形:

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

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

我们可以创建一个没有孩子的父母实例,如下代码片段所示:

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

和预期一样被删除。

因为共同的循环引用,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对象(例如,ID为0x101213910)是其中一个不可移除垃圾。为了减少引用计数到零,我们既需要更新每个垃圾清单上的Parent实例来移除孩子,又需要更新列表中每个Child实例来删除Parent实例的引用。

请注意,我们不能通过将代码放入__del__()方法中来打破这个循环。__del__()方法只能在循环被打破后且引用计数已经为零时调用。当我们有循环引用,我们可以不再依靠简单的Python引用计数来清除内存中未使用的对象。我们必须既显式地打破循环又使用允许垃圾收集的weakref引用。

3、循环引用和weakref模块

我们需要循环引用的情况下但也希望__del__()很好地工作的情况下,我们可以使用弱引用。循环引用的一个常见用例就是相互引用:父母有一组孩子;每个孩子都有一个引用回到父母。如果一个Player类有多个手,则Hand对象包含一个Player类的引用会很有帮助。

默认的对象引用可以称为强引用;然而,直接引用是一个更好的术语。它们被用于Python中的引用计数机制且如果引用计数不能删除对象,也能被垃圾收集器发现。它们不会被忽略。

强引用一个对象是如此直接。考虑下面的语句:

当我们说:

a = B()

a变量直接引用B类创建的对象。B实例的引用计数至少为1,因为有一个变量引用了。

弱引用是包含两个步骤来查找对象之间的联系。弱引用将使用x.parent(),调用弱引用作为一个可调用对象来跟踪实际的父母对象。这两步的过程允许引用计数或垃圾收集删除已引用的对象,让弱引用悬空。

weakref模块定义一组使用弱引用的集合来代替强引用。这允许我们去创建字典,例如,允许其他未使用对象的垃圾集合。

我们可以修改ParentChild类来使用弱引用从ChildParent,允许更简单的未使用对象的销毁。

以下是修改后的类,使用弱引用从ChildParent

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)))

我们已经改变了ChildParent的引用为weakref对象引用。

Child类中,我们必须通过两步操作找到Parent对象:

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

我们可以显式地检查确保引用的对象被发现。很有可能引用被悬空。

当我们使用这个新Parent2类,我们看到引用计数为0且对象被删除:

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

weakref引用死了(因为该引用被销毁),我们有三个潜在的响应:

  • 重新创建引用。也许会从数据库重新加载它。

  • 在内存垃圾收集器意外删除对象情况下使用warnings模块编写调试信息。

  • 忽略这个问题。

通常,weakref死了是因为对象的引用已被移除:变量超出范围,名称空间不再使用,应用程序关闭。出于这个原因,第三个响应是相当普遍的。对象试图创建引用很可能是要被删除。

4、__del__()和close()方法

__del__()最常见的用途是确保文件都已经关闭了。

一般,类定义打开文件将有类似下面显示的代码:

__del__ = close

这将确保__del__()方法同样也是close()方法。

任何比这更复杂的情况是最好使用上下文管理器。在第五章《使用可调用和上下文》中可以看到有关于上下文管理器的更多信息。

__new__()方法和不可变对象

__new__()方法的一个用例是用来初始化不可变对象。__new__()方法就是我们构建一个未初始化对象的地方。这允许在__init__()方法设置对象属性值之前进行处理。

__new__()方法用于扩展不可变类而__init__()方法不能轻易地被覆写。

以下是行不通的类。我们定义了携带单元相关信息版本的float

class Float_Fail(float):
    
    def __init__(self, value, unit):
        super().__init__(value)
        self.unit = unit

我们正在(错误地)初始化一个不可变对象。

以下是当我们试图使用这个类时发生的情形:

>>> s2 = Float_Fail(6.5, \"knots\")
Traceback (most recent call last):
  File \"\", line 1, in 
TypeError: float() takes at most 1 argument (2 given)

由此我们看到,我们不能轻易地覆写__init__()方法来建立内置不可变的float类。我们与所有其他不可变类一样有类似的问题。因为不变性定义,我们不能设置不可变对象、self的属性值。我们只能在对象构造时设置属性值。在这之后进入__new__()方法。

__new__()方法是一个很神奇的静态方法。这是真的不使用@staticmethod装饰器。它不使用self变量,它的工作就是创建对象且最终将被分配到self变量。

对于这个用例,方法签名是__new__(cls, *args, **kwa)cls参数是一个类实例必须创建的。对于下一节中的元类用例,args序列值比这更复杂。

__new__()的默认实现仅仅做如下操作:return super().__new__(cls)。它委托操作到超类。该工作结束委托到object.__new__(),构建所需类简单的、空对象。__new__()的参数和关键字,带有异常的cls参数,将被传递给__init__()作为标准的Python行为的一部分。

有两个值得注意的例外,这正是我们想要的。以下是一些例外:

  • 当我们想要子类化不可变类定义,稍后我们会深入研究。

  • 当我们需要创建一个元类。这是下一节的主题,和创建不可变对象完全不同。

相比在创建内置不可变类型的子类时覆写__init__(),我们必须在创建时通过覆写__new__()来调整对象。下面是一个示例类定义来展示我们扩展float的正确方法:

class Float_Units(float):
    
    def __new__(cls, value, unit):
        obj = super().__new__(cls, value)
        obj.unit = unit
        return obj

在前面的代码中,我们在创建对象时设置属性值。

以下代码片段给了我们一个附有单位信息的浮点值:

>>> speed = Float_Units(6.5, \"knots\")
>>> speed
6.5
>>> speed * 10
65.0
>>> speed.unit
\'knots\'

注意speed * 10表达式,没有创建Float_Units对象。这个类定义继承了来自float的所有操作符特殊方法;float运算特殊方法都能创建float对象。创建Float_Units对象是第七章《创造数字》的内容。

__new__()方法和元类

作为元类的一部分,__new__()的其他用例是如何控制构建一个类定义。这有别于__new__()如何控制构建一个不可变对象,如之前展示的那样。

元类构建类。一旦已经形成了一个类对象,类对象就被用于构建实例。所有类的元类定义是typetype()函数被用来创建类对象。

此外,type()函数可以用来揭示类对象。

下面是一个微不足道的例子用来构建一个新的、几乎没有直接使用type()类作为构造函数:

Useless= type(\"Useless\",(),{})

一旦我们创建这个类,我们可以创建这个Useless类的对象。然而,它们不会做太多因为没有方法或属性。

我们可以使用这个新创建的Useless类来创建对象。下面是一个例子:

>>> Useless()
<__main__.Useless object at 0x101001910>
>>> u = _
>>> u.attr = 1
>>> dir(u)
[\'__class__\', \'__delattr__\', \'__dict__\', \'__dir__\', \'__doc__\',
\'__eq__\', \'__format__\', \'__ge__\', \'__getattribute__\', \'__gt__\',
\'__hash__\', \'__init__\', \'__le__\', \'__lt__\', \'__module__\', \'__ne__\',
\'__new__\', \'__reduce__\', \'__reduce_ex__\', \'__repr__\', \'__setattr__\',
\'__sizeof__\', \'__str__\', \'__subclasshook__\', \'__weakref__\', \'attr\']

我们可以将属性添加到这个类对象。它工作得很好,最起码可以作为一个对象。

这几乎相当于使用type.SimpleNamespace或定义了一个类,如下所示:

class Useless:
    pass

这会带来严重的问题:我们为什么会在首先定义类的定义方式上混乱?

答案是,类的一些默认特性不能完美的适用于一些边缘情况。我们将讨论四个情况来介绍一下元类:

  • 我们可以使用元类来保存一些类的源文本的信息。通过内置type构建的类使用dict类型来存储各种方法和类级别属性。dict本质上是无序的,属性和方法的出现没有特定的顺序。在源处以最初的顺序呈现极其不可能的。我们将在第一个例子中展示。

  • 我们将从第4章到7章看到元类用来创建抽象基类(ABC)。依赖元类的__new__()方法来确认具体子类是完整的基础知识,我们将在第4章《一致设计的基本知识》介绍。

  • 可以使用元类来简化某些对象序列化。我们将在第9章《序列化和储蓄JSON、YAML、Pickle、CSV和XML》看到。

  • 作为最后且相比简单的例子,我们将看一个类里的自我引用。我们将设计类引用一个主类。这不是一个父类子类关系。这是一群同等的子类,但有与一个同等组里面的一个有联系且作为的主类。为了与同行保持一致,主类需要引用它本身,不可能没有元类。这将是我们的第二个例子。

1、元类示例1——已排序属性

这是Python语言参考3.3.3节创建自定义类的典型例子。这个元类将记录已定义的属性和方法函数的顺序。

有以下三个部分:

1、创建一个元类。__prepare__()函数和__new__()函数的元类将改变构建目标类的方式,通过OrderedDict类改变老式的dict类。

2、创建一个基于元类的抽象超类。这个抽象类简化其他类的继承。

3、创建抽象超类的子类,得益于元类。

下面是元类示例,将保留创建属性的顺序:

import collections

class Ordered_Attributes(type):
    
    @classmethod
    def __prepare__(metacls, name, bases, **kwds):
        return collections.OrderedDict()
       
    def __new__(cls, name, bases, namespace, **kwds):
        result = super().__new__(cls, name, bases, namespace)
        result._order = tuple(n for n in namespace if not n.startswith(\'__\'))
        return result

这个类通过新版本的__prepare__()__new__()来扩展内置的默认元类——type

__prepare__()方法在创建类之前执行,它的工作是创建初始定义的名称空间对象到被添加的定义中。这个方法可以在其他准备前执行类体正在处理。

__new__()静态方法在类体元素已经被添加到名称空间后执行。给出类对象、类名、超类元组,完整建立好的命名空间映射对象。这是个典型的例子:它委托__new__()的实际工作到超类;元类的超类是内置type;我们使用type.__new__()来创建可以调整的默认类对象。

该示例的__new__()方法添加了一个属性、_order到类定义中,给我们展示了属性原来的顺序。

我们可以使用这个元类来代替type当定义新的抽象超类,如下所示:

class Order_Preserved(metaclass=Ordered_Attributes):
    pass

然后我们可以使用这个新的抽象类作为任何新类的超类,如下:

class Something(Order_Preserved):
    
    this = \'text\'
    
    def z(self):
        return False
    
    b= \'order is preserved\'
    a= \'more text\'

当我们看到Something类,我们可以看到如下代码片段:

>>> Something._order
>>> (\'this\', \'z\', \'b\', \'a\')

我们可以考虑利用这些信息来正确的序列化对象或与源定义相关的提供调试信息。

2、元类示例2——自引用

我们来看一个例子,涉及到单位转换。例如,长度单位包括米、厘米、英寸、英尺和许多其他单位。管理单位转换可以算是一个挑战。表面上,我们需要一个在所有各种单位中可能的转换因子矩阵。英尺到米、英尺到英寸、英尺到脚码、米到英寸、米到码等等——每一个组合。

然而实际上,我们可以做得更好如果我们定义长度的标准单位。我们可以转换任何单位到标准且标准到其他单位。通过这样做,我们可以很容易的执行任何可能的转换为两步操作,消除了复杂的所有可能转换的矩阵:英尺到标准、英寸到标准、码到标准、米到标准。

在接下来的例子中,我们不打算以任何方式子类化floatnumbers.Number。比起绑单位到某个值,我们将允许每个值保留一个简单的数字。这是Flyweight设计模式的一个例子。类不定义包含相关值的对象。对象只包含转换因素。

另一种(绑定单位到值)会导致相当复杂的多维度分析。虽然有趣,但是它相当的复杂。

我们将定义两个类:UnitStandard_Unit。我们可以很容易地确定每个Unit类都有一个引用到相对应的Standard_Unit。我们如何确保每个Standard_Unit类都有一个引用到自身?类定义里的自引用是不可能的,因为类尚未定义。

以下是我们Unit类定义:

class Unit:
    \"\"\"Full name for the unit.\"\"\"
    factor = 1.0
    standard = None # Reference to the appropriate StandardUnit
    name = \"\" # Abbreviation of the unit\'s name.

    @classmethod
    def value(class_, value):
        if value is None: 
            return None
        return value / class_.factor
    
    @classmethod
    def convert(class_, value):
        if value is None: 
            return None
        return value * class_.factor

Unit.value()的意图是将一个给定单位转换为标准单位值。Unit.convert()方法是将一个标准单位值转换为给定的单位。

单位转换工作,如下面代码片段所示:

>>> m_f= FOOT.value(4)
>>> METER.convert(m_f)
1.2191999999999998

创建的值是内置float值。对于温度,value()convert()方法需要覆写,简单的乘法是行不通的。

对于Standard_Unit,我们可以如下操作:

class INCH:
    standard= INCH

然而,并没有什么用。INCH并没有在INCH类体内定义。类在定义前并不存在。

我们可以做这些作为备胎:

class INCH:
    pass
INCH.standard = INCH

然而,这相当让人讨厌。

我们可以定义装饰器,如下:

@standard
class INCH:
    pass

这个修饰器函数可以调整类定义来添加一个属性。我们将在第八章《修饰符和Mixins——交叉方面》。

因此,我们将定义可以插入循环引用到类定义的元类,如下:

class UnitMeta(type):

    def __new__(cls, name, bases, dict):
        new_class= super().__new__(cls, name, bases, dict)
        new_class.standard = new_class
        return new_class

这就迫使将类变量standard加入到类定义。

对于大多数单位,SomeUnit.standard引用TheStandardUnit类。同时我们也有TheStandardUnit.standard引用TheStandardUnit类。这种Unit和子类Standard_Unit中一致的结构可以帮助编写文档和单位转换自动化。

下面是Standard_Unit类:

class Standard_Unit(Unit, metaclass=UnitMeta):
    pass

单位转换因子1.0继承自Unit,所以这个类对于提供的值不做任何事情。它包含了特殊的元类定义,这样它将会有一个自引用来阐明这个类是标准的对于特别尺寸规格。

作为最优选择,我们可以覆写value()convert()方法来避免乘法和除法。

以下是一些单位类定义的示例:

class INCH(Standard_Unit):
    \"\"\"Inches\"\"\"
    name = \"in\"

class FOOT(Unit):
    \"\"\"Feet\"\"\"
    name = \"ft\"
    standard = INCH
    factor = 1 / 12

class CENTIMETER(Unit):
    \"\"\"Centimeters\"\"\"
    name = \"cm\"
    standard = INCH
    factor = 2.54

class METER(Unit):
    \"\"\"Meters\"\"\"
    name = \"m\"
    standard = INCH
    factor = .0254

我们定义INCH为标准单位。其他单位的定义将转换到英寸和从英寸转换过来。

我们为每个单位提供了一些文档:文档字符串全名和一个短名称的name属性。转换因子自动应用继承自Unitconvert()value()函数。

在我们的应用程序中,这些定义允许以下类型的编程:

>>> x_std= INCH.value(159.625)
>>> FOOT.convert(x_std)
13.302083333333332
>>> METER.convert(x_std)
4.054475
>>> METER.factor
0.0254

我们可以给英寸设置一个特定的尺寸,然后报告该值给任何其他兼容的单位。

元类所做的就是让我们从单元类定义中做下面这样的查询:

>>> INCH.standard.__name__
\'INCH\'
>>> FOOT.standard.__name__
\'INCH\'

这些类型的引用可以让我们跟踪所有给定规格的各种单位。

总结

我们看了许多基本的特殊方法,这是我们设计的任何类的基本特征。这些方法已经是每个类的一部分,但默认继承自对象可能不符合我们的处理需求。

我们几乎总是需要覆写__repr__()__str__()__foramt__()。这些方法的默认实现不是很有帮助。

比较罕见的是如果我们编写自己的集合需要覆写__bool__()。这是第六章《创建容器和集合》的主题。

我们经常需要覆写比较关系和__hash__()方法。定义仅适用于简单的不可变对象但不适合可变对象。当然我们可能不需要编写所有的比较运算符;我们将在第八章《修饰符和Mixins——交叉方面》看看@functools.total_ordering装饰器。

其他两个基本的特殊方法名称,__new__()__del__(),都用于专门的目的。使用__new__()来扩展一个不可变类对它来说是最常见的用例。

这些基本的特殊方法,连同__init__(),将几乎出现在每一个我们定义的类中。其余的特殊方法都用于专门的目的;它们分为六个类别:

  • 属性访问:这些特殊的方法实现我们所看到的object.attribute表达式,object.attribute左边的赋值和在del语句中的object.attribute

  • 可调用:一个特殊的方法实现我们所看到的,函数作为一个参数来应用,就像内置len()函数一样。

  • 集合:这些特殊的方法实现众多集合的特性。包括诸如sequence[index]mapping[key]set | set

  • 数字:这些特殊方法提供了算术运算符和比较运算符。我们可以用这些方法来扩展Python使用的数字领域。

  • 上下文:通过with语句有两个特殊的方法实现一个上下文管理器。

  • 迭代器:有特殊方法定义迭代器。这不是必要的,作为生成器函数可以非常优雅的处理这个特性。如论如何,我们都要看看如何设计自己的迭代器。

在下一章,我们将解说属性、特性和描述符。