Skip to content

Latest commit

 

History

History
1125 lines (843 loc) · 60.9 KB

File metadata and controls

1125 lines (843 loc) · 60.9 KB

四、相同的对象

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

有很多方法可以合并具有相似功能的代码段或对象。 在本章中,我们将介绍最著名的面向对象原理:继承。 正如第 1 章,“面向对象设计”中讨论的那样,继承使我们可以创建*,即两个或多个类之间的*关系,将通用逻辑抽象为超类并进行管理 子类中的特定详细信息。 特别是,我们将介绍以下方面的 Python 语法和原理:

  • 基本继承
  • 从内置继承
  • 多重继承
  • 多态性和鸭子打字

基本继承

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

如果我们不显式继承其他类,则我们的类将自动继承object。 但是,我们可以公开声明我们的类是使用以下语法从object派生的:

class MySubClass(object):
    pass

这就是继承! 从技术上讲,该示例与第 2 章和 Python 中的对象的第一个示例没有什么不同,因为如果我们未明确提供[ 不同的超类。 超类或父类是从其继承的类。 子类是从超类继承的类。 在这种情况下,超类是object,而MySubClass是子类。 子类也被称为是从其父类派生的,或者该子类扩展了父类。

正如您从示例中发现的一样,继承比基本类定义所需的语法更少。 只需在类名之后但在冒号终止类定义之前的括号内包括父类的名称。 这就是我们要告诉 Python 新类应该从给定的超类派生的全部操作。

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

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设置了变量,则实际上将创建仅与该对象关联的新的实例变量。 class 变量将保持不变,并可以通过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", "[email protected]")
>>> s = Supplier("Sup Plier", "[email protected]")
>>> 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)

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

>>> c1 = Contact("John A", "[email protected]")
>>> c2 = Contact("John B", "[email protected]")
>>> c3 = Contact("Jenna C", "[email protected]")
>>> [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 中不起作用。 像列表和字典的[]和{}语法一样,它是更复杂的构造的简写形式。 在讨论多重继承时,我们将在短期内了解更多信息,但是现在知道在 Python 2 中,您必须调用super(EmailContact, self).__init__()。 特别注意,第一个参数是子类的名称,而不是某些人希望的作为要调用的父类的名称。 另外,请记住类在对象之前。 我总是忘了顺序,因此 Python 3 中的新语法为我节省了很多时间来查找它。

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

多重继承

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

注意

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

最简单且最有用的多重继承形式称为 mixin。 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模块。

该类没有做任何特殊的事情(实际上,它几乎不能作为一个独立的类起作用),但是它确实允许我们使用多重继承定义一个描述ContactMailSender的新类:

class EmailableContact(Contact, MailSender):
    pass

多重继承的语法看起来像类定义中的参数列表。 我们没有在括号中包含一个基类,而是包含了两个(或多个),并以逗号分隔。 我们可以测试这个新的混合动力以查看混合工作:

>>> e = EmailableContact("John Smith", "[email protected]")
>>> 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 面向对象的快捷方式中简要介绍猴子补丁),在Contact类之后添加send_mail方法 类已创建。 这是通过定义一个接受self参数的函数并将其设置为现有类的属性来完成的。

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

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

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

但是,继承也是可行的解决方案,这就是我们要探讨的内容。 让我们添加一个包含地址的新类。 我们将这个新类称为“ AddressHolder”,而不是“ Address”,因为继承定义了关系。 说“朋友”是“地址”是不正确的,但是由于朋友可以有“地址”,因此我们可以说“朋友”是“ AddressHolder”。 以后,我们可以创建其他拥有地址的实体(公司,建筑物)。 这是我们的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超类(请记住,所有类均源自object)。 Friend然后在AddressHolder上调用__init__,这又隐式地初始化object超类。 这意味着父类已设置两次。 对于object类,这是相对无害的,但是在某些情况下,它可能会带来灾难。 想象一下,对于每个请求尝试两次连接到数据库!

基类只能调用一次。 曾经,是的,但是什么时候? 我们先叫Friend,然后叫Contact,然后叫Object,然后叫AddressHolder? 还是Friend,然后是Contact,然后是AddressHolder,然后是Object

注意

通过修改类的 __mro__方法解析顺序)属性,可以即时调整方法的调用顺序。 这超出了本模块的范围。 如果您认为需要了解它,我建议 Expert Python 编程TarekZiadéPackt Publishing 或阅读有关该主题的原始文档,网址为 [] http://www.python.org/download/releases/2.3/mro/](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调用之后执行的,因此打印输出按实际执行每种方法的顺序排列。 让我们从后到前查看输出,看看谁在打电话。

首先,Subclasscall_me调用super().call_me(),碰巧引用了LeftSubclass.call_me()。 然后LeftSubclass.call_me()方法调用super().call_me(),但是在这种情况下,super()指的是RightSubclass.call_me()

请特别注意:super调用是而不是调用LeftSubclass的超类(即BaseClass)上的方法。 而是,它正在调用RightSubclass,即使它不是LeftSubclass的直接父代! 这是下一个方法,而不是父方法。 然后RightSubclass调用BaseClass,并且super调用已确保类层次结构中的每个方法执行一次。

不同的参数集

回到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__? 对于任何打算使用该类的人来说,这是最重要的问题,因此应在方法中添加一个文档字符串以说明正在发生的情况。

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

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

  • 不要将phone用作显式关键字参数。 而是将其保留在kwargs词典中。 Friend可以使用语法kwargs['phone']进行查找。 当它将**kwargs传递给super呼叫时,phone仍将在词典中。
  • phone设为显式关键字参数,但使用标准字典语法kwargs['phone'] = phonekwargs字典传递给super之前,先对其进行更新。
  • 使phone为显式关键字参数,但使用kwargs.update方法更新kwargs词典。 如果您有几个要更新的参数,这将很有用。 您可以使用dict(phone=phone)构造函数或字典语法{'phone': phone}创建传递到update的字典。
  • 使phone为显式关键字参数,但使用语法super().__init__(phone=phone, **kwargs)将其显式传递给超级调用。

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

多态性

在第 1 章,“面向对象设计”中向我们介绍了多态性。 这是一个很简单的名字,描述了一个简单的概念:根据所使用的子类而发生不同的行为,而不必明确知道该子类实际上是什么。 例如,假设有一个播放音频文件的程序。 媒体播放器可能需要先加载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的哪个子类。 解压缩音频文件的详细信息封装在[H​​TG4]中。 如果我们测试此示例,它将按我们希望的那样工作:

>>> 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__如何能够在不真正知道其引用的子类的情况下检查文件类型?

实际上,多态是有关面向对象编程的最酷的事情之一,它使某些编程设计变得显而易见,而这在早期的范例中是不可能的。 但是,由于使用鸭子输入,Python 使多态性变得不那么酷。 在 Python 中进行鸭子输入可以让我们使用任何提供所需行为的对象,而不必强制其成为子类。 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 中互换使用,因此减少了对多态通用超类的需求。 继承对于共享代码仍然有用,但是,如果要共享的只是公共接口,那么只需要鸭子输入即可。 对继承的需求的减少也减少了对多重继承的需求。 通常,当多重继承看起来是一个有效的解决方案时,我们可以使用鸭子类型来模仿多个超类之一。

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

鸭子类型的另一个有用的功能是,鸭子类型的对象仅需要提供那些实际上已被访问的方法和属性。 例如,如果我们需要创建一个假文件对象来读取数据,则可以创建一个具有read()方法的新对象; 如果将要与该对象交互的代码仅从文件中读取,则不必重写write方法。 更简洁地说,鸭子类型不需要提供可用对象的整个接口,它只需要满足实际访问的接口即可。

抽象基类

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

在实践中,几乎没有必要创建新的抽象基类,但我们可能会发现实现现有 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

这就是为什么鸭子类型比经典多态性更出色的原因。 我们可以创建关系,而没有使用继承(或更糟糕的是,多重继承)的开销。

关于Container ABC 的有趣之处在于,实现它的任何类都可以免费使用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

创建抽象基类

正如我们先前看到的一样,不必具有抽象基类来启用鸭子类型。 但是,想象一下我们正在创建一个带有第三方插件的媒体播放器。 在这种情况下,建议创建一个抽象基类以记录第三方插件应提供的 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类提供这两个属性,因此它可以干净地实例化。

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

更常见的面向对象语言在接口和类的实现之间有着清晰的分隔。 例如,某些语言提供了显式的interface关键字,该关键字使我们可以定义类必须具有的方法而无需任何实现。 在这样的环境中,抽象类是一种既提供接口又提供某些但不是全部方法的具体实现的类。 任何类都可以明确声明其实现了给定的接口。

Python 的 ABC 有助于提供接口功能,而不会影响鸭子输入的好处。

揭开魔术的神秘面纱

如果要创建满足此特定约定的抽象类,则可以在不了解子类代码的情况下复制并粘贴。 我们将在整个模块中介绍大多数不寻常的语法,但让我们逐行对其进行概述。

    @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关键字将不起作用(没有父对象,只有父类),因此我们直接在父类上直接调用 static 方法。 此方法使用 Python dict构造函数创建一个值字典,该值可以传递到__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类添加一个字典类变量:

    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

Case study

Case study

Case study

Case study