Skip to content

Latest commit

 

History

History
1136 lines (848 loc) · 63.6 KB

File metadata and controls

1136 lines (848 loc) · 63.6 KB

三、当对象相似时

在编程界,重复代码被认为是邪恶的。我们不应该在不同的地方有相同或相似代码的多个副本。

有许多方法可以合并具有类似功能的代码片段或对象。在本章中,我们将介绍最著名的面向对象原则:继承。如第 1 章面向对象设计所述,继承允许我们创建是两个或多个类之间的关系,将公共逻辑抽象为超类,并管理子类中的特定细节。特别是,我们将介绍以下方面的 Python 语法和原则:

  • 基本遗传
  • 从内置继承
  • 多重继承
  • 多态性与鸭子分型

基本遗传

从技术上讲,我们创建的每个类都使用继承。所有 Python 类都是名为object的特殊类的子类。这个类在数据和行为方面提供的很少(它提供的行为都是仅供内部使用的双下划线方法),但是它允许 Python 以相同的方式处理所有对象。

如果我们没有显式地从其他类继承,我们的类将自动从object继承。但是,我们可以使用以下语法公开声明我们的类源自object

class MySubClass(object):
    pass

这就是遗产!从技术上讲,这个例子与我们在第 2 章中的第一个例子,Python 中的对象没有什么不同,因为如果我们不显式地提供不同的超类,Python 3 会自动从object继承。超类或父类是从继承的类。子类是从超类继承的类。在这种情况下,超类是object,而MySubClass是子类。子类也可以说是从其父类派生的,或者说子类扩展了父类。

您可能已经从这个示例中了解到,继承在基本类定义上需要最少的额外语法。只需将父类的名称包含在类名称后面的括号中,但在终止类定义的冒号之前。这就是我们要告诉 Python 新类应该从给定的超类派生出来所要做的一切。

我们如何在实践中应用继承?继承最简单也是最明显的用途是向现有类添加功能。让我们从一个简单的联系人管理器开始,它跟踪几个人的姓名和电子邮件地址。contact 类负责维护类变量中所有联系人的列表,并初始化单个联系人的姓名和地址:

class Contact:
    all_contacts = []

    def __init__(self, name, email):
        self.name = name
        self.email = email
        Contact.all_contacts.append(self)

这个例子向我们介绍了类变量。all_contacts列表是类定义的一部分,因此该类的所有实例都共享该列表。这意味着只有一个Contact.all_contacts列表,我们可以将其作为Contact.all_contacts访问。不太明显的是,我们也可以在从Contact实例化的任何对象上以self.all_contacts的形式访问它。如果在对象上找不到该字段,那么它将在类上找到,从而引用同一个列表。

提示

小心使用这种语法,因为如果您曾经使用self.all_contacts设置变量,您实际上将创建一个仅与该对象关联的实例变量。类变量仍将保持不变,并可作为Contact.all_contacts访问。

这是一个简单的类,允许我们跟踪关于每个联系人的几条数据。但是,如果我们的一些联系人也是我们需要向其订购供应品的供应商呢?我们可以在Contact类中添加order方法,但这将允许人们意外地从客户或家庭朋友的联系人处订购物品。相反,让我们创建一个新的Supplier类,它的行为类似于我们的Contact类,但有一个额外的order方法:

class Supplier(Contact):
    def order(self, order):
        print("If this were a real system we would send "
                "'{}' order to '{}'".format(order, self.name))

现在,如果我们在我们信任的解释器中测试这个类,我们会看到所有联系人,包括供应商,在他们的__init__中接受一个名称和电子邮件地址,但只有供应商有一个功能订单方法:

>>> c = Contact("Some Body", "somebody@example.net")
>>> s = Supplier("Sup Plier", "supplier@example.net")
>>> print(c.name, c.email, s.name, s.email)
Some Body somebody@example.net Sup Plier supplier@example.net
>>> c.all_contacts
[<__main__.Contact object at 0xb7375ecc>,
 <__main__.Supplier object at 0xb7375f8c>]
>>> c.order("I need pliers")
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'Contact' object has no attribute 'order'
>>> s.order("I need pliers")
If this were a real system we would send 'I need pliers' order to
'Sup Plier '

因此,现在我们的Supplier类可以做联系人可以做的一切(包括将自己添加到all_contacts列表中)以及作为供应商需要处理的所有特殊事情。这就是继承之美。

扩展内置程序

这种继承的一个有趣用途是向内置类添加功能。在前面看到的Contact类中,我们将联系人添加到所有联系人的列表中。如果我们还想按姓名搜索该列表呢?我们可以在Contact类中添加一个方法来搜索它,但感觉这个方法实际上属于列表本身。我们可以使用继承来实现这一点:

class ContactList(list):
    def search(self, name):
        '''Return all contacts that contain the search value
        in their name.'''
        matching_contacts = []
        for contact in self:
            if name in contact.name:
                matching_contacts.append(contact)
        return matching_contacts

class Contact:
    all_contacts = ContactList()

    def __init__(self, name, email):
        self.name = name
        self.email = email
        self.all_contacts.append(self)

我们没有将普通列表实例化为类变量,而是创建了一个新的ContactList类来扩展内置的list。然后,我们将这个子类实例化为我们的all_contacts列表。我们可以按如下方式测试新的搜索功能:

>>> c1 = Contact("John A", "johna@example.net")
>>> c2 = Contact("John B", "johnb@example.net")
>>> c3 = Contact("Jenna C", "jennac@example.net")
>>> [c.name for c in Contact.all_contacts.search('John')]
['John A', 'John B']

您想知道我们是如何将内置语法[]更改为可以继承的东西的吗?使用[]创建空列表实际上是使用list()创建空列表的简写;这两个语法的行为相同:

>>> [] == list()
True

实际上,[]语法实际上是所谓的语法糖,它在引擎盖下调用list()构造函数。list数据类型是一个我们可以扩展的类。事实上,列表本身扩展了object类:

>>> isinstance([], object)
True

作为第二个示例,我们可以扩展dict类,类似于列表,使用{}语法速记时构造的类:

class LongNameDict(dict):
    def longest_key(self):
        longest = None
        for key in self:
            if not longest or len(key) > len(longest):
                longest = key
        return longest

这很容易在交互式解释器中测试:

>>> longkeys = LongNameDict()
>>> longkeys['hello'] = 1
>>> longkeys['longest yet'] = 5
>>> longkeys['hello2'] = 'world'
>>> longkeys.longest_key()
'longest yet'

大多数内置类型都可以进行类似的扩展。常用的扩展内置程序有objectlistsetdictfilestr。例如intfloat等数字类型也偶尔从中继承。

超控超控

所以,继承对于向现有类添加新行为来说非常重要,但是如何改变行为呢?我们的contact类只允许名称和电子邮件地址。这对于大多数联系人来说可能已经足够了,但是如果我们想为好友添加一个电话号码呢?

正如我们在第 2 章中所看到的,Python 中的对象,我们只需在联系人构造后设置一个phone属性,就可以轻松实现这一点。但是,如果我们想使第三个变量在初始化时可用,我们必须重写__init__。重写意味着在子类中更改或用新方法(同名)替换超类的方法。这样做不需要特殊的语法;将自动调用子类的新创建方法,而不是超类的方法。例如:

class Friend(Contact):
    def __init__(self, name, email, phone):
        self.name = name
        self.email = email
        self.phone = phone

可以覆盖任何方法,而不仅仅是__init__。然而,在继续之前,我们需要解决本例中的一些问题。我们的ContactFriend类有重复的代码来设置nameemail属性;这会使代码维护变得复杂,因为我们必须在两个或多个地方更新代码。更令人担忧的是,我们的Friend类忽略了将自己添加到我们在Contact类上创建的all_contacts列表中。

我们真正需要的是一种在Contact类上执行原始__init__方法的方法。这就是super函数的作用;它将对象作为父类的实例返回,允许我们直接调用父方法:

class Friend(Contact):
    def __init__(self, name, email, phone):
        super().__init__(name, email)
        self.phone = phone

本例首先使用super获取父对象的实例,并对该对象调用__init__,传递预期参数。然后它进行自己的初始化,即设置phone属性。

请注意,super()语法在较早版本的 Python 中不起作用。与列表和字典的[]和{}语法一样,它是更复杂结构的缩写。稍后我们将在讨论多重继承时进一步了解这一点,但现在要知道,在 Python2 中,您必须调用super(EmailContact, self).__init__()。请特别注意,第一个参数是子类的名称,而不是您想要调用的父类的名称,正如一些人所期望的那样。另外,请记住类位于对象之前。我总是忘记顺序,所以 Python3 中的新语法节省了我数小时的查找时间。

super()调用可以在任何方法中进行,而不仅仅是__init__。这意味着可以通过重写和调用super来修改所有方法。对super的调用也可以在方法中的任意点进行;我们不必在方法的第一行进行调用。例如,我们可能需要在将传入参数转发到超类之前对其进行操作或验证。

多重遗传

多重继承是一个敏感的话题。原则上,它非常简单:从多个父类继承的子类能够从两个父类访问功能。实际上,这没有听起来那么有用,许多专家程序员建议不要使用它。

提示

根据经验,如果你认为你需要多重继承,你可能是错的,但如果你知道你需要多重继承,你可能是对的。

最简单、最有用的多重继承形式称为混合蛋白。mixin 通常是一个超类,它并不意味着独立存在,而是意味着被其他类继承以提供额外的功能。例如,假设我们想向Contact类添加允许向self.email发送电子邮件的功能。发送电子邮件是我们可能希望在许多其他类上使用的一项常见任务。因此,我们可以编写一个简单的 mixin 类来为我们发送电子邮件:

class MailSender:
    def send_mail(self, message):
        print("Sending mail to " + self.email)
        # Add e-mail logic here

为简洁起见,这里不包括实际的电子邮件逻辑;如果您有兴趣研究它是如何完成的,请参阅 Python 标准库中的smtplib模块。

这个类没有做任何特殊的事情(事实上,它几乎不能作为一个独立的类运行),但是它允许我们定义一个新的类,它使用多重继承来描述一个Contact和一个MailSender

class EmailableContact(Contact, MailSender):
    pass

多重继承的语法类似于类定义中的参数列表。我们没有在括号中包含一个基类,而是包含两个(或更多)基类,用逗号分隔。我们可以测试这种新的混合动力车,看看混合动力车的性能:

>>> e = EmailableContact("John Smith", "jsmith@example.net")
>>> Contact.all_contacts
[<__main__.EmailableContact object at 0xb7205fac>]
>>> e.send_mail("Hello, test e-mail here")
Sending mail to jsmith@example.net

Contact初始值设定项仍在all_contacts列表中添加新联系人,并且 mixin 能够向self.email发送邮件,因此我们知道一切正常。

这并不难,您可能想知道关于多重继承的可怕警告是什么。我们将在一分钟内进入复杂,但让我们考虑一些其他的选择,而不是在这里使用 Mixin:

  • 我们本可以使用单继承并将send_mail函数添加到子类中。这里的缺点是,对于需要电子邮件的任何其他类,都必须复制电子邮件功能。
  • 我们可以创建一个用于发送电子邮件的独立 Python 函数,并在需要发送电子邮件时使用作为参数提供的正确电子邮件地址调用该函数。
  • 我们本可以探索一些使用组合而不是继承的方法。例如,EmailableContact可以有一个MailSender对象,而不是从中继承。
  • 我们可以在第 7 章Python 面向对象快捷方式中简要介绍猴子补丁(monkey patch),Contact类在创建类后有一个send_mail方法。这是通过定义一个接受self参数的函数,并将其设置为现有类的属性来实现的。

当混合来自不同类的方法时,多重继承可以正常工作,但是当我们必须在超类上调用方法时,它会变得非常混乱。有多个超类。我们怎么知道该叫哪一个?我们怎么知道叫他们进来的顺序?

让我们通过在Friend类中添加家庭地址来探索这些问题。我们可以采取一些方法。地址是代表街道、城市、国家和联系人的其他相关详细信息的字符串集合。我们可以将这些字符串作为参数传递给Friend类的__init__方法。我们还可以将这些字符串存储在元组或字典中,并将它们作为单个参数传递到__init__。如果没有需要添加到地址的方法,那么这可能是最好的做法。

另一种选择是创建一个新的Address类来将这些字符串保存在一起,然后将该类的一个实例传递给Friend类的__init__方法。此解决方案的优点是,我们可以向数据中添加行为(例如,提供方向或打印地图的方法),而不仅仅是静态存储数据。这是一个组合示例,正如我们在第 1 章面向对象设计中所讨论的。“has-a”组合关系是解决此问题的一个完全可行的解决方案,它允许我们在其他实体(如建筑物、企业或组织)中重用Address类。

然而,继承也是一个可行的解决方案,这就是我们想要探索的。让我们添加一个包含地址的新类。我们将这个新类称为“AddressHolder”而不是“Address”,因为继承定义了一个是一个关系。说“朋友”是“地址”是不正确的,但既然朋友可以有“地址”,我们可以说“朋友”是“地址持有者”。稍后,我们可以创建其他实体(公司、建筑物)来保存地址。这是我们的AddressHolder课程:

class AddressHolder:
    def __init__(self, street, city, state, code):
        self.street = street
        self.city = city
        self.state = state
        self.code = code

很简单,;我们只是获取所有数据,并在初始化时将其放入实例变量中。

钻石问题

我们可以使用多重继承将这个新类添加为现有Friend类的父类。棘手的是,我们现在有两个父__init__方法,这两个方法都需要初始化。它们需要用不同的参数初始化。我们如何做到这一点?好吧,我们可以从一个天真的方法开始:

class Friend(Contact, AddressHolder):
    def __init__(
        self, name, email, phone,street, city, state, code):
        Contact.__init__(self, name, email)
        AddressHolder.__init__(self, street, city, state, code)
        self.phone = phone

在本例中,我们直接对每个超类调用__init__函数,并显式传递self参数。这个例子在技术上有效;我们可以直接在类上访问不同的变量。但也有一些问题。

首先,如果我们忽略显式调用初始值设定项,超类可能会未初始化。这不会破坏这个示例,但在常见场景中可能会导致难以调试的程序崩溃。例如,设想尝试将数据插入到尚未连接的数据库中。

第二,更危险的是,由于类层次结构的组织,超类可能被多次调用。请看这个继承关系图:

The diamond problem

来自Friend类的__init__方法首先调用Contact上的__init__,它隐式初始化object超类(记住,所有类都派生自objectFriend然后调用AddressHolder上的__init__,这会再次隐式初始化object超类*。这意味着父类已经设置了两次。对于object类,这相对来说是无害的,但在某些情况下,它可能意味着灾难。想象一下,对于每个请求,尝试两次连接到数据库!*

*基类只能调用一次。一次,是的,但是什么时候?我们会先叫Friend,然后叫Contact,然后叫Object,然后叫AddressHolder吗?或者Friend,然后Contact,然后AddressHolder,然后Object

提示

通过修改类上的__mro__方法解析顺序)属性,可以动态调整方法的调用顺序。这超出了本书的范围。如果您认为需要了解,我推荐Python 编程专家Tarek ZiadéPackt Publishing】,或者在阅读关于该主题的原始文档 http://www.python.org/download/releases/2.3/mro/

让我们看第二个人为的例子,它更清楚地说明了这个问题。这里我们有一个基类,它有一个名为call_me的方法。两个子类重写该方法,然后另一个子类使用多重继承扩展这两个方法。这被称为钻石继承,因为类图的钻石形状:

The diamond problem

让我们将此图转换为代码;此示例显示了调用方法的时间:

class BaseClass:
    num_base_calls = 0
    def call_me(self):
        print("Calling method on Base Class")
        self.num_base_calls += 1

class LeftSubclass(BaseClass):
    num_left_calls = 0
    def call_me(self):
        BaseClass.call_me(self)
        print("Calling method on Left Subclass")
        self.num_left_calls += 1

class RightSubclass(BaseClass):
    num_right_calls = 0
    def call_me(self):
        BaseClass.call_me(self)
        print("Calling method on Right Subclass")
        self.num_right_calls += 1

class Subclass(LeftSubclass, RightSubclass):
    num_sub_calls = 0
    def call_me(self):
        LeftSubclass.call_me(self)
        RightSubclass.call_me(self)
        print("Calling method on Subclass")
        self.num_sub_calls += 1

这个例子简单地确保每个被重写的call_me方法直接调用具有相同名称的父方法。它通过将信息打印到屏幕上,让我们知道每次调用方法时的情况。它还更新类上的静态变量,以显示调用了多少次。如果我们实例化一个Subclass对象并对其调用一次方法,我们将得到以下输出:

>>> s = Subclass()
>>> s.call_me()
Calling method on Base Class
Calling method on Left Subclass
Calling method on Base Class
Calling method on Right Subclass
Calling method on Subclass
>>> print(
... s.num_sub_calls,
... s.num_left_calls,
... s.num_right_calls,
... s.num_base_calls)
1 1 1 2

因此,我们可以清楚地看到基类的call_me方法被调用了两次。这可能会导致一些潜在的错误,如果这种方法是做实际工作,如存入银行帐户两次。

多重继承需要记住的是,我们只想调用类层次结构中的“next”方法,而不是“parent”方法。事实上,下一个方法可能不在当前类的父类或父类上。super关键词再次拯救了我们。事实上,super最初是为了使复杂形式的多重遗传成为可能而开发的。以下是使用super编写的相同代码:

class BaseClass:
    num_base_calls = 0
    def call_me(self):
        print("Calling method on Base Class")
        self.num_base_calls += 1

class LeftSubclass(BaseClass):
    num_left_calls = 0
    def call_me(self):
        super().call_me()
        print("Calling method on Left Subclass")
        self.num_left_calls += 1

class RightSubclass(BaseClass):
    num_right_calls = 0
    def call_me(self):
        super().call_me()
        print("Calling method on Right Subclass")
        self.num_right_calls += 1

class Subclass(LeftSubclass, RightSubclass):
    num_sub_calls = 0
    def call_me(self):
        super().call_me()
        print("Calling method on Subclass")
        self.num_sub_calls += 1

变化很小;我们简单地用对super()的调用替换了天真的直接调用,尽管底层子类只调用super一次,而不必同时调用左侧和右侧。更改非常简单,但在执行时请注意区别:

>>> s = Subclass()
>>> s.call_me()
Calling method on Base Class
Calling method on Right Subclass
Calling method on Left Subclass
Calling method on Subclass
>>> print(s.num_sub_calls, s.num_left_calls, s.num_right_calls,
s.num_base_calls)
1 1 1 1

看起来不错,我们的基本方法只被调用一次。但是super()到底在这里干什么?由于print语句是在super调用之后执行的,所以打印的输出是按照每个方法实际执行的顺序进行的。让我们从后到前查看输出,看看谁在调用什么。

首先,Subclass中的call_me调用super().call_me(),正好是指LeftSubclass.call_me()。然后,LeftSubclass.call_me()方法调用super().call_me(),但在本例中,super()指的是RightSubclass.call_me()

需要特别注意的是:super调用是而不是调用LeftSubclass超类(即BaseClass上的方法。相反,它正在呼叫RightSubclass,尽管它不是LeftSubclass的直接父代!这是下一个方法,而不是父方法。RightSubclass然后调用BaseClasssuper调用确保类层次结构中的每个方法执行一次。

不同的参数集

当我们回到Friend多重继承示例时,这将使事情变得复杂。在Friend__init__方法中,我们最初为两个父类调用__init__,使用不同的参数集:

Contact.__init__(self, name, email)
AddressHolder.__init__(self, street, city, state, code)

使用super时,我们如何管理不同的参数集?我们不一定知道哪个类super将首先尝试初始化。即使我们这样做了,我们也需要一种方法来传递“额外”参数,以便在其他子类上对super的后续调用接收正确的参数。

具体来说,如果第一次调用supernameemail参数传递给Contact.__init__,然后Contact.__init__调用super,则需要能够将与地址相关的参数传递给“下一个”方法,即AddressHolder.__init__

每当我们想要用相同的名称调用超类方法,但使用不同的参数集时,这就是一个问题。最常见的情况是,您希望调用具有完全不同参数集的超类的唯一时间是在__init__中,就像我们在这里所做的那样。即使使用常规方法,我们也可能希望添加仅对一个子类或一组子类有意义的可选参数。

可悲的是,解决这个问题的唯一办法就是从一开始就计划好。我们必须设计基类参数列表,以接受每个子类实现都不需要的任何参数的关键字参数。最后,我们必须确保该方法可以自由地接受意外参数,并将它们传递给它的super调用,以防以后的方法在继承顺序中需要这些参数。

Python 的函数参数语法提供了执行此操作所需的所有工具,但它使整个代码看起来很麻烦。查看Friend多重继承代码的正确版本:

class Contact:
    all_contacts = []

    def __init__(self, name='', email='', **kwargs):
        super().__init__(**kwargs)
        self.name = name
        self.email = email
        self.all_contacts.append(self)

class AddressHolder:
    def __init__(self, street='', city='', state='', code='',
            **kwargs):
        super().__init__(**kwargs)
        self.street = street
        self.city = city
        self.state = state
        self.code = code

class Friend(Contact, AddressHolder):
    def __init__(self, phone='', **kwargs):
        super().__init__(**kwargs)
        self.phone = phone

我们已将所有参数更改为关键字参数,并将其作为默认值提供一个空字符串。我们还确保包含一个**kwargs参数,以捕获我们的特定方法不知道如何处理的任何附加参数。它通过super调用将这些参数传递给下一个类。

提示

如果您不熟悉**kwargs语法,它基本上会收集传递到方法中的、未在参数列表中显式列出的任何关键字参数。这些参数存储在名为kwargs的字典中(我们可以随意调用变量,但惯例建议使用kwkwargs。当我们使用**kwargs语法调用不同的方法(例如,super().__init__)时,它将解压字典并将结果作为普通关键字参数传递给该方法。我们将在第 7 章Python 面向对象快捷方式中详细介绍这一点。

前面的示例执行它应该执行的操作。但是它开始看起来很混乱,并且很难回答这个问题,我们需要把什么论点传递到Friend.__init__中?对于任何计划使用该类的人来说,这是最重要的问题,因此应该向该方法添加一个 docstring 来解释发生了什么。

此外,如果我们想在父类中重用变量,那么即使是这个实现也是不够的。当我们将**kwargs变量传递给super时,字典中不包含任何作为显式关键字参数包含的变量。例如,在Friend.__init__中,对super的调用在kwargs字典中没有phone。如果其他任何类需要phone参数,我们需要确保它在传递的字典中。更糟糕的是,如果我们忘记了这一点,那么调试将很困难,因为超类不会抱怨,而只会将默认值(在本例中为空字符串)分配给变量。

有几种方法可以确保向上传递变量。假设Contact类由于某种原因需要使用phone参数进行初始化,Friend类也需要访问它。我们可以执行以下任一操作:

  • 不要将phone作为显式关键字参数包含。相反,把它留在kwargs字典里。Friend可以使用语法kwargs['phone']进行查找。当它将**kwargs传递到super调用时,phone仍将在字典中。
  • 使用标准字典语法kwargs['phone'] = phonephone作为显式关键字参数,但在将其传递给super之前更新kwargs字典。
  • 使phone成为显式关键字参数,但使用kwargs.update方法更新kwargs字典。如果有多个参数要更新,这将非常有用。您可以使用dict(phone=phone)构造函数或字典语法{'phone': phone}创建传递到update的字典。
  • 使phone成为显式关键字参数,但使用语法super().__init__(phone=phone, **kwargs)显式传递给超级调用。

我们已经介绍了 Python 中涉及多重继承的许多注意事项。当我们需要考虑所有可能的情况时,我们必须为它们做计划,我们的代码将变得混乱。基本的多重继承可能很方便,但在许多情况下,我们可能希望选择一种更透明的方式来组合两个不同的类,通常使用组合或我们将在第 10 章Python 设计模式 I第 11 章中介绍的设计模式之一 Python 设计模式 II

多态性

第一章面向对象设计中介绍了多态性。这是一个描述一个简单概念的花哨名称:不同的行为取决于所使用的子类,而不必明确知道子类实际上是什么。例如,想象一个播放音频文件的程序。媒体播放器可能需要加载AudioFile对象,然后加载play对象。我们在对象上放置了一个play()方法,负责解压缩或提取音频,并将其路由到声卡和扬声器。玩AudioFile的动作可能很简单:

audio_file.play()

但是,对于不同类型的文件,解压缩和提取音频文件的过程是非常不同的。.wav文件是未压缩存储的,.mp3.wma.ogg文件都有完全不同的压缩算法。

我们可以使用多态性继承来简化设计。每种类型的文件都可以由AudioFile的不同子类表示,例如WavFileMP3File。每一个文件都有一个play()方法,但该方法对每个文件的实现方式不同,以确保遵循正确的提取过程。媒体播放器对象永远不需要知道它所指的是AudioFile的哪个子类;它只调用play(),多态性地让对象处理播放的实际细节。让我们看一个快速的骨架,它显示了这可能是什么样子:

class AudioFile:
    def __init__(self, filename):
        if not filename.endswith(self.ext):
            raise Exception("Invalid file format")

        self.filename = filename

class MP3File(AudioFile):
    ext = "mp3"
    def play(self):
        print("playing {} as mp3".format(self.filename))

class WavFile(AudioFile):
    ext = "wav"
    def play(self):
        print("playing {} as wav".format(self.filename))

class OggFile(AudioFile):
    ext = "ogg"
    def play(self):
        print("playing {} as ogg".format(self.filename))

所有音频文件检查以确保初始化时提供了有效的扩展名。但是您注意到父类中的__init__方法如何能够从不同的子类访问ext类变量了吗?这就是工作中的多态性。如果文件名未以正确的名称结尾,则会引发异常(下一章将详细介绍异常)。事实上,AudioFile并没有实际存储对ext变量的引用,这并不能阻止它在子类上访问它。

此外,AudioFile的每个子类都以不同的方式实现play()(本例实际上并没有播放音乐;音频压缩算法真的需要一本单独的书!)。这也是多态性的作用。媒体播放器可以使用完全相同的代码来播放文件,无论它是什么类型;它不关心它正在查看的是AudioFile的哪个子类。解压缩音频文件的详细信息是封装的。如果我们测试此示例,它将如我们所希望的那样工作:

>>> ogg = OggFile("myfile.ogg")
>>> ogg.play()
playing myfile.ogg as ogg
>>> mp3 = MP3File("myfile.mp3")
>>> mp3.play()
playing myfile.mp3 as mp3
>>> not_an_mp3 = MP3File("myfile.ogg")
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "polymorphic_audio.py", line 4, in __init__
 raise Exception("Invalid file format")
Exception: Invalid file format

看看AudioFile.__init__如何在不知道它所指的子类的情况下检查文件类型?

多态性实际上是面向对象编程中最酷的东西之一,它使一些在早期范例中不可能实现的编程设计变得显而易见。然而,由于 duck 类型,Python 使多态性变得不那么酷。Python 中的 Duck 类型允许我们使用任何提供所需行为的对象,而不必强迫它成为子类。Python 的动态特性使这一点变得微不足道。下面的示例没有扩展AudioFile,但可以使用完全相同的接口在 Python 中与之交互:

class FlacFile:
    def __init__(self, filename):
        if not filename.endswith(".flac"):
            raise Exception("Invalid file format")

        self.filename = filename

    def play(self):
        print("playing {} as flac".format(self.filename))

我们的媒体播放器可以像扩展AudioFile一样轻松地播放此对象。

多态性是在许多面向对象上下文中使用继承的最重要原因之一。因为任何提供正确接口的对象都可以在 Python 中互换使用,所以它减少了对多态公共超类的需要。继承对于共享代码仍然很有用,但是,如果共享的只是公共接口,则只需要 duck 类型。这减少了对继承的需求,也减少了对多重继承的需求;通常,当多重继承看起来是一个有效的解决方案时,我们可以使用 duck 类型来模拟多个超类中的一个。

当然,仅仅因为对象满足特定的接口(通过提供所需的方法或属性),并不意味着它可以在所有情况下简单地工作。它必须以在整个系统中有意义的方式实现该接口。仅仅因为一个对象提供了一个play()方法,并不意味着它将自动与媒体播放器一起工作。例如,我们的国际象棋 AI 对象来自第 1 章面向对象设计,可能有一个移动棋子的play()方法。即使它满足接口,如果我们尝试将它插入媒体播放器,这个类也可能以惊人的方式崩溃!

duck 类型化的另一个有用特性是 duck 类型化对象只需要提供那些实际被访问的方法和属性。例如,如果我们需要创建一个伪文件对象来读取数据,我们可以创建一个具有read()方法的新对象;如果要与对象交互的代码只从文件中读取,我们不必重写write方法。更简洁地说,duck 类型不需要提供可用对象的整个接口,它只需要实现实际访问的接口。

抽象基类

虽然 duck 键入很有用,但事先判断一个类是否将满足您所需的协议并不总是那么容易。因此,Python 引入了抽象基类的思想。抽象基类ABCs定义了一组方法和属性,类必须实现这些方法和属性才能被视为该类的 duck 类型实例。该类可以扩展抽象基类本身,以便用作该类的实例,但它必须提供所有适当的方法。

在实践中,很少需要创建新的抽象基类,但我们可能会找到实现现有 ABC 实例的机会。我们将首先介绍如何实现 ABC,然后简要介绍如何在需要时创建自己的 ABC。

使用抽象基类

Python 标准库中存在的大多数抽象基类都位于collections模块中。其中最简单的是Container类。让我们在 Python 解释器中检查它,看看这个类需要什么方法:

>>> from collections import Container
>>> Container.__abstractmethods__
frozenset(['__contains__'])

因此,Container类正好有一个需要实现的抽象方法,__contains__。您可以发出help(Container.__contains__)来查看函数签名应该是什么样子:

Help on method __contains__ in module _abcoll:__contains__(self, x) unbound _abcoll.Container method

所以,我们看到__contains__需要接受一个参数。不幸的是,帮助文件没有告诉我们这个参数应该是什么,但是从 ABC 的名称和它实现的单个方法来看,很明显这个参数就是用户检查容器是否保存的值。

此方法由liststrdict实现,以指示给定值是否在该数据结构中。但是,我们也可以定义一个愚蠢的容器,告诉我们给定的值是否在奇数整数集中:

class OddContainer:
    def __contains__(self, x):
        if not isinstance(x, int) or not x % 2:
            return False
        return True

现在,我们可以实例化一个OddContainer对象并确定,即使我们没有扩展Container,类也是一个Container对象:

>>> from collections import Container
>>> odd_container = OddContainer()
>>> isinstance(odd_container, Container)
True
>>> issubclass(OddContainer, Container)
True

这就是为什么 duck 类型比经典多态性更可怕。我们可以创建关系,而不需要使用继承(或者更糟糕的是,多重继承)的开销。

ContainerABC 的有趣之处在于,任何实现它的类都可以免费使用in关键字。实际上,in只是委托给__contains__方法的语法糖。任何具有__contains__方法的类都是Container,因此可以通过in关键字进行查询,例如:

>>> 1 in odd_container
True
>>> 2 in odd_container
False
>>> 3 in odd_container
True
>>> "a string" in odd_container
False

创建抽象基类

正如我们前面看到的一样,不需要有一个抽象基类来启用 duck 类型。然而,想象一下我们正在创建一个带有第三方插件的媒体播放器。在这种情况下,建议创建一个抽象基类来记录第三方插件应该提供的 API。abc模块提供了执行此操作所需的工具,但我会提前警告您,这需要一些 Python 最神秘的概念:

import abc

class MediaLoader(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def play(self):
        pass

    @abc.abstractproperty
    def ext(self):
        pass

    @classmethod
    def __subclasshook__(cls, C):
        if cls is MediaLoader:
            attrs = set(dir(C))
            if set(cls.__abstractmethods__) <= attrs:
                return True

        return NotImplemented

这是一个非常复杂的例子,它包含了一些 Python 特性,在本书的后面部分才会解释这些特性。为了完整起见,这里包含了它,但您不需要了解所有内容,就可以获得如何创建自己的 ABC 的要点。

第一件奇怪的事情是metaclass关键字参数,它被传递到通常会看到父类列表的类中。这是一个很少使用的构造,来自元类编程的神秘艺术。在这本书中我们不会涉及元类,所以你需要知道的是,通过分配ABCMeta元类,你给了你的类超能力(或者至少是超类)能力。

接下来,我们将看到@abc.abstractmethod@abc.abstractproperty结构。这些是 Python 装饰程序。我们将在第 5 章中讨论何时使用面向对象编程。现在,只要知道,通过将一个方法或属性标记为抽象,您就说明了这个类的任何子类都必须实现该方法或提供该属性,才能被视为该类的适当成员。

看看如果实现提供或不提供这些属性的子类会发生什么:

>>> class Wav(MediaLoader):
...     pass
...
>>> x = Wav()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Wav with abstract methods ext, play
>>> class Ogg(MediaLoader):
...     ext = '.ogg'
...     def play(self):
...         pass
...
>>> o = Ogg()

由于Wav类无法实现抽象属性,因此无法实例化该类。该类仍然是一个合法的抽象类,但您必须对其进行子类化才能实际执行任何操作。Ogg类提供这两个属性,因此它可以干净地实例化。

回到MediaLoaderABC,让我们剖析一下__subclasshook__方法。基本上是说,任何提供此 ABC 的所有抽象属性的具体实现的类都应该被视为MediaLoader的子类,即使它实际上不是从MediaLoader类继承的。

更常见的面向对象语言在接口和类的实现之间有明确的分离。例如,一些语言提供了一个显式的interface关键字,允许我们定义一个类在没有任何实现的情况下必须具有的方法。在这样的环境中,抽象类既提供接口,也提供某些方法(但不是所有方法)的具体实现。任何类都可以显式地声明它实现了给定的接口。

Python 的 ABC 有助于提供接口的功能,而不会损害 duck 类型的好处。

揭开魔法的神秘面纱

如果您想创建满足此特定契约的抽象类,可以复制并粘贴子类代码,而不必理解它。我们将在整本书中介绍大多数不寻常的语法,但让我们逐行回顾一下以获得概述。

    @classmethod

此装饰器将该方法标记为类方法。它本质上说,可以对类而不是实例化对象调用该方法:

    def __subclasshook__(cls, C):

这定义了__subclasshook__类方法。Python 解释器调用这个特殊的方法来回答这个问题,C是这个类的子类吗?

        if cls is MediaLoader:

我们检查该方法是否是专门针对该类而不是该类的子类调用的。例如,这可以防止将Wav类视为Ogg类的父类:

            attrs = set(dir(C))

这一行所做的只是获取类所拥有的方法和属性集,包括其类层次结构中的任何父类:

            if set(cls.__abstractmethods__) <= attrs:

此行使用集合表示法查看该类中的抽象方法集是否已在候选类中提供。注意,它不会检查这些方法是否已经实现,只是检查它们是否存在。因此,一个类可能是一个子类,但它本身仍然是一个抽象类。

                return True

如果提供了所有的抽象方法,那么候选类就是这个类的一个子类,我们返回True。该方法可以合法地返回三个值中的一个:TrueFalseNotImplementedTrueFalse表示该类是否为该类的子类:

        return NotImplemented

如果未满足任何条件(即类不是MediaLoader或未提供所有抽象方法),则返回NotImplemented。这告诉 Python 机器使用默认机制(候选类是否显式扩展该类?)进行子类检测。

简言之,我们现在可以将Ogg类定义为MediaLoader类的子类,而无需实际扩展MediaLoader类:

>>> class Ogg():
...     ext = '.ogg'
...     def play(self):
...         print("this will play an ogg file")
...
>>> issubclass(Ogg, MediaLoader)
True
>>> isinstance(Ogg(), MediaLoader)
True

案例研究

让我们试着用一个更大的例子把我们所学的一切联系起来。我们将设计一个简单的房地产应用程序,允许代理管理可供购买或出租的房产。将有两种类型的财产:公寓和房屋。代理需要能够输入有关新房产的一些相关详细信息,列出所有当前可用的房产,并将房产标记为已出售或已出租。为简洁起见,我们不必担心编辑房产细节或在房产出售后重新激活房产。

该项目将允许代理使用 Python 解释器提示符与对象交互。在这个充满图形用户界面和 web 应用程序的世界里,你可能会奇怪为什么我们要创建如此老式的程序。简单地说,窗口程序和 web 应用程序都需要大量的开销知识和样板代码来完成所需的工作。如果我们使用这两种模式中的任何一种来开发软件,我们就会迷失在 GUI 编程或 web 编程中,以至于忽略了我们试图掌握的面向对象原则。

幸运的是,大多数 GUI 和 web 框架都使用面向对象的方法,我们现在学习的原则将有助于将来理解这些系统。我们将在第 13 章并发中简要讨论这两个问题,但完整的细节远远超出了一本书的范围。

从我们的需求来看,似乎有相当多的名词可以表示系统中的对象类。显然,我们需要表示一个属性。房屋和公寓可能需要分开分类。租赁和购买似乎也需要单独的代表。因为我们现在关注的是继承,所以我们将研究使用继承或多重继承来共享行为的方法。

HouseApartment都是属性类型,因此Property可以是这两个类的超类。RentalPurchase需要额外考虑;如果我们使用继承,我们需要有单独的类,例如,HouseRentalHousePurchase,并使用多重继承来组合它们。与基于合成或关联的设计相比,这感觉有点笨拙,但让我们来看看我们能想出什么。

那么,哪些属性可能与Property类相关联?不管是公寓还是房子,大多数人都想知道建筑面积、卧室数量和浴室数量。(有许多其他属性可能会被建模,但我们将对原型保持简单。)

如果物业是一栋房子,它会想宣传层数,是否有车库(附属、独立或无车库),以及院子是否有围栏。公寓需要标明是否有阳台,洗衣房是套间、投币式还是场外。

这两种属性类型都需要一个方法来显示该属性的特征。目前,没有明显的其他行为。

租赁物业将需要存储每月租金,物业是否配备家具,是否包括公用设施,如果不包括,估计是什么。购买物业需要存储购买价格和预计年度物业税。对于我们的应用程序,我们只需要显示这些数据,因此我们只需添加一个与其他类中使用的方法类似的display()方法即可。

最后,我们需要一个Agent对象来保存所有属性的列表,显示这些属性,并允许我们创建新的属性。创建属性需要提示用户提供每种属性类型的相关详细信息。这可以在Agent对象中完成,但是Agent需要知道很多关于属性类型的信息。这不是利用多态性。另一种选择是将提示放入每个类的初始值设定项甚至构造函数中,但这不允许将来在 GUI 或 web 应用程序中应用这些类。更好的方法是创建一个静态方法来执行提示并返回提示参数的字典。然后,Agent所要做的就是提示用户输入属性类型和支付方式,并要求正确的类自身实例化。

这是一个很大的设计!下面的类图可以更清楚地传达我们的设计决策:

Case study

哇,真是太多了!我不认为在没有交叉箭头的情况下添加另一个继承级别是可能的。多重继承是一件棘手的事情,即使在设计阶段也是如此。

这些类最棘手的方面是确保在继承层次结构中调用超类方法。让我们从Property实现开始:

class Property:
    def __init__(self, square_feet='', beds='',
            baths='', **kwargs):
        super().__init__(**kwargs)
        self.square_feet = square_feet
        self.num_bedrooms = beds
        self.num_baths = baths

    def display(self):
        print("PROPERTY DETAILS")
        print("================")
        print("square footage: {}".format(self.square_feet))
        print("bedrooms: {}".format(self.num_bedrooms))
        print("bathrooms: {}".format(self.num_baths))
        print()

    def prompt_init():
        return dict(square_feet=input("Enter the square feet: "),
                beds=input("Enter number of bedrooms: "),
                baths=input("Enter number of baths: "))
    prompt_init = staticmethod(prompt_init)

这个类非常简单。我们已经在__init__中添加了额外的**kwargs参数,因为我们知道它将用于多重继承情况。我们还包括了对super().__init__的调用,以防我们不是多重继承链中的最后一个调用。在本例中,我们正在使用关键字参数,因为我们知道在继承层次结构的其他级别不需要它们。

我们在prompt_init方法中看到了一些新的东西。此方法在初始创建后立即被转换为静态方法。静态方法只与类(类似于类变量)关联,而不是与特定的对象实例关联。因此,他们没有self论点。因此,super关键字将不起作用(没有父对象,只有父类),因此我们只需直接调用父类上的静态方法。此方法使用 Pythondict构造函数创建可传递到__init__的值字典。通过调用input提示每个键的值。

Apartment类扩展了Property,结构类似:

class Apartment(Property):
    valid_laundries = ("coin", "ensuite", "none")
    valid_balconies = ("yes", "no", "solarium")

    def __init__(self, balcony='', laundry='', **kwargs):
        super().__init__(**kwargs)
        self.balcony = balcony
        self.laundry = laundry

    def display(self):
        super().display()
        print("APARTMENT DETAILS")
        print("laundry: %s" % self.laundry)
        print("has balcony: %s" % self.balcony)

    def prompt_init():
        parent_init = Property.prompt_init()
        laundry = ''
        while laundry.lower() not in \
                Apartment.valid_laundries:
            laundry = input("What laundry facilities does "
                    "the property have? ({})".format(
                    ", ".join(Apartment.valid_laundries)))
        balcony = ''
        while balcony.lower() not in \
                Apartment.valid_balconies:
            balcony = input(
                "Does the property have a balcony? "
                "({})".format(
                ", ".join(Apartment.valid_balconies)))
        parent_init.update({
            "laundry": laundry,
            "balcony": balcony
        })
        return parent_init
    prompt_init = staticmethod(prompt_init)

display()__init__()方法使用super()调用各自的父类方法,以确保Property类已正确初始化。

prompt_init静态方法现在从父类获取字典值,然后添加自己的一些附加值。它调用dict.update方法将新字典值合并到第一个字典值中。然而,那个prompt_init方法看起来相当丑陋;它循环两次,直到用户使用结构相似但变量不同的代码输入有效输入。最好提取这个验证逻辑,这样我们就可以只在一个位置维护它;它可能对以后的课程也很有用。

在所有关于继承的讨论中,我们可能认为这是一个使用 mixin 的好地方。相反,我们有机会研究继承不是最佳解决方案的情况。我们要创建的方法将在静态方法中使用。如果我们要从提供验证功能的类继承,那么该功能还必须作为静态方法提供,该方法不访问该类上的任何实例变量。如果它不访问任何实例变量,那么把它变成一个类有什么意义呢?为什么我们不让这个验证功能成为一个模块级的函数,它接受一个输入字符串和一个有效答案的列表,并保持不变呢?

让我们探讨一下此验证函数的外观:

def get_valid_input(input_string, valid_options):
    input_string += " ({}) ".format(", ".join(valid_options))
    response = input(input_string)
    while response.lower() not in valid_options:
        response = input(input_string)
    return response

我们可以在解释器中测试这个函数,独立于我们正在处理的所有其他类。这是一个好迹象,这意味着我们设计的不同部分彼此之间没有紧密耦合,可以在不影响其他代码的情况下独立改进。

>>> get_valid_input("what laundry?", ("coin", "ensuite", "none"))
what laundry? (coin, ensuite, none) hi
what laundry? (coin, ensuite, none) COIN
'COIN'

现在,让我们快速更新我们的Apartment.prompt_init方法,使用这个新函数进行验证:

    def prompt_init():
        parent_init = Property.prompt_init()
        laundry = get_valid_input(
                "What laundry facilities does "
                "the property have? ",
                Apartment.valid_laundries)
        balcony = get_valid_input(
            "Does the property have a balcony? ",
            Apartment.valid_balconies)
        parent_init.update({
            "laundry": laundry,
            "balcony": balcony
        })
        return parent_init
    prompt_init = staticmethod(prompt_init)

这比我们的原始版本更容易阅读(和维护!)。现在我们已经准备好构建House类。此类具有与Apartment并行的结构,但引用了不同的提示和变量:

class House(Property):
    valid_garage = ("attached", "detached", "none")
    valid_fenced = ("yes", "no")

    def __init__(self, num_stories='',
            garage='', fenced='', **kwargs):
        super().__init__(**kwargs)
        self.garage = garage
        self.fenced = fenced
        self.num_stories = num_stories

    def display(self):
        super().display()
        print("HOUSE DETAILS")
        print("# of stories: {}".format(self.num_stories))
        print("garage: {}".format(self.garage))
        print("fenced yard: {}".format(self.fenced))

    def prompt_init():
        parent_init = Property.prompt_init()
        fenced = get_valid_input("Is the yard fenced? ",
                    House.valid_fenced)
        garage = get_valid_input("Is there a garage? ",
                House.valid_garage)
        num_stories = input("How many stories? ")

        parent_init.update({
            "fenced": fenced,
            "garage": garage,
            "num_stories": num_stories
        })
        return parent_init
    prompt_init = staticmethod(prompt_init)

这里没有什么新的东西需要探索,所以让我们继续学习PurchaseRental类。尽管它们的用途明显不同,但在设计上也与我们刚才讨论的类似:

class Purchase:
    def __init__(self, price='', taxes='', **kwargs):
        super().__init__(**kwargs)
        self.price = price
        self.taxes = taxes

    def display(self):
        super().display()
        print("PURCHASE DETAILS")
        print("selling price: {}".format(self.price))
        print("estimated taxes: {}".format(self.taxes))

    def prompt_init():
        return dict(
            price=input("What is the selling price? "),
            taxes=input("What are the estimated taxes? "))
    prompt_init = staticmethod(prompt_init)

class Rental:
    def __init__(self, furnished='', utilities='',
            rent='', **kwargs):
        super().__init__(**kwargs)
        self.furnished = furnished
        self.rent = rent
        self.utilities = utilities

    def display(self):
        super().display()
        print("RENTAL DETAILS")
        print("rent: {}".format(self.rent))
        print("estimated utilities: {}".format(
            self.utilities))
        print("furnished: {}".format(self.furnished))

    def prompt_init():
        return dict(
            rent=input("What is the monthly rent? "),
            utilities=input(
                "What are the estimated utilities? "),
            furnished = get_valid_input(
                "Is the property furnished? ",
                    ("yes", "no")))
    prompt_init = staticmethod(prompt_init)

这两个类没有超类(除了object之外),但我们仍然调用super().__init__,因为它们将与其他类合并,我们不知道super调用的顺序。该接口类似于用于HouseApartment的接口,当我们将这四个类的功能组合到单独的子类中时,该接口非常有用。例如:

class HouseRental(Rental, House):
    def prompt_init():
        init = House.prompt_init()
        init.update(Rental.prompt_init())
        return init
    prompt_init = staticmethod(prompt_init)

这有点令人惊讶,因为类本身既没有__init__方法,也没有display方法!因为在这些方法中,两个父类都适当地调用了super,所以我们只需扩展这些类,这些类就会按照正确的顺序运行。当然,prompt_init不是这样,因为它是一个静态方法,不调用super,所以我们显式地实现了这个方法。在编写其他三种组合之前,我们应该测试该类,以确保其行为正常:

>>> init = HouseRental.prompt_init()
Enter the square feet: 1
Enter number of bedrooms: 2
Enter number of baths: 3
Is the yard fenced?  (yes, no) no
Is there a garage?  (attached, detached, none) none
How many stories? 4
What is the monthly rent? 5
What are the estimated utilities? 6
Is the property furnished?  (yes, no) no
>>> house = HouseRental(**init)
>>> house.display()
PROPERTY DETAILS
================
square footage: 1
bedrooms: 2
bathrooms: 3

HOUSE DETAILS
# of stories: 4
garage: none
fenced yard: no

RENTAL DETAILS
rent: 5
estimated utilities: 6
furnished: no

看起来工作正常。prompt_init方法提示所有超类的初始值设定项,display()还协同调用所有三个超类。

上例中继承类的顺序很重要。如果我们写的是class HouseRental(House, Rental)而不是class HouseRental(Rental, House),那么display()就不会调用Rental.display()!在我们的HouseRental版本上调用display时,指的是该方法的Rental版本,调用super.display()获取House版本,再次调用super.display()获取属性版本。如果我们将其反转,display将引用House类的display()。调用 super 时,它调用Property父类上的方法。但是Property在其display方法中没有对super的调用。这意味着不会调用Rental类的display方法!通过按我们所做的顺序放置继承列表,我们确保Rental调用super,然后它负责层次结构的House端。您可能认为我们可以添加对Property.display()super调用,但这将失败,因为Property的下一个超类是object,而object没有display方法。另一种解决方法是允许RentalPurchase扩展Property类,而不是直接从object派生。(或者我们可以动态修改方法解析顺序,但这超出了本书的范围。)

现在我们已经测试了它,我们准备创建其余的组合子类:

class ApartmentRental(Rental, Apartment):
    def prompt_init():
        init = Apartment.prompt_init()
        init.update(Rental.prompt_init())
        return init
    prompt_init = staticmethod(prompt_init)

class ApartmentPurchase(Purchase, Apartment):
    def prompt_init():
        init = Apartment.prompt_init()
        init.update(Purchase.prompt_init())
        return init
    prompt_init = staticmethod(prompt_init)

class HousePurchase(Purchase, House):
    def prompt_init():
        init = House.prompt_init()
        init.update(Purchase.prompt_init())
        return init
    prompt_init = staticmethod(prompt_init)

这应该是我们路上最紧张的设计了!现在我们所要做的就是创建Agent类,该类负责创建新列表并显示现有列表。让我们从更简单的属性存储和列表开始:

class Agent:
    def __init__(self):
        self.property_list = []

    def display_properties(self):
        for property in self.property_list:
            property.display()

添加属性需要首先查询属性的类型以及属性是用于购买还是用于租赁。我们可以通过显示一个简单的菜单来实现这一点。一旦确定了这一点,我们就可以提取正确的子类,并使用我们已经开发的prompt_init层次结构提示所有细节。听起来很简单?它是。让我们首先向Agent类添加一个 dictionary 类变量:

    type_map = {
        ("house", "rental"): HouseRental,
        ("house", "purchase"): HousePurchase,
        ("apartment", "rental"): ApartmentRental,
        ("apartment", "purchase"): ApartmentPurchase
        }

这是一些看起来很有趣的代码。这是一个字典,其中键是两个不同字符串的元组,值是类对象。类对象?是的,类可以像普通对象或基本数据类型一样传递、重命名和存储在容器中。使用这个简单的字典,我们可以简单地劫持我们早期的get_valid_input方法,以确保获得正确的字典键并查找适当的类,如下所示:

    def add_property(self):
        property_type = get_valid_input(
                "What type of property? ",
                ("house", "apartment")).lower()
        payment_type = get_valid_input(
                "What payment type? ",
                ("purchase", "rental")).lower()

        PropertyClass = self.type_map[
            (property_type, payment_type)]
        init_args = PropertyClass.prompt_init()
        self.property_list.append(PropertyClass(**init_args))

这看起来可能也有点滑稽!我们在字典中查找该类并将其存储在名为PropertyClass的变量中。我们不知道哪个类是可用的,但是这个类知道它自己,所以我们可以通过多态调用prompt_init来获得一个适合传递给构造函数的值字典。然后,我们使用关键字参数语法将字典转换为参数,并构造新对象以加载正确的数据。

现在我们的用户可以使用这个Agent类来添加和查看属性列表。添加特性以将特性标记为可用或不可用,或者编辑和删除特性,都不需要做很多工作。我们的原型现在状态良好,可以在房地产agent上演示其功能。以下是演示会话的工作原理:

>>> agent = Agent()
>>> agent.add_property()
What type of property?  (house, apartment) house
What payment type?  (purchase, rental) rental
Enter the square feet: 900
Enter number of bedrooms: 2
Enter number of baths: one and a half
Is the yard fenced?  (yes, no) yes
Is there a garage?  (attached, detached, none) detached
How many stories? 1
What is the monthly rent? 1200
What are the estimated utilities? included
Is the property furnished?  (yes, no) no
>>> agent.add_property()
What type of property?  (house, apartment) apartment
What payment type?  (purchase, rental) purchase
Enter the square feet: 800
Enter number of bedrooms: 3
Enter number of baths: 2
What laundry facilities does the property have?  (coin, ensuite,
one) ensuite
Does the property have a balcony? (yes, no, solarium) yes
What is the selling price? $200,000
What are the estimated taxes? 1500
>>> agent.display_properties()
PROPERTY DETAILS
================
square footage: 900
bedrooms: 2
bathrooms: one and a half

HOUSE DETAILS
# of stories: 1
garage: detached
fenced yard: yes
RENTAL DETAILS
rent: 1200
estimated utilities: included
furnished: no
PROPERTY DETAILS
================
square footage: 800
bedrooms: 3
bathrooms: 2

APARTMENT DETAILS
laundry: ensuite
has balcony: yes
PURCHASE DETAILS
selling price: $200,000
estimated taxes: 1500

练习

环顾一下工作区中的一些物理对象,看看是否可以在继承层次结构中描述它们。几个世纪以来,人类一直在将世界划分成这样的分类法,所以这应该不难。对象类之间是否存在任何不明显的继承关系?如果要在计算机应用程序中对这些对象建模,它们将共享哪些属性和方法?哪些必须被多态性覆盖?它们之间有什么性质完全不同?

现在,编写一些代码。不,不适用于物理层次结构;那太无聊了。物理项的属性比方法多。想想一个宠物编程项目,你在过去的一年里一直想解决它,但却一直没有时间去做。无论你想解决什么问题,试着想想一些基本的继承关系。然后实施它们。确保您还注意到实际上不需要使用继承的关系类型。是否有任何地方需要使用多重继承?你确定吗?你能看到任何你想使用混音器的地方吗?试着拼凑出一个快速的原型。它不必是有用的,甚至不必部分工作。您已经了解了如何使用python -i测试代码;只需编写一些代码并在交互式解释器中进行测试。如果有效的话,再写一些。如果没有,请修复它!

现在,看看房地产的例子。事实证明,这是多重继承的一个非常有效的用途。我不得不承认,当我开始设计的时候,我有我的怀疑。看看最初的问题,看看是否可以想出另一种只使用单一继承的设计来解决它。如何使用抽象基类?那么一个完全不使用继承的设计呢?你认为哪一个是最优雅的解决方案?优雅是 Python 开发的主要目标,但是对于什么是最优雅的解决方案,每个程序员都有不同的看法。有些人倾向于使用组合来最清晰地思考和理解问题,而另一些人则发现多重继承是最有用的模型。

最后,尝试为这三种设计添加一些新功能。任何你喜欢的特征都可以。首先,我希望看到一种区分可用和不可用属性的方法。如果已经租了,对我来说没什么用!

哪种设计最容易扩展?哪一个最难?如果有人问你为什么这么想,你能解释一下你自己吗?

总结

我们从简单继承(面向对象程序员工具箱中最有用的工具之一)一直到多重继承(最复杂的工具之一)。继承可用于使用继承向现有类和内置项添加功能。将类似代码抽象到父类中有助于提高可维护性。可以使用super调用父类上的方法,并且必须安全地格式化参数列表,以便在使用多重继承时这些调用能够工作。

在下一章中,我们将介绍处理特殊情况的微妙艺术。*