购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1 基本继承

严格来说,我们创建的所有类都使用了继承关系。所有的Python类都是名为object的特殊内置类的子类。object类提供了类的基本数据和行为(它提供的所有方法都是双下画线开头__的特殊方法,只在内部使用),从而允许Python以同样的方式对待所有对象。

如果我们不明确地从其他类继承,那么我们的类将默认继承自object。当然,我们也可以声明我们的类继承自object,使用下面的语法:

这就是继承关系!严格来说,这个示例与第2章的第一个示例没有区别。因为如果没有明确提供其他 超类 ,那么Python 3中的所有类都会默认继承自object。超类,或者是父类,是指被继承的类,子类是继承自超类的类。在这个示例中,超类是object,子类是MySubClass。通常称子类源自父类或子类扩展自父类。

可能你已经从这个示例中搞清楚,继承关系只需要在类定义的基本语法上添加少量额外语法就可以:只要将父类的名称放进类名后及冒号前的括号内即可,这样就可以告诉Python新类应该继承自给定的父类。

在实践中应该如何应用继承关系?最简单和最明显的用法就是为已存在的类添加功能。让我们从一个简单的联系人管理器开始,这个管理器可以追踪多个人的名字和E-mail地址。Contact类用一个类变量维护所有联系人的全局列表,并为每个联系人初始化姓名和地址:

这个示例向我们介绍了 类变量 :all_contacts列表,由于它是类定义的一部分,因此被这个类的所有实例所共享。这意味着只有一个Contact.all_contacts列表。我们也可以通过Contact.all_contacts访问,也可以在Contact对象中通过self.all_contacts访问,如果对象中找不到相应的变量(通过self),就会从类中去寻找,从而都会指向同一个列表。

使用self访问变量时,有一个要注意的地方。使用self可以读取类变量的值。但如果你用self.all_contacts=来 设定 变量的值,你实际上会创建一个只与那个对象相关的 新的 实例变量。原来的类变量将不会改变,仍然可以通过Contact.all_contacts访问。

通过下面的示例,可以看出类变量Contact.all_contacts记录了所有的联系人:

这个简单的类允许我们追踪每个联系人的一些数据,但是如果我们的某些联系人同时也是供货商,我们需要从他们那里下单,该怎么办?我们可以为Contact类添加一个order()方法,但是这样将会造成可以给客户、家人、朋友等不是供应商的联系人下单,这是不合理的。更好的做法是,创建一个新的Supplier类,它继承自Contact类,但是拥有一个额外的order()方法,这个方法接收一个尚未定义的Order对象作为参数:

现在,用交互式Python来测试这个类,我们可以发现所有的Contact(包括Supplier)的__init__方法都接收name和email参数,但是只有Supplier实例有order()方法:

所以,所有Contact能做的事情我们的Supplier类也可以做(包括把它自己加入Contact.all_contacts列表中),同时它也能做供货商需要处理的特殊事务。这就是继承之美。

注意,Contact.all_contacts保存了所有Contact类的实例,以及它的子类Supplier的实例。但如果我们使用self.all_contacts,那么将 不会 把所有对象都保存到Contact类中,因为Supplier的实例将会被保存在Supplier.all_contacts中。

3.1.1 扩展内置对象

继承的一个有趣的用法是给内置类添加新功能。在前面看到的Contact类中,我们将联系人添加到所有联系人列表中。如果想要根据名字搜索这个列表呢?我们可以为Contact类添加一个搜索方法,但是这个方法似乎应该属于列表本身。

下面的示例展示了如何通过继承内置类来实现这个功能,在这里我们继承list类。我们规定新定义的list子类只能存放Contact的实例,我们可以使用list["Contact"]的写法把这一规定告诉 mypy 工具。为了使这个语法在Python 3.9中生效,我们需要从__future__包中引入annotations模块:

我们没有使用通用的list类作为实例变量,而是创建了一个新的ContactList类来继承内置的list数据类型,然后实例化这个子类并将其赋值给all_contacts列表。我们可以用如下的方式测试这个新的搜索功能:

我们有两种创建通用list对象的方法。使用类型提示,我们有了另一种不需要实际创建列表实例就可以声明列表变量的方法。

首先,用[]创建一个空列表,实际上这是用list()创建空列表的简便方式,这两种语法完全一样:

实际上,[]语法就是所谓的 语法糖 syntax sugar ,其在底层调用list()构造方法。这种写法只需要写2个字符([]),而不是6个字符(list())。这里的list是指一种数据类型,是一个我们可以继承的类。

mypy 或类似的工具可以检查ContactList.search()方法,以确保它创建了一个只包含Contact对象的list实例。请使用0.8.2或更新的版本,因为老版本的 mypy 不完全支持这些基于泛型的注解。

因为Contact类的定义在ContactList定义的下面,也就是说,在定义ContactList的时候还没有定义Contact类,所以我们在指定类型的时候要使用字符串代表未被定义的Contact类,使用这种写法:list["Contact"]。但通常我们会先定义被引用的类,然后再定义使用它的类。如果我们先定义Contact类,再定义ContactList类,就可以直接使用类名而不用写字符串,也就是这样:list[Contact]。

作为第二个示例,我们可以扩展dict类。它是一些键值对的集合。与列表相似,它可以用{}作为语法糖,更简单地构造字典。下面是一个字典的扩展类,它可以追踪字典中最长的key:

类型提示dict[str,int]规定了这个类的key必须是str类型的,value必须是int类型的。这样可以帮助 mypy 判定longest_key()方法的合理性。因为key是字符串,所以可以在方法中使用len判定key的长度。最后的结果是一个str类型或者None,所以方法的返回值被描述为Optional[str]。(返回None合理吗?也许不合理,或许抛出ValueError异常更合理一点儿,不过这要等到第4章介绍)。

我们定义的类处理的是字符串和整数。也许字符串是用户名,整数是用户在网站上读过的文章数。除了核心用户名和阅读历史,我们也需要知道最长的名字有多长,这样就可以确定展示用户名和阅读历史的表格需要多宽。我们可以在交互式解释器中简单测试一下:

如果我们需要一个更常规的字典,该怎么办呢?比如,字典中的值可能是字符串 整数。我们需要使用一个更宽泛的类型提示,可以这样写:dict[str,Union[str,int]]。使用Union,我们指定的字典的值可能是字符串或整数。

大多数内置类型都可以用相似的方法扩展。这些内置类型可以分为几类,它们有各自的类型提示。

泛型集合set、list、dict,使用形如set[something]、list[something]和dict[key,value]的类型提示指定集合中可以存放的具体类型(something),而不是存放什么都可以。为了使用这种泛型类型的注解,需要在代码第一行加上from__future__import annotations。

· 使用typing.NamedTuple可以定义新的不可变元组,并可以给元组中的元素命名。这会在第7章和第8章中涵盖。

· Python具有与文件相关的I/O对象的类型提示。一种新的文件可以使用类型提示typing.TextIO或typing.BinaryIO来描述内置的文件操作。

· 通过扩展typing.Text可以创建新的字符串类型。在大多数情况下,内置的str类可以满足我们的所有需求。

· 新的数字类型通常衍生于numbers模块中的内置数字类型,因为它们提供了很多数字类型的基本功能。

我们将在本书中大量使用泛型集合。如前所述,我们将在后面的章节中讨论命名元组。内置类型的其他扩展对本书来说太高级了,因而不会涵盖。在下一节中,我们将更深入地研究继承的好处,以及如何在子类中选择性地利用超类的特性。

3.1.2 重写和super

继承关系很适合向已存在的类中添加新的行为,但是如何修改某些行为?我们的contact类只接收name和email作为初始化参数。这对于大部分联系人足够了,但是如果想要为好朋友添加一个电话号码,该怎么办?

正如我们在第2章中看到的,我们可以在构造完联系人之后设定新的phone属性。如果想要让第3个变量可以在初始化过程中设定,则必须重写__init__()方法。重写意味着在子类中修改或替换超类原有的方法(用相同的名称)。重写不需要特殊的语法,子类中新创建的方法将会被优先调用,而不是用超类的方法,如下面的代码所示:

任何方法都可以被重写,不只是__init__()。但在继续下去之前,我们先说明一下这个示例中的问题。我们的Contact和Friend类设定name和email属性的代码是重复的;这会让代码维护更复杂,因为我们不得不在多个地方同时更新代码。更不利的是,这样Friend类就无法将自己添加到Contact类上创建的all_contacts列表中。最后,如果我们给Contact类添加了新的功能,我们希望Friend类也自动拥有这个新功能。

我们真正需要做的是,在新的Friend类中执行Contact类上原有的__init__()方法。这正是super()函数的功能,它返回父类实例化得到的对象,让我们可以直接调用父类的方法:

这个示例首先用super()获取父类对象的实例,然后调用它的__init__()方法,传入所需的参数。然后执行它自己的初始化过程,也就是设定phone属性,该属性是Friend类独有的。

Contact类中定义了一个__repr__()方法,用来生成一个代表对象的字符串。我们的Friend类并没有重写这个方法。这样的后果是:

从输出结果可以看出,打印的字符串并没有包含新的phone属性。在设计类时,很容易忽略某些特定方法的定义。

super()可以在任何方法中被调用。因此,所有父类方法都可以通过重写和调用super()进行修改。我们也不必在第一行调用super(),而是在任何一行代码中调用super()。比如,我们可能需要先验证或修改传入的参数,然后调用super()把参数传递给父类。 dG6uIov8rVxCOA0JgzRFT+VLUTc3TGsrWkHGDcHvq/7roogEO3Ed2KHIF6FE27u8

点击中间区域
呼出菜单
上一章
目录
下一章
×