Python 中的 OOP - 第二部分。






4.91/5 (9投票s)
Python 中的面向对象编程 - 第二部分(继承)。
在本系列的第一部分中,我介绍了面向对象编程的三个支柱。我涵盖了封装,下一个重要主题是继承。
OOP 中的继承是什么?
继承是面向对象编程中的一个概念,它有助于程序员
-
建模 - 一种关系(并非在每种编程语言中都如此,有些情况只共享实现)
-
代码重用 - 帮助开发人员遵守DRY 原则,并重用代码中现有的实现和逻辑
-
扩展功能 - 有些情况下,无法更改使用类的源代码(其他人在使用它,或者它不是
public
,或者根本不允许);在这种情况下,可以通过应用继承来扩展类的功能。
Python 支持单继承和多继承。在 Python 2.x 和 3.x 中,继承的实现方式及其工作方式存在重大差异。我将提供的所有示例都适用于 Python 3.x。
Python 中的单继承
在 Python 中,可以通过 class MySubClass(MyBaseClass)
语法来实现继承。基本上,在新的类名后面的括号中,我指定了超类名。
我将坚持使用传统的例子,即将 Animal
作为基类(或称为超类),而不同的动物物种称为子类。
class Animal:
__name = None
__age = 0
__is_hungry = False
__nr_of_legs = 0
def __init__(self, name, age, is_hungry, nr_of_legs):
self.name = name
self.age = age
self.is_hungry = is_hungry
self.nr_of_legs = nr_of_legs
#
# METHODS
#
def eat(self, food):
print("{} is eating {}.".format(self.name, food))
#
# PROPERTIES
#
@property
def name(self):
return self.__name
@name.setter
def name(self,new_name):
self.__name = new_name
@property
def age(self):
return self.__age
@age.setter
def age(self,new_age):
self.__age = new_age
@property
def is_hungry(self):
return self.__is_hungry
@is_hungry.setter
def is_hungry(self,new_is_hungry):
self.__is_hungry = new_is_hungry
@property
def nr_of_legs(self):
return self.__nr_of_legs
@nr_of_legs.setter
def nr_of_legs(self,new_nr_of_legs):
self.__nr_of_legs = new_nr_of_legs
在 Animal
类中,我定义了 4 个 private
成员(__name
, __age
, __is_hungry
, __nr_of_legs
)。对于所有 4 个成员,我使用 OOP in Python 文章第一部分介绍的 @property
装饰器创建了属性。我创建了一个带有 4 个参数(不计算 self
参数)的构造函数,该构造函数使用属性来设置 private
成员的值。除了 4 个属性之外,我创建了一个名为 eat(self, food)
的方法,该方法打印 X is eating Y
,其中 X
是 animal
的名称,Y
是作为参数传入的食物。Animal
类作为 Snake
类的基类。
class Snake(Animal):
__temperature = 28
def __init__(self, name, temperature):
super().__init__(name, 2, True, 0)
self.temperature = temperature
#
# METHODS
#
def eat(self, food):
if food == "meat":
super().eat(food)
else:
raise ValueError
#
# PROPERTIES
#
@property
def temperature(self):
return self.__temperature
@temperature.setter
def temperature(self,new_temperature):
if new_temperature < 10 or new_temperature > 40:
raise ValueError
self.__temperature = new_temperature
Snake
类的构造函数接受 2 个参数,即 snake
的名称和它的 temperature
。对于 __temperature private
成员,我创建了一个属性,因此我在构造函数中使用它来存储传递给构造函数的值。在构造函数中,我首先使用 super()
关键字调用基类的构造函数(还有其他方法可以调用父类构造函数,但在 Python 3.x 中,这是推荐的方式)。调用基类构造函数时,我传入了一些预定义的值,例如 nr_of_legs
设置为零,因为蛇没有腿,而 is_hungry
设置为 True
,因为蛇通常比其他动物“更饿”。
与其他编程语言一样,我也可以在 Python 中重写方法。我重写了 eat
方法并添加了额外的逻辑。如果给 snake
的食物不是肉,我将引发一个 ValueError,否则我将调用在基类(Animal
)中定义的 eat
方法。
Python 中的多重继承
Python 允许我们从多个基类派生,这称为多重继承。当存在具有不同功能但这些功能可以组合并一起使用的类时,这可能很有用。
在 Python 中,从多个类继承的语法非常简单,只需在新类名称后面的括号中列出基类即可,例如:class MySubClass(MyBaseClass1, MyBaseClass2, MyBaseClass3)
。
class Phone:
def __init__(self):
print("Phone constructor invoked.")
def call_number(self, phone_number):
print("Calling number {}".format(phone_number))
class Computer:
def __init__(self):
print("Computer constructor invoked.")
def install_software(self, software):
print("Computer installing the {} software".format(software))
class SmartPhone(Phone,Computer):
def __init__(self):
super().__init__()
我定义了 3 个类(Phone
, Computer
, SmartPhone
),其中 2 个是基类(Phone
和 Computer
),1 个是派生类 SmartPhone
。逻辑上,这似乎是正确的,智能手机可以打电话,可以安装软件,因此 SmartPhone
类可以 call_number
(继承自 Phone
类)并且可以 install_software
(继承自 Computer
类)。
#
# will be discussed later why only the constructor of Phone class was invoked
#
>>> my_iphone = SmartPhone()
Phone constructor invoked.
>>> my_iphone.call_number("123456789")
Calling number 123456789
>>> my_iphone.install_software("python")
Computer installing the python software
如果查看 SmartPhone
类的构造函数,它非常简单,它调用了基类的构造函数。这是正确的,它调用了构造函数,问题是从哪个基类调用?从代码中可以看出,它只调用了 Phone
类的构造函数。问题是为什么?
解释并不简单,它与 Python 的 MRO(也称为方法解析顺序)有关。
Python 中的方法解析顺序
方法解析顺序 (MRO) 是一组规则,有助于定义和确定类的线性化。线性化(也称为优先级列表)是类的祖先(类)的列表,按从最近到最远的顺序排列。MRO 仅对允许多重继承的编程语言很重要。MRO 有助于编程语言处理菱形问题。在 Python 2.3 中,规则发生了一次根本性变化,有助于定义更具体的类 MRO(此版本使用C3 线性化),Michele Simionato 写了一篇关于方法和算法的非常好的帖子,该帖子虽然不短,但包含大量示例和算法的详细解释。Guido van Rossum 写了一篇关于 Python MRO 的详尽文章。Perl 编程语言也使用 C3 线性化算法来定义类的 MRO。
为了回答我最初的问题:为什么 super().__init__()
只调用 Phone
类的构造函数?那是因为,在调用 super().__init__()
时,基类的方法解析顺序解析为 Phone
类(MRO 中第一个父类)。MRO 中出现的其他基类的 __init__()
方法不会被调用。MRO 会影响基类构造函数的调用方式。在使用多重继承时,我作为开发人员必须确保我的基类得到正确初始化。我更新了 SmartPhone
类的代码,以确保 Computer
和 Phone
类都得到初始化(此外,我将 object
添加为 Phone
和 Computer
类的基类,以确保在 Python 2.x 中运行时获得相同的 MRO)。
class Phone(object):
def __init__(self):
print("Phone constructor invoked.")
def call_number(self, phone_number):
print("Calling number {}".format(phone_number))
class Computer(object):
def __init__(self):
print("Computer constructor invoked.")
def install_software(self, software):
print("Computer installing the {} software".format(software))
class SmartPhone(Phone, Computer):
def __init__(self):
Phone.__init__(self)
Computer.__init__(self)
现在创建新的 SmartPhone
类时,两个构造函数都会被执行
Python 3.2.3 (default, Feb 27 2014, 21:31:18)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from oop_multi import SmartPhone
>>> s = SmartPhone()
Phone constructor invoked.
Computer constructor invoked.
>>>
可以使用 mro()
方法或类的 __mro__
属性在 Python 中显示类的 MRO。
>>> SmartPhone.mro()
[<class 'SmartPhone'>, <class 'Phone'>, <class 'Computer'>, <class 'object'>]
>>> SmartPhone.__mro__
(<class 'SmartPhone'>, <class 'Phone'>, <class 'Computer'>, <class 'object'>)
MRO 中的第一个项目始终是我们对其调用 mro()
方法的类。在实际类之后,是 Phone
类,然后是 Computer
,最后,指定了所有这三个类的基类,即 object
。
在大型类层次结构的情况下定义 MRO 可能很困难,在某些情况下无法完成。如果基类之间存在交叉引用,Python 解释器将引发 TypeError
,例如
class Phone(object):
def __init__(self):
print("Phone constructor invoked.")
def call_number(self, phone_number):
print("Calling number {}".format(phone_number))
class Computer(object):
def __init__(self):
print("Computer constructor invoked.")
def install_software(self, software):
print("Computer installing the {} software".format(software))
class SmartPhone(Phone,Computer):
def __init__(self):
Phone.__init__(self)
Computer.__init__(self)
class Tablet(Computer,Phone):
def __init__(self):
Computer.__init__(self)
Phone.__init__(self)
class Phablet(SmartPhone,Tablet):
def __init__(self):
SmartPhone.__init__(self)
Tablet.__init__(self)
上面我创建了两个新类,Tablet
从 Computer
和 Phone
派生(注意与 SmartPhone
的顺序变化,其中我从 Phone
和 Computer
派生),以及 Phablet
从 SmartPhone
和 Tablet
派生。由于 SmartPhone
和 Tablet
类交叉引用了它们的基类(Phone
和 Computer
),Python 解释器将引发 TypeError
,因为 C3 线性化算法无法确定祖先的确切顺序。
>>> from mro_type_error import Phablet
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "mro_type_error.py", line 30, in <module>
class Phablet(SmartPhone, Tablet):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases Phone, Computer
>>>