65.9K
CodeProject 正在变化。 阅读更多。
Home

Python 中的 OOP - 第二部分。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (9投票s)

2014年11月26日

CPOL

6分钟阅读

viewsIcon

24966

Python 中的面向对象编程 - 第二部分(继承)。

在本系列的第一部分中,我介绍了面向对象编程的三个支柱。我涵盖了封装,下一个重要主题是继承。

OOP 中的继承是什么?

继承是面向对象编程中的一个概念,它有助于程序员

  1. 建模 - 一种关系(并非在每种编程语言中都如此,有些情况只共享实现)

  2. 代码重用 - 帮助开发人员遵守DRY 原则,并重用代码中现有的实现和逻辑

  3. 扩展功能 - 有些情况下,无法更改使用类的源代码(其他人在使用它,或者它不是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
>>>
© . All rights reserved.