>>> from env_helper import info; info()
页面更新时间: 2024-01-19 23:30:13
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-17-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

9.5. 构造函数

构造函数(constructor),其实就是本教程前面一些示例中使用的初始化方法,只是命名为 __init__ 。 然而,构造函数不同于普通方法的地方在于,将在对象创建后自动调用它们。因此,无需采用这种的做法:

>>> f = FooBar()
>>> f.init()

构造函数让你只需像下面这样做就可以创建对象同时进行初始化:

>>> f = FooBar()

在Python中,创建构造函数很容易,只需将方法 init 的名称从普通的 init 改为 __init__ 即可。 使用这个专有名称,在实例化对象后不需要再单独调用 __init__ 方法;尽量实际上

>>> class FooBar:
>>>     def __init__(self):
>>>         self.somevar = 42
>>> f = FooBar()
>>> f.somevar
42

如果给构造函数添加几个参数,结果将如何呢?请看下面的代码:

>>> class FooBar:
>>>     def __init__(self, value=42):
>>>         self.somevar = value

你认为该如何使用这个构造函数呢?由于参数是可选的,你可以当什么事都没发生,还像原 来那样做。但如果要指定这个参数(或者说如果这个参数不是可选的)呢?你肯定猜到了,不过这里还是演示一下。

>>> f = FooBar('This is a constructor argument')
>>> f.somevar
'This is a constructor argument'

在所有的Python魔法方法中, __init__() 绝对是你用得最多的。

注意 Python 提供了魔法方法 __del__() ,也称作析构函数(destructor)。 这个方法在对象被销毁(作为垃圾被收集)前被调用,但鉴于你无法知道准确的调用时间,建议尽可能不要使 用。

9.5.1. 重写普通方法和特殊的构造函数

前面介绍了继承。每个类都有一个或多个超类,并从它们那里继承行为。对类B的实例调用方法(或访问其属性)时,如果找不到该方法(或属性),将在其超类A中查找。请看下面两个类:

>>> class A:
>>>     def hello(self):
>>>         print("Hello, I'm A.")
>>> class B(A):
>>>     pass

类A定义了一个名为hello的方法,并被类B继承。下面的示例演示了这些类是如何工作的:

>>> a = A()
>>> b = B()
>>> a.hello()
Hello, I'm A.
>>> b.hello()
Hello, I'm A.

由于类B自己没有定义方法hello,因此对其调用方法hello时,打印的是消息”Hello, I’m A.”。 要在子类中添加功能,一种基本方式是添加方法。然而,你可能想重写超类的某些方法,以

定制继承而来的行为。例如,B可以重写方法hello,如下述修改后的类B定义所示:

>>> class B(A):
>>>     def hello(self):
>>>         print("Hello, I'm B.")

这样修改定义后,b.hello()的结果将不同。

>>> b = B()
>>> b.hello()
Hello, I'm B.

重写是继承机制的一个重要方面,对构造函数来说尤其重要。构造函数用于初始化新建对象 的状态,而对大多数子类来说,除超类的初始化代码外,还需要有自己的初始化代码。虽然所有 方法的重写机制都相同,但与重写普通方法相比,重写构造函数时更有可能遇到一个特别的问题: 重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。

请看下面的Bird类:

>>> class Bird:
>>>     def __init__(self):
>>>         self.hungry = True
>>>     def eat(self):
>>>         if self.hungry:
>>>             print('Aaaah ...')
>>>             self.hungry = False
>>>         else:
>>>             print('No, thanks!')

这个类定义了所有鸟都具备的一种基本能力:进食。下面的示例演示了如何使用这个类:

>>> b = Bird()
>>> b.eat()
Aaaah ...
>>> b.eat()
No, thanks!

从这个示例可知,鸟进食后就不再饥饿。下面来看子类SongBird,它新增了鸣叫功能。

>>> class SongBird(Bird):
>>>     def __init__(self):
>>>         self.sound = 'Squawk!'
>>>     def sing(self):
>>>         print(self.sound)

SongBird类使用起来与Bird类一样容易:

>>> sb = SongBird()
>>> sb.sing()
Squawk!

SongBird是Bird的子类,继承了方法eat,但如果你尝试调用它,将发现一个问题。

sb.eat()

Traceback (most recent call last): File "", line 1, in ?
File "birds.py", line 6, in eat if self.hungry:
AttributeError: SongBird instance has no attribute 'hungry'

异常清楚地指出了问题出在什么地方:SongBird没有属性hungry。为何会这样呢?因为在 SongBird中重写了构造函数,但新的构造函数没有包含任何初始化属性hungry的代码。要消除这 种错误,SongBird的构造函数必须调用其超类(Bird)的构造函数,以确保基本的初始化得以执 行。为此,有两种方法:调用未关联的超类构造函数,以及使用函数super。接下来的两节将介 绍这两种方法。

9.5.2. 调用未关联的超类构造函数

本节介绍的方法主要用于解决历史遗留问题。在较新的Python版本中,显然应使用函数 super(这将在下一节讨论)。然而,很多既有代码使用的都是本节介绍的方法,因此你必须对 其有所了解。另外,这种方法也极具启迪意义,淋漓尽致地说明了关联方法和未关联方法之间 的差别。

言归正传。如果你觉得本节的标题有点吓人,请放松心情。调用超类的构造函数实际上很容 易,也很有用。下面先给出前一节末尾问题的解决方案。

>>> class SongBird(Bird):
>>>     def __init__(self):
>>>         Bird.__init__(self)
>>>         self.sound = 'Squawk!'
>>>     def sing(self):
>>>         print(self.sound)

在SongBird类中,只添加了一行,其中包含代码Bird. init (self)。先来证明这确实管用, 再解释这到底意味着什么。

>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah ...
>>> sb.eat()
No, thanks!

这样做为何管用呢?对实例调用方法时,方法的参数self将自动关联到实例(称为关联的方 法),这样的示例你见过多个。然而,如果你通过类调用方法(如Bird. init ),就没有实例 与其相关联。在这种情况下,你可随便设置参数self。这样的方法称为未关联的。这就对本节的 标题做出了解释。

通过将这个未关联方法的self参数设置为当前实例,将使用超类的构造函数来初始化

SongBird对象。这意味着将设置其属性hungry。

9.5.3. 使用函数 super

如果你使用的不是旧版Python,就应使用函数super。这个函数只适用于新式类,而你无论 如何都应使用新式类。调用这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方 法时,调用的将是超类(而不是当前类)的方法。因此,在SongBird的构造函数中,可不使用Bird, 而是使用super(SongBird, self)。另外,可像通常那样(也就是像调用关联的方法那样)调用方 法 init 。在Python 3中调用函数super时,可不提供任何参数(通常也应该这样做),而它将 像变魔术一样完成任务。

下面是前述示例的修订版本:

>>> class Bird:
>>>     def __init__(self):
>>>         self.hungry = True
>>>     def eat(self):
>>>         if self.hungry:
>>>             print('Aaaah ...')
>>>             self.hungry = False
>>>         else:
>>>             print('No, thanks!')
>>> class SongBird(Bird):
>>>     def __init__(self):
>>>         super().__init__()
>>>         self.sound = 'Squawk!'
>>>     def sing(self):
>>>         print(self.sound)

这个新式版本与旧式版本等效:

>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah ...
>>> sb.eat()
No, thanks!

9.5.4. 使用函数super有何优点

在我看来,相比于直接对超类调用未关联方法,使用函数super更直观,但这并非其唯一 的优点。实际上,函数super很聪明,因此即便有多个超类,也只需调用函数super一次(条件 是所有超类的构造函数也使用函数super)。另外,对于使用旧式类时处理起来很棘手的问题(如两个超类从同一个类派生而来),在使用新式类和函数super时将自动得到处理。你无需知 道函数super的内部工作原理,但必须知道的是,使用函数super比调用超类的未关联构造函 数(或其他方法)要好得多。

函数super返回的到底是什么呢?通常,你无需关心这个问题,只管假定它返回你所需的 超类即可。实际上,它返回的是一个super对象,这个对象将负责为你执行方法解析。当你访 问它的属性时,它将在所有的超类(以及超类的超类,等等)中查找,直到找到指定的属性或 引发AttributeError异常。