在编程界,重复代码被认为是邪恶的。我们不应该在不同的地方有相同或相似代码的多个副本。
有许多方法可以合并具有类似功能的代码片段或对象。在本章中,我们将介绍最著名的面向对象原则:继承。如第 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'
大多数内置类型都可以进行类似的扩展。常用的扩展内置程序有object
、list
、set
、dict
、file
和str
。例如int
和float
等数字类型也偶尔从中继承。
所以,继承对于向现有类添加新行为来说非常重要,但是如何改变行为呢?我们的contact
类只允许名称和电子邮件地址。这对于大多数联系人来说可能已经足够了,但是如果我们想为好友添加一个电话号码呢?
正如我们在第 2 章中所看到的,Python 中的对象,我们只需在联系人构造后设置一个phone
属性,就可以轻松实现这一点。但是,如果我们想使第三个变量在初始化时可用,我们必须重写__init__
。重写意味着在子类中更改或用新方法(同名)替换超类的方法。这样做不需要特殊的语法;将自动调用子类的新创建方法,而不是超类的方法。例如:
class Friend(Contact):
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
可以覆盖任何方法,而不仅仅是__init__
。然而,在继续之前,我们需要解决本例中的一些问题。我们的Contact
和Friend
类有重复的代码来设置name
和email
属性;这会使代码维护变得复杂,因为我们必须在两个或多个地方更新代码。更令人担忧的是,我们的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
参数。这个例子在技术上有效;我们可以直接在类上访问不同的变量。但也有一些问题。
首先,如果我们忽略显式调用初始值设定项,超类可能会未初始化。这不会破坏这个示例,但在常见场景中可能会导致难以调试的程序崩溃。例如,设想尝试将数据插入到尚未连接的数据库中。
第二,更危险的是,由于类层次结构的组织,超类可能被多次调用。请看这个继承关系图:
来自Friend
类的__init__
方法首先调用Contact
上的__init__
,它隐式初始化object
超类(记住,所有类都派生自object
。Friend
然后调用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
的方法。两个子类重写该方法,然后另一个子类使用多重继承扩展这两个方法。这被称为钻石继承,因为类图的钻石形状:
让我们将此图转换为代码;此示例显示了调用方法的时间:
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
然后调用BaseClass
和super
调用确保类层次结构中的每个方法执行一次。
当我们回到Friend
多重继承示例时,这将使事情变得复杂。在Friend
的__init__
方法中,我们最初为两个父类调用__init__
,使用不同的参数集:
Contact.__init__(self, name, email)
AddressHolder.__init__(self, street, city, state, code)
使用super
时,我们如何管理不同的参数集?我们不一定知道哪个类super
将首先尝试初始化。即使我们这样做了,我们也需要一种方法来传递“额外”参数,以便在其他子类上对super
的后续调用接收正确的参数。
具体来说,如果第一次调用super
将name
和email
参数传递给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
的字典中(我们可以随意调用变量,但惯例建议使用kw
或kwargs
。当我们使用**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'] = phone
将phone
作为显式关键字参数,但在将其传递给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
的不同子类表示,例如WavFile
、MP3File
。每一个文件都有一个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 的名称和它实现的单个方法来看,很明显这个参数就是用户检查容器是否保存的值。
此方法由list
、str
和dict
实现,以指示给定值是否在该数据结构中。但是,我们也可以定义一个愚蠢的容器,告诉我们给定的值是否在奇数整数集中:
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 类型比经典多态性更可怕。我们可以创建是关系,而不需要使用继承(或者更糟糕的是,多重继承)的开销。
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
正如我们前面看到的一样,不需要有一个抽象基类来启用 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
类提供这两个属性,因此它可以干净地实例化。
回到MediaLoader
ABC,让我们剖析一下__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
。该方法可以合法地返回三个值中的一个:True
、False
或NotImplemented
。True
和False
表示该类是否为该类的子类:
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 章、并发中简要讨论这两个问题,但完整的细节远远超出了一本书的范围。
从我们的需求来看,似乎有相当多的名词可以表示系统中的对象类。显然,我们需要表示一个属性。房屋和公寓可能需要分开分类。租赁和购买似乎也需要单独的代表。因为我们现在关注的是继承,所以我们将研究使用继承或多重继承来共享行为的方法。
House
和Apartment
都是属性类型,因此Property
可以是这两个类的超类。Rental
和Purchase
需要额外考虑;如果我们使用继承,我们需要有单独的类,例如,HouseRental
和HousePurchase
,并使用多重继承来组合它们。与基于合成或关联的设计相比,这感觉有点笨拙,但让我们来看看我们能想出什么。
那么,哪些属性可能与Property
类相关联?不管是公寓还是房子,大多数人都想知道建筑面积、卧室数量和浴室数量。(有许多其他属性可能会被建模,但我们将对原型保持简单。)
如果物业是一栋房子,它会想宣传层数,是否有车库(附属、独立或无车库),以及院子是否有围栏。公寓需要标明是否有阳台,洗衣房是套间、投币式还是场外。
这两种属性类型都需要一个方法来显示该属性的特征。目前,没有明显的其他行为。
租赁物业将需要存储每月租金,物业是否配备家具,是否包括公用设施,如果不包括,估计是什么。购买物业需要存储购买价格和预计年度物业税。对于我们的应用程序,我们只需要显示这些数据,因此我们只需添加一个与其他类中使用的方法类似的display()
方法即可。
最后,我们需要一个Agent
对象来保存所有属性的列表,显示这些属性,并允许我们创建新的属性。创建属性需要提示用户提供每种属性类型的相关详细信息。这可以在Agent
对象中完成,但是Agent
需要知道很多关于属性类型的信息。这不是利用多态性。另一种选择是将提示放入每个类的初始值设定项甚至构造函数中,但这不允许将来在 GUI 或 web 应用程序中应用这些类。更好的方法是创建一个静态方法来执行提示并返回提示参数的字典。然后,Agent
所要做的就是提示用户输入属性类型和支付方式,并要求正确的类自身实例化。
这是一个很大的设计!下面的类图可以更清楚地传达我们的设计决策:
哇,真是太多了!我不认为在没有交叉箭头的情况下添加另一个继承级别是可能的。多重继承是一件棘手的事情,即使在设计阶段也是如此。
这些类最棘手的方面是确保在继承层次结构中调用超类方法。让我们从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)
这里没有什么新的东西需要探索,所以让我们继续学习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
,所以我们显式地实现了这个方法。在编写其他三种组合之前,我们应该测试该类,以确保其行为正常:
>>> 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
方法。另一种解决方法是允许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
类添加一个 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
调用父类上的方法,并且必须安全地格式化参数列表,以便在使用多重继承时这些调用能够工作。
在下一章中,我们将介绍处理特殊情况的微妙艺术。*