企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] 在编程世界中,重复代码被认为是邪恶的。我们不应该让相同或相似代码的多个副本存在不同的地方。 </b> 有许多方法可以用来合并具有类似功能的代码或对象。在本章中,我们将讲述最著名的面向对象原则:继承。如第1章“面向对象设计”所述,继承允许我们创建两个或多个类之间的*是一个*关系,将公共逻辑抽象转换成超类,在子类中管理特定细节。我们将介绍以下方面的Python语法和原则: * 基本继承 * 从内置类继承 * 多重继承 * 多态性和鸭子类型 ## 基本继承 技术上,我们创建的每个类都使用了继承。所有的Python类都是名为`object`的特殊类的子类。这个类提供非常少的数据和行为(它提供的行为都是双下划线方法,仅供内部使用。译注:使用`dir(object)`查看,感觉方法还是很多的!),但它确实允许Python以相同的方式处理所有对象。 </b> 如果我们没有从不同的类显式继承,我们的类将自动从`object`继承。然而,我们可以使用以下语法公开声明我们的类来自`object`: ``` class MySubClass(object): pass ``` 这就是继承!从技术上讲,这个例子和我们第2章“Python中的对象”中的第一个示例没有什么不同,因为,如果我们不显式提供一个不同的超类,Python 3将自动继承自`object`。超类或父类是被继承的类。子类是继承自超类的类。在这种例子中,超类是`object`,而`MySubClass`是子类。子类也可以说是从它的父类派生出来的,或者说子类扩展了父类。 </b> 正如你可能已经从示例中想到的,继承在基本类定义之上需要一点最少的额外语法量。只需在类名后冒号(用于终止类定义)前的括号内包含父类的名字即可。我们唯一要做的事,就是告诉Python新类是从给定的超类派生出来的。 </b> 我们如何在实践中应用继承?最简单和最明显的继承是向现有类添加功能。让我们从一个简单的跟踪几个人的姓名和电子邮件地址的联系人管理器开始。这个`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`访问。 (译注:貌似和上面说的不太一样,如下图所示,使用`self.all_contacts`后,类变量`Contact.all_contacts`没有保持不变) ![](https://box.kancloud.cn/12674da66354042b2a46ffe414ca6772_400x365.png) ![](https://box.kancloud.cn/a7a594a2c25a255b0d3c16e79f0f2faf_711x88.png) 这是一个简单的类,它允许我们跟踪每个联系人的数据。但是如果我们的一些联系人也是我们需要从他们那里采购货物的供应商呢?我们可以向`Contact`类添加一个`order`方法,但这将允许人们意外地从顾客或家庭朋友们等联系人那里订购东西。相反,让我们创建一个新的供应商类,其行为类似于我们的联系人类,但有一个额外的订购`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__`中都接受一个姓名和电子邮件地址,但仅限于供应商有一个可应用的订购`order`方法: ``` >>> 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`类可以做任何联系人`Contact`类可以做的事情(包括把它自己添加到所有联系人`all_contacts`列表)和它作为供应商需要处理的独特的事情。这就是继承的美。 ### 扩展内置类 继承的一个有趣的用途是给内置类添加功能。在前面看到的联系人`Contact`类中,我们将联系人添加到所有联系人的列表中。如果我们想按名字搜索这个列表呢?嗯,我们可以在`Contact`类上加上一个搜索方法,但实际上这个方法更像是属于列表(一种内置数据结构)本身。我们可以通过继承来做到这一点: ``` class ContactList(list): def search(self, 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`,并用`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'] ``` (译注:最后一行可以简单写成`[c.name for c in Contact.all_contacts if "John" in c.name]`,这样就不用写`search`方法了) 你想知道我们是如何做到把内置语法`[]`变成我们可继承的形式吗?用`[]`创建空列表实际上是使用`list()`创建空列表的简写;这两种语法实际是相同的: ``` >>> [] == list() True ``` 事实上,`[]`语法是所谓的**语法糖**,它被称为钩子(hood),调用`list()`的构造器。列表`list`数据类型是一个我们可以扩展的类。事实上,列表`list`本身扩展了对象`object`类: ``` >>> isinstance([], object) True ``` 作为第二个例子,我们可以扩展字典`dict`类,它类似于列表,当使用`{}`语法缩写时,它将被构造(注意取`key`和条件语句): ``` 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' ``` 大多数内置类型都可以类似地进行扩展。常见的可被扩展的内置类型有对象`object`、列表`list`、集合`set`、字典`dict`、文件`file`和字符串`str`。数值类型,如`int`和`float`偶尔也会被扩展。 ### 重写和Super 因此,继承对于向现有的类*添加*新的行为是很好的,但是如果是*改变*行为呢?我们的联系人类只允许姓名和电子邮件地址。这对于大多数联系人来说可能已经足够了,但是如果我们想给亲密朋友添加一部电话号码呢? </b> 正如我们在第2章“Python中的对象”中看到的,在我们构造完一个联系人对象后,我们可以通过设置添加联系人的电话属性。但是如果我们想在初始化时添加这个第三个变量(电话属性),我们必须重写`__init__`。重写意味着在子类中修改或替换和超类同名的方法。这样做并不需要特殊语法;子类将自动调用新创建的方法,而不是超类的方法。例如: ``` class Friend(Contact): def __init__(self, name, email, phone): self.name = name self.email = email self.phone = phone ``` 任何方法都可以被重写,而不仅仅是`__init__`。然而,在我们继续之前,我们需要解决这个例子中的一些问题。我们的联系人`Contact`类和朋友`Friend`类有用来设置姓名和电子邮件属性的重复代码;这使得代码维护变得很复杂,因为我们必须在两个或多个地方更新代码。更令人担忧的是,我们的朋友`Friend`类忽略了将自己添加我们在联系人`Contact`类中创建的`all_contacts`列表。 </b> 我们真正需要的是一种可以执行`Contact`类原始`__init__`的方法。这就是`super`函数的作用;它返回的对象是父类的实例,允许我们直接调用父类方法: ``` class Friend(Contact): def __init__(self, name, email, phone): super().__init__(name, email) self.phone = phone ``` 这个示例首先使用`super`创建一个父类对象的实例,然后调用这个对象的`__init__`方法,传入预期的参数。然后再执行自己的初始化,即设置电话属性。 > 请注意,`super()`语法在python的旧版本中不起作用。像`[]`和`{}`对于列表和词典的意义一样,它是一个更复杂结构的简写。我们将在我们讨论多重继承时,进一步了解它,但现在只要知道,在Python 2中,你必须调用`super(EmailContact,self).__init__()`(译注:我怀疑`EmailContact`应该写成`Friend`)。请特别注意,第一个参数是子类的名称,而不是像很多人希望的是父类的名称。此外,请记住类参数在对象参数之前。我总是忘记顺序,所以Python 3中的新语法省去了我很多检查的时间。 所以,可以对任何方法使用`super()`,而不仅仅是`__init__`。这意味着一切 方法都可以通过重写和调用`super`来修改。可以在方法的任何位置调用`super`;我们不必把`super`放在方法中的第一行。例如,我们可能需要操纵或验证传入参数,然后再将它们转发给超类。 ## 多重继承 多重继承是一个敏感的话题。原则上,它很简单:一个子类可以从多个父类继承,并能访问父类的功能。实际上,这没有听起来那么有用,许多专家程序员建议不要使用它。 > 根据经验,如果你认为你需要多重继承,你就是可能是错的,但是如果你知道你需要它,你可能是对的。(译注:唔知讲咩!) 多重继承最简单和最有用的形式叫做`mixin`。`mixin`通常是一个超类,它并不意味着为自己而存在,而是意味着被其他类继承以提供额外的功能。比如说我们希望在联系人`Contact`类中添加允许发送电子邮件到联系人邮箱`self.email`的功能。发送电子邮件是一项常见的任务,我们可能想在许多类上使用这个功能。因此,我们可以编写一个简单的`mixin`类来为我们实现发送电子邮件的功能: ``` class MailSender: def send_mail(self, message): print("Sending mail to " + self.email) # 在这里加入 e-mail 逻辑 ``` 为了简洁起见,我们这里不包括实际的电子邮件逻辑;如果你感兴趣它是如何完成的,请参见Python标准库中的`smtplib`模块。 </b> 这个类不做任何特殊的事情(事实上,它勉强起到一个独立类的作用),但是它确实允许我们使用多重继承定义一个同时描述联系人`Contact`和邮件发送者`MailSender`的新类: ``` class EmailableContact(Contact, MailSender): pass ``` 多重继承的语法看起来像类定义中的参数列表。括号中不仅仅包含一个基类,而是包含两个(或更多)用逗号隔开的类。我们可以测试这个混合类,看看`mixin`的效果: ``` >>> 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`,🆗,一切正常。 </b> 这并不难,你可能想知道关于多重继承可怕的警告是什么?我们一会儿再讨论这个复杂的问题,但是先让我们考虑一些不使用`mixin`的其他选择: * 我们可以使用单一继承并在子类中添加`send_mail`函数。缺点是,对于需要电子邮件的任何其他类,都得复制这个函数。 * 我们可以创建一个独立的Python函数来发送电子邮件,当需要发送电子邮件时,传递正确的电子邮件地址作为函数参数,然后调用该函数。 * 我们可以探索一些使用组合而不是继承的方法。例如,电子邮件联系人`EmailableContact`可以有一个邮件发送者`MailSender`对象,而不是继承它。 * 我们可以进行猴子补丁`monkey-patch`(我们将在第7章“Python面向对象的快捷方式”中简要介绍猴子补丁)。当联系人`Contact`类被创建后,我们用`monkey-patch`给这个类添加`send_mail`方法。我们可以定义一个接受`self`参数的函数,并将其设置为现有类的属性。 当混合的方法来自不同类时,多重继承工作正常,但是当我们必须在超类上调用方法时,事情将变得非常混乱。有多个超类,我们怎样知道应该调用哪一个方法?我们该如何知道调用它们的顺序是什么? </b> 让我们在朋友`Friend`类上添加一个家庭住址来探索这些问题。我们有一些可用的方法。地址是一些字符串的集合,分别代表街道、城市、国家和联系人的其他详细信息。我们可以将这些字符串中的每一个作为参数传递到`Friend`类的`__init__`方法。我们也可以将这些字符串存储在元组或字典中,并作为单个参数输入到`__init__`中。如果没有给地址添加方法的需要,这可能是最好的方案。 </b> 另一种选择是创建一个新的地址`Address`类来保存这些字符串,然后将该类的一个实例传递到`Friend`类的`__init__`方法中。这个解决方案的优点是我们可以给数据添加行为(比如,根据地址指路或打印地图的方法),而不仅仅是静态存储数据。这是一个组合的例子,正如我们在第1章“面向对象设计”中讨论的那样。“有一个”的组合关系是这个问题的一个完全可行的解决方案。它允许我们在其他实体中重用地址`Address`类,例如建筑物、商业,或者组织。 </b> 然而,继承也是一个可行的解决方案,这也是我们想要探索的。让我们添加一个包含地址的新类。我们将这个新类称为“地址持有者”`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`参数。这个例子在技术上是可行的;我们可以直接在类中访问不同的变量。但是有几个问题。 </b> 首先,如果我们忽略显式调用超类的初始化函数,则有可能未初始化某个超类。在这个例子里这不是大问题,但它可能导致在常见情况下出现难以调试的问题,并导致程序崩溃。想象一下试图将数据插入到还没有连接的数据库中。 </b> 第二,也是更险恶的,由于类层级的缘故,超类可能会被多次调用。请看这个继承图: ![](https://box.kancloud.cn/317e3777d474a0c934f7510cfe199c00_357x332.png) 朋友`Friend`类中的`__init__`方法首先调用联系人`Contact`的`__init__`,它隐式初始化`object`超类(记住,所有类都从`object`继承而来)。然后`Friend`调用`AddressHolder`上的`__init__`,再一次进行隐式初始化`object`超类。这意味着`object`被创建了两次。对于`object`类,这不是什么大问题,但是在某些情况下,它可能产生会灾难。想象一下,每个请求都要连接数据库两次! </b> 基类应该只能调用一次。一次,是的,那么什么时候调用呢?我们是先调用`Friend`,然后`Contact`,然后`object`,然后`AddressHolder`?还是先调用`Friend`,然后`Contact`,然后`AddressHolder`,然后是`object`? > 调用方法的顺序可以通过修改上类的`__mro__`(方法解析顺序)属性来实现。这超出了本书的范围。如果你需要理解它,我推荐你阅读Expert Python Programming, Tarek Ziadé, Packt Publishing,,或阅读关于该主题的原始文档,网址为[http://www.python.org/download/releases/2.3/mro/](http://www.python.org/download/releases/2.3/mro/). 让我们看第二个人为的例子,它更清楚地说明了这个问题。这里我们有一个基类,它有一个名为`call_me`的方法。两个子类重写该方法,然后另一个子类多重继承了这两个子类。这被称为钻石继承,因为类图像钻石的形状: ![](https://box.kancloud.cn/6567dafafb3cd63470e49fd2fd384482_358x331.png) 让我们把这张图转换成代码;此示例显示了调用方法的时间: ``` 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`类对象,并调用它的`call_me`方法一次,我们将得到这样的输出: ``` >>> 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`方法被调用了两次。如果该方法真的用在实际工作中,可能会导致一些潜在的错误——比如存入银行账户——两次。 </b> 需要记住的一点是,在多重继承中,我们只想调用类层次结构中的“下一个”方法,而不是“父类”方法。事实上,下一个方法可能不在当前类的父类或祖先类上。`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()`调用之后执行的,所以打印的输出的顺序是每个方法实际执行的顺序。让我们从后往前看看谁在调用什么。 </b> 首先,`Subclass`的`call_me`调用`super().call_me()`,指的是`LeftSubclass.call_me()`。`LeftSubclass.call_me()`方法调用 `super().call_me()`,但在这种情况下,`super()`指的是`RightSubclass.call_ me()`。 </b> 请特别注意:`super`调用的不是左子类`LeftSubclass`的超类(`BaseClass`)。相反,它调用的是右子类`RightSubclass`,即使它不是左子类的直接父类!这是`下一个`方法,不是父类方法。右子类然后调用基类,`super`调用能确保类层次结构中的每个方法只被执行一次。 ### 不同的参数集 当我们回到我们的`Friend`多重继承示例时,会发现事情变得越来复杂。在`Friend`的`__init__`方法中,我们最初不得不*使用不同的参数集*,调用两个父类的`__init__`: ``` Contact.__init__(self, name, email) AddressHolder.__init__(self, street, city, state, code) ``` 当我们使用`super`时,我们是如何管理不同的参数集呢?我们不必知道`super`将首先尝试初始化哪个类。即使我们这样做了,我们仍然需要一种传递“额外”参数的方法,以便于后续在其他子类上对`super`的调用,能够接收正确的参数。 </b> 特别是,如果对`super`的第一次调用,传递了`name`和`email`参数给`Contact.__init__`。`Contact.__init__`继续调用`super`,它需要将与地址相关的参数传递给“下一个”方法,也就是`AddressHolder.__init__`。 </b> 每当我们调用有相同名字、但有不同的参数集的超类方法时,这会产生一个问题。大多数时候,你唯一需要调用一个完整的、具有不同的参数集的超类方法,是在`__init__`进行的,正如我们在这里所做的。然而,对于常规方法,我们可能希望添加仅对一个子类或一组子类有意义的可选参数。 </b> 可悲的是,解决这个问题的唯一方法是从开始就做好计划。我们必须设计我们的基类参数列表来接受任何并非每个子类实现都需要的参数。最后,我们必须确保方法能够自由地接受并不期望的参数,并将它们传递给`super`调用,这对继承顺序中的后续方法是必要的。 </b> 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`调用将这些参数传递给下一个类。(译注:如果我把`Contact`类的`super`给注释掉,`Friend`类的实例对象就不会有地址信息了) > 如果您不熟悉`**kwargs`语法,就当它是在参数列表中不能显示列出的任何关键字参数。这些参数存储在名为`kwargs`的字典里(我们可以随意给这个字典命名,但一般建议使用`kw`或`kwargs`)。当我们调用一个不同的带有`**kwargs`语法的方法(例如,`super().__init__`),它会打开字典并将字典中的内容作为普通的关键字参数传递给方法。我们将在第7章“Python面向对象快捷方式”中详细讨论这一点。 前面的例子做了它应该做的事情。但它开始看起来乱七八糟了,很难回答这样一个问题:我们需要哪些参数传递给`Friend.__init__`?这对任何计划使用这个类的人来说都是至关重要的,所以应该向方法中添加一个`docstring`来解释发生了什么。 </b> 此外,如果我们想*重用*父类中的变量,我们所做的事情是不够的。当我们将`**kwargs`变量传递给`super`时,字典不会包括作为显式关键字参数的任何变量。例如,在`Friend.__init__`,对`super`的调用中,并没有将`phone`包含在`kwargs`字典中。如果任何其他类需要`phone`参数,我们需要确保它在传递的字典中。更糟糕的是,如果我们忘记这样做,它会很难调试,因为超类不会抱怨,只是简单地分配一个变量的默认值(在本例中为空字符串)。 </b> 有几种方法可以确保变量向上传递。假设由于某种原因,`Contact`类确实需要`phone`参数初始化,`Friend`类也需要访问`phone`参数。我们可以执行以下任一操作: * 不要将`phone`作为显式关键字参数。相反,把它放在`kwargs`字典里。`Friend`可以使用`kwargs['phone']`语法查找它。当它将`**kwargs`传递给`super`调用时,`phone`参数仍然会在字典里。 * 将`phone`参数作为显式关键字参数,但在传递给`super`之前,使用标准字典语法`kwargs['phone'] = phone`更新`kwargs`字典。 * 将`phone`参数作为显式关键字参数,但使用`kwargs.update`方法更新字典。如果有几个参数要更新,这会很有用。你可以使用`dict(phone=phone)`构造函数或字典语法`{"phone":phone }`来创建一个,并对传入的词典进行更新`update`。 * 将`phone`参数作为显示关键词参数,但使用语法`super().__init__(phone=phone, **kwargs)`,将其传递给`super`调用。 我们已经讨论了Python中涉及多重继承的许多警告。当我们需要考虑所有可能的情况时,我们必须为它们做好计划,我们的代码会变得混乱。基本的多重继承可能很方便,但是,在许多情况下在这种情况下,我们可能希望选择一种更透明的方式来合并两个完全不同的类,通常使用组合或者我们将要在第10章“Python设计模式I”和第11章“Python设计模式II”中讨论的某种设计模式。 ## 多态性 我们在第1章“面向对象设计”中被介绍了多态性。这是描述一个简单概念的奇特名字:不同行为的发生取决于正在使用哪个子类,而不必明确知道这个子类是什么。例如,想象一个播放音频文件的程序。媒体播放器可能需要加载音频文件`AudioFile`对象,然后播放`play`它。我们会这个对象上调用`play()`方法。这个`play()`方法负责解压缩或提取对象上的音频,并将它发送到声卡和扬声器。播放音频文件`AudioFile`的行为可以很简单: ``` audio_file.play() ``` 然而,对于不同类型的音频文件,解压缩和提取音频文件的过程非常不同的。`.wav`文件存储未压缩的音频,而`. mp3`、`.wma`,还有`.ogg`文件都有着完全不同的压缩算法。 </b> 我们可以使用多态性继承来简化设计。每种类型文件可以由音频文件的不同子类来表示,例如,`WavFile`,`MP3`文件。其中每一个都有一个`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`变量的引用,并不能阻止它在子类中访问`ext`变量。 </b> 此外,音频文件的每个子类以不同的方式实现`play()`(这个例子实际上并不播放音乐;音频压缩算法真的值得写一本单独的书!)。这也是多态性在起作用。媒体播放器可以使用完全相同的代码来播放文件,不管它是什么类型;查看它正在播放的音频文件是哪个子类,并不重要。解压缩音频文件的细节被*封装*了。如果我们测试这个例子,它会像我们希望的那样工作: ``` >>> 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__`能够检查文件类型,而无需实际知道它指的是哪个子类吗?(译注:子类类变量对父类是可见的) </b> 多态性实际上是面向对象编程最酷的事情之一,这使得一些早期范例不可能的编程设计变得显而易见。然而,由于鸭子类型,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`一样轻松地播放这个对象。 </b> 使用多态性的最重要原因之一是在面向对象的上下文之间使用继承。因为任何提供正确接口的对象都可以在Python中互换使用,它减少了对多态公共超类的需求。继承对于共享代码仍然是有用的,但是,如果共享的都是公共接口,那么只需要鸭子类型。这种对继承需求的减少也减少了多重继承的需求;通常,当多重继承似乎是一个有效的解决方案,我们就可以用鸭子类型来模仿多个超类中的一个。 </b> 当然,仅仅因为一个对象满足特定的接口(通过提供所需的方法或属性)并不意味着它在所有情况下都能简单地工作。必须以在整个系统中有意义的方式实现接口。仅仅因为一个对象提供`play()`方法并不意味着它将自动与媒体播放器一同工作。例如,我们在第1章“面向对象设计”中,也有一个象棋AI对象,也有一个`play()`方法用来移动棋子。即使它满足了接口,如果我们尝试将它插入媒体播放器,这个类仍然会以某种特别的方式崩溃! </b> 鸭子类型的另一个有用特性是鸭子类型的对象只需要提供那些实际被访问的方法和属性。例如,如果我们需要创建一个假的文件对象来读取里面的数据,我们可以创建一个新的具有`read()`方法的对象;如果与对象交互的代码只是从文件中读取,那我们不必重写`write`方法。简而言之,鸭子类型不需要提供一个可用对象的完整的接口,它只需要实现实际被访问的接口的就行。 ## 抽象基类 虽然鸭子类型很有用,但要提前判定一个类是否满足你的要求,是不容易的。因此,Python引入了抽象基类。抽象基类或ABCs定义了一个类必须拥有的一组方法和属性,只有这样,它才能被认为是鸭子类型的类实例。抽象基类可以扩展抽象基类本身,以便用作实例,但它必须提供所有适当的方法。 </b> 实际上,很少需要创建新的抽象基类,但是我们可能会发现实现现有ABCs实例的场合。我们先介绍ABCs的实现,然后在简要地看看如何创建自己的抽象基类(如果需要的话)。 ### 使用抽象基类 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__`需要一个单独的参数。不幸的是帮助文件并没有告诉我们这个参数应该是什么的信息,但是从抽象类的名称来看,这是很明显的,这个参数是用来检查容器是否包含的那个值。 </b> 这个方法可以用在列表`list`、字符串`str`和字典`dict`,用来检查给定值是否在数据结构中。然而,我们也可以定义一个愚蠢的容器,告诉我们一个给定值是否在奇数集合中: ``` class OddContainer: def __contains__(self, x): if not isinstance(x, int) or not x % 2: return False return True ``` 现在,我们可以实例化一个`OddContainer`对象,即使我们没有通过扩展`Container`类得到`OddContainer`类,`OddContainer`类仍然是容器`Container`对象: ``` >>> from collections import Container >>> odd_container = OddContainer() >>> isinstance(odd_container, Container) True >>> issubclass(OddContainer, Container) True ``` 这就是为什么鸭子类型比经典多态性更令人敬畏的原因。我们可以创建一个没有使用继承(或者更糟糕的是,多重继承)开销的关系。(译注:但是如果给一个新类赋予`list`的方法,鸭子类型似乎并不成立,例如下图) ![](https://box.kancloud.cn/0d3bc60c5313c91cd295b8d792302a60_393x122.png) 容器`Container`抽象类的有趣之处在于,任何实现`Container`抽象类的类都可以自由地使用`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 ``` 这是一个复杂的例子,它包含了几个在本书的后面才会解释到的特性。这里只是为了例子的完整,但你没有必要了解所有这些,也可以创建自己的抽象类。 </b> 第一件奇怪的事情是`metaclass`关键字参数被传递到类,通常看到的参数是父类列表。这是元类编程的神秘艺术中,一个很少使用的结构。我们不会在本书讲述元类,你需要知道的是通过指定`ABCMeta`元类,你的类将具有超能力(或者至少超类能力)。 </b> 接下来,我们看到@abc.abstractmethod和@abc.abstractproperty构造函数。这些是python装饰器。我们将在第5章“何时使用面向对象编程”中讨论。现在,只要知道通过标记一个方法或属性是抽象的,声明这个类的任何子类必须实现该方法或提供该属性,以便被视为类的正式成员。 </b> 看看如果你不给子类提供这些属性,会发生什么: ``` >>> 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`类提供了这两个属性,所以它可以干净地实例化。 </b> 回到`MediaLoader`抽象类,让我们剖析`__subclasshook__`方法。它基本上是说,任何提供所有`MediaLoader`抽象属性的类应被视为`MediaLoader`的一个子类,即使它实际上不是从`MediaLoader`类继承而来的。 </b> 更常见的面向对象语言在接口和类的实现上,有明显的不同。例如,一些语言提供显式的接口`interface`关键字,允许我们定义类必须有的方法,但不用实现它们。在这样的环境中,抽象类既提供了接口,又提供了部分方法(不是所有方法)的具体实现。任何类都可以显示声明它实现了一个给定的接口。 </b> Python的抽象类有助于在不丧失鸭子类型好处的情况下提供接口功能。 ### 揭开魔法的神秘面纱 如果你想创建抽象类来满足这个特殊的契约,你可以复制并粘贴子类代码,而不需要理解它。我们将涵盖大部分整本书中不同寻常的语法,但是让我们一行一行地来看,以了解全貌。 ``` @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 ``` 如果已经提供了所有的抽象方法,那么候选类就是子类,我们返回真。该方法可以合法地返回三个值中的任意一个:真、假或未实现。真和假表示类是或者不是这个类的子类: ``` return NotImplemented ``` 如果没有满足任何条件(也就是说,该类不是`MediaLoader`,或者没有提供所有抽象方法),则返回`NotImplemented`。这告诉Python机器使用默认机制(候选类显式扩展这个类?)用于子类检测。 简而言之,我们现在可以将Ogg类定义为`MediaLoader`类的子类,虽然我们没有实际扩展`MediaLoader`类用于获得`Ogg`类: ``` >>> class Ogg(): ... ext = '.ogg' ... def play(self): ... print("this will play an ogg file") ... >>> issubclass(Ogg, MediaLoader) True >>> isinstance(Ogg(), MediaLoader) True ``` ## 案例研究 让我们试着用一个更大的例子把我们所学的一切联系起来。我们会设计一个简单的房地产应用程序,允许中介管理可供购买或出租的物业。有两种类型的物业:公寓和住宅。中介需要能够输入一些有关新物业的相关细节,列出所有当前可用的物业,并将物业标记为出售或出租。为了简单起见,我们先不用担心编辑物业细节或重新激活一个售出的物业。 </b> 该项目将允许中介使用Python解释器窗口与对象进行交互。在这个GUI和WEB应用的世界里,你可能想知道为什么我们要创建这样老式的程序。简单地说,窗口程序和web应用程序都需要大量的知识开销和样板代码。如果我们使用这两种模式中的任何一种,我们都会迷失在GUI编程或WEB编程中,而忽略了我们正在尝试掌握的面向对象原则。 </b> 幸运的是,大多数GUI和WEB框架都使用面向对象的方法,我们现在研究的原理将有助于理解这些框架。我们将在第13章“并发性”中简要讨论这两个框架,但完整细节远远超出了一本书的范围。 </b> 看看我们的需求,似乎有相当多的名词可以代表我们系统中的对象类别。显然,我们需要代表一个物业的类。住宅和公寓可能需要单独的类。租赁和购买也似乎需要单独的代表。既然我们现在关注的是继承,我们将研究如何使用继承或多重继承来共享行为。 </b> 住宅`House`和公寓`Apartment`都是物业类型,所以物业`Property`可以是这两个类的超类。租赁`Rental`和购买`Purchase`需要一些额外的考虑;如果我们使用继承,我们需要有单独的类,例如,住宅租赁`HouseRental`和购买住宅`HousePurchase`的类,并使用多重继承来合并它们。与基于组合或关联的设计相比,这有点笨拙,但是让我们跑跑看,看看我们能想出什么。 </b> 那么,哪些属性可能与物业`Property`类相关联呢?不管是公寓还是住宅,大多数人都会想知道面积、卧室和浴室数量。(有很多其他可以用来建模的属性,但我们尽可能让我们的原型保持简单。) </b> 如果物业是一栋住宅,我们可能会想登广告宣传房子有几层,是否有车库(附属的、分离的或没有),以及院子是否有围栏。公寓则会显示它是否有阳台,洗衣房是套间的、硬币、还是公共的? </b> 两种物业类型都需要一个方法来显示物业特征。目前,没有其他明显的行为。 </b> 租赁物业将需要存储每月租金,以及物业是否有家具,是否包括公用设施,如果不包括,租金将是多少。代售的物业需要存储销售价格和预估的年度物业税。对于我们的应用程序,我们只需要显示这些数据,这样我们就可以只添加一个类似于其他类中使用的`display()`方法。 </b> 最后,我们需要一个中介`Agent`对象,它包含所有物业的列表,并显示这些物业属性,并允许中介创建新的物业。创建物业需要提示用户输入每种物业类型的相关详细信息。这可以在中介`Agent`对象中完成,但是`Agent`需要知道许多物业类型的信息。这没有利用多态性的优势。另一种选择是将提示放入每个类的初始化函数或者构造函数中,但这将不允许将来在GUI或WEB应用中使用这些类。更好的想法是创建一个静态方法(译注:Python in Nutshell P118,提到类级别的方法有两个:静态方法和类方法)来执行提示并返回提示参数的字典。然后,中介`Agent`所要做的就是提示用户物业类型和支付方式,并要求正确的类进行实例化。 </b> 设计太多了!下面的类图可以将我们的设计决策表达得更清楚一点: ![](https://box.kancloud.cn/64d73160f5d33390fedcb9bd52c4f413_478x421.png) 哇,有很多继承箭头!我认为不可能再添加没有交叉箭头的继承级别。多重继承就是这样一件麻烦的事情,甚至在设计阶段。 </b> 这些类最棘手的方面是确保在继承层次结构中正确得调用超类方法。让我们从物业`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`参数,因为我们知道`**kwargs`将被用于多重继承情况。我们还添加了`super()._init__`方法,以防我们不是多重继承链中的最后一个调用(译注:这句话的意思是,如果在多重继承链中,不是最后一个被调用,在中间位置,又没有使用`super`,是很危险的,会导致后面的参数都不会得到继承)。在这种情况下,我们正在*消费*键参数,因为我们知道在其他继承层次结构,这些键参数不是必须的。 </b> 我们在`prompt_init`方法中看到了一些新的东西。这种方法在最初创建后被设置成静态方法。静态方法只和类(类似于类变量)相关联,而不是特定的对象实例。因此,他们没有`self`变量。正因为如此,`super`关键词将不起作用(没有父对象,只有父类),所以我们简单地在父类里直接调用静态方法。此方法使用Python字典构造函数创建可以传递到`__init__`的值字典。每个键值通过调用`input`方法来提示输入。(译注:Python in Nutshell P118,提到静态方法和类、类的实例相关的,所以这里有点疑问。例如下面这段代码,类的实例也可以用静态方法) ``` class AClass(object): def astatic(): print('a static method') astatic = staticmethod(astatic) an_instance = AClass() AClass.astatic() a static method an_instance.astatic() a static method ``` 公寓`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`类已被正确的初始化。 </b> `prompt_init`静态方法现在正在从父类获取字典值,然后添加它自己的一些附加值。它调用`dict.update`方法将新字典值合并到第一个字典值中。然而,这个`prompt_init`方法看起来很难看;它循环两次,直到用户使用结构相似但变量不同的代码,进行有效的输入为止。提取这个验证逻辑比较好,以便我们只在一个位置维护它;很可能对以后的类也很有用。 </b> 到目前为止,所有的讨论都是关于继承的,我们可能认为这是一个使用`mixin`的好地方。然而,在这种情况下,我们有机会研究继承并不是最好的解决方案。我们想要创建的方法将被用在静态方法中。如果我们打算继承自一个提供验证功能的类,该功能将也必须作为不访问任何实例变量的静态方法提供给新的类上。如果它不访问任何实例变量,那么创建一个类有什么意义吗?为什么我们不把这个验证功能变成模块级的函数呢,它接受一个输入字符串和一个有效答案列表,并就此罢休? </b> 让我们探索一下这个验证函数是什么样子的: ``` 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) ``` 这更容易阅读(也更容易维护!),比我们的原始版本更好。现在我们准备建造住宅类。这个类有一个与公寓`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) ``` 这里没有什么新的可探索的,所以让我们转到购买`Purchase` 和租赁`Rental`类上。尽管目的明显不同,但它们和我们刚才讨论的类,在设计上是相似的: ``` 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`方法会按什么顺序调用。接口和用于住宅`House`和公寓`Apartment`的接口很相似,当我们在使用这四个类在不同子类中进行组合时,会很有帮助。例如: ``` 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`的静态方法,所以我们显式地实现了`prompt_init`。我们应该测试这个类,以确保在我们写下其他三个组合之前,它的表现良好: ``` >>> init = HouseRental.prompt_init() Enter the square feet: 1 Enter number of bedrooms: 2 Enter number of baths: 3 Is the yard fenced? Is there a garage? (yes, no) no (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`版本的`display`方法,它调用`super.display()`以获取`House`版本的`display()`,再次调用`super.display()`,将获取物业`Property`版本的`display()`。如果我们反转它,`display`指的是`House`类的`display()`。当调用`super`时,它调用`Property`父类上的方法。但`Property`在`display`方法中没有对`super`的调用。这意味着`Rental`将不会调用类的`display`方法!通过按照我们所做的顺序放置继承列表,我们确保`Rental`调用`super`,然后处理`House`一方的继承关系。你可能认为我们可以添加对`Property.display()`的`super`调用,但这是不行地,因为`Property`的下一个超类是`object`,而`object`是没有`display`方法的。解决这个问题的另一个方法是允许`Rental`和`Purchase`来扩展`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`来获得一个适合传递给构造函数的值字典。然后我们使用关键字参数语法将字典转换为参数并构造新对象加载正确的数据。 </b> 现在,我们的用户可以使用这个`Agent`类来添加和查看属性列表。添加一些功能将物业标记为可用或不可用,或者编辑和删除物业,并不需要做大量工作。我们的原型现在处于一个足够好的状态,并可以向一名真正的中介展示它的功能。看看演示是如何工作的: ``` >>> agent = Agent() >>> agent.add_property() What type of property? What payment type? (house, apartment) house (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? Is there a garage? (yes, no) yes (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? What payment type? (house, apartment) apartment (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 ``` ## 摘要 我们已经脱离了简单的继承,这是程序员工具箱里面向对象中最有用的工具之一,一直到最复杂的多重继承。继承可以用来增加现有类或内置类型的功能。将类似的代码抽象到父类中有助于增加可维护性。可以使用`super`和参数列表调用父类上的方法,使用多重继承时,列表必须安全格式化,这些调用才能工作。 </b> 在下一章,我们将讲述处理特殊情况的微妙艺术。 ## 参考资料 [Python类方法、静态方法与实例方法](https://www.cnblogs.com/blackmatrix/p/5606364.html) [Python super() 函数](https://www.runoob.com/python/python-func-super.html) [继承与多态](https://www.liaoxuefeng.com/wiki/897692888725344/923030507728352)