构造函数(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)。
这个方法在对象被销毁(作为垃圾被收集)前被调用,
但鉴于无法知道准确的调用时间,建议尽可能不要使用。
前面介绍了继承。每个类都有一个或多个超类,并从它们那里继承行为。
对类 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
。接下来的两节将介 绍这两种方法。
本节介绍的方法主要用于解决历史遗留问题。在较新的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
。
如果使用的不是旧版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!
相比于直接对超类调用未关联方法,使用函数 super
更直观,但这并非其唯一 的优点。
实际上,函数 super
很聪明,因此即便有多个超类,
也只需调用函数 super
一次(条件 是所有超类的构造函数也使用函数 super
)。另外,
对于使用旧式类时处理起来很棘手的问题(如两个超类从同一个类派生而来),
在使用新式类和函数 super
时将自动得到处理。无需知道函数 super
的内部工作原理,
但必须知道的是,使用函数 super
比调用超类的未关联构造函 数(或其他方法)要好得多。
函数 super
返回的到底是什么呢?通常,无需关心这个问题,只管假定它返回所需的超类即可。
实际上,它返回的是一个 super
对象,这个对象将负责为执行方法解析。当访问它的属性时,
它将在所有的超类(以及超类的超类,等等)中查找,直到找到指定的属性或 引发 AttributeError
异常。