13.5 实例

如果说类是一种数据结构定义类型,那么实例则声明了一个这种类型的变量。换言之,实例是有生命的类。就像设计完一张蓝图后,就是设法让它成为现实。实例是那些主要用在运行期时的对象,类被实例化得到实例,该实例的类型就是这个被实例化的类。在Python 2.2版本之前,实例是“实例类型”,而不考虑它从哪个类而来。

13.5.1 初始化:通过调用类对象来创建实例

很多其他的OO语言都提供new关键字,通过new可以创建类的实例。Python的方式更加简单。一旦定义了一个类,创建实例比调用一个函数还容易——不费吹灰之力。实例化的实现,可以使用函数操作符,如下示:

13.5 实例 - 图1

可以看到,仅调用“calling”类:MyClass(),就创建了类MyClass的实例mc。返回的对象是你所调用类的一个实例。当使用函数记法来调用“call”一个类时,解释器就会实例化该对象,并且调用Python所拥有与构造函数最相近的东西(如果你定义了的话)来执行最终的定制工作,比如设置实例属性,最后将这个实例返回给你。

13.5 实例 - 图2核心笔记:Python2.2前后的类和实例

类和类型在2.2版本中就统一了,这使得Python的行为更像其他面向对象编程语言。任何类或者类型的实例都是这种类型的对象。比如,如果你让Python告诉你,类MyClass的实例mc是否是类MyClass的一个实例。回答是肯定的,Python不会说谎。同样,它会告诉你零是integer类型的一个实例:

13.5 实例 - 图3

但如果你仔细看,比较MyClass和int,你将会发现二者都是类型(type):

13.5 实例 - 图4

对比一下,如果在Python早于2.2版本时,使用经典类,此时类是类对象,实例是实例对象。在这两个对象类型之间没有任何关系,除了实例的class属性引用了被实例化以得到该实例的类。把MyClass在Python2.1版本中作为经典类重新定义,并运行相同的调用(注意:int()那时还不具备工厂功能……它还仅是一个通常的内建函数):

13.5 实例 - 图5

为了避免任何混淆,你只要记住当你定义一个类时,你并没有创建一个新的类型,而是仅仅一个类对象;而对2.2版本及后续版本,当你定义一个(新式的)类后,你已创建了一个新的类型。

13.5.2 init() “构造器”方法

当类被调用,实例化的第一步是创建实例对象。一旦对象创建了,Python检查是否实现了init()方法。默认情况下,如果没有定义(或覆盖)特殊方法init(),对实例不会施加任何特别的操作。任何所需的特定操作,都需要程序员实现init(),覆盖它的默认行为。如果init()没有实现,则返回它的对象,实例化过程完毕。

然而,如果init()已经被实现,那么它将被调用,实例对象作为第一个参数(self)被传递进去,像标准方法调用一样。调用类时,传进的任何参数都交给了init()。实际中,你可以想像成这样:把创建实例的调用当成是对构造器的调用。

总之,(a)你没有通过调用new来创建实例,你也没有定义一个构造器。是Python为你创建了对象;(b) init(),是在解释器为你创建一个实例后调用的第一个方法,在你开始使用它之前,这一步可以让你做些准备工作。

init()是很多为类定义的特殊方法之一。其中一些特殊方法是预定义的,缺省情况下,不进行任何操作,比如init(),要定制,就必须对它进行重载,还有些方法,可能要按需要去实现。本章中,我们会讲到很多这样的特殊方法。你将会经常看到init()的使用,在此,就不举例说明了。

13.5.3 new()“构造器”方法

init()相比,new()方法更像一个真正的构造器。类型和类在版本2.2就统一了,Python用户可以对内建类型进行派生,因此,需要一种途径来实例化不可变对象,比如派生字符串、数字等。

在这种情况下,解释器则调用类的new()方法,一个静态方法,并且传入的参数是在类实例化操作时生成的。new()会调用父类的new()来创建对象(向上代理)。

为何我们认为new()比init()更像构造器呢?这是因为new()必须返回一个合法的实例,这样解释器在调用init()时,就可以把这个实例作为self传给它。调用父类的new()来创建对象,正像其他语言中使用new关键字一样。

new()和init()在类创建时,都传入了(相同)参数。13.11.3节中有个例子使用了new()。

13.5.4 del()“解构器”方法

同样,有一个相应的特殊解构器(destructor)方法名为del()。然而,由于Python具有垃圾对象回收机制(靠引用计数),这个函数要直到该实例对象所有的引用都被清除掉后才会执行。Python中的解构器是在实例释放前提供特殊处理功能的方法,它们通常没有被实现,因为实例很少被显式释放。

在下面的例子中,我们分别创建(并覆盖)init()和del()构造及解构函数,然后,初始化类并给同样的对象分配很多别名。id()内建函数可用来确定引用同一对象的三个别名。最后一步是使用del语句清除所有的别名,显示何时调用了多少次解构器。

13.5 实例 - 图6

注意,在上面的例子中,解构器是在类C实例所有的引用都被清除掉后,才被调用的,比如,当引用计数已减少到0。如果你预期你的del()方法会被调用,却实际上没有被调用,这意味着,你的实例对象由于某些原因,其引用计数不为0,这可能有别的对它的引用,而你并不知道这些让你的对象还活着的引用所在。

另外,要注意,解构器只能被调用一次,一旦引用计数为0,则对象就被清除了。这非常合理,因为系统中任何对象都只被分配及解构一次。

总结:

  • 不要忘记首先调用父类的del()。

  • 调用del x不表示调用了x.del()——前面也看到,它仅仅是减少x的引用计数。

  • 如果你有一个循环引用或其他的原因,让一个实例的引用逗留不去,该对象的del()可能永远不会被执行。

  • del()未捕获的异常会被忽略掉(因为一些在del()用到的变量或许已经被删除了)。不要在del()中干与实例没任何关系的事情。

  • 除非你知道你正在干什么,否则不要去实现del()。

  • 如果你定义了del,并且实例是某个循环的一部分,垃圾回收器将不会终止这个循环——你需要自己显式调用del。

13.5 实例 - 图7核心笔记:跟踪实例

Python没有提供任何内部机制来跟踪一个类有多少个实例被创建了,或者记录这些实例是些什么东西。如果需要这些功能,你可以显式加入一些代码到类定义或者init()和del()中去。最好的方式是使用一个静态成员来记录实例的个数。靠保存它们的引用来跟踪实例对象是很危险的,因为你必须合理管理这些引用,不然你的引用可能没办法释放(因为还有其他的引用)!看下面一个例子:

13.5 实例 - 图8