返回介绍

多重继承:混合类

发布于 2024-01-29 22:24:15 字数 7953 浏览 0 评论 0 收藏 0

很多基于类的设计都要求组合方法的全异的集合。在class语句中,首行括号内可以列出一个以上的超类。当这么做时,就是在使用所谓的多重继承:类和其实例继承了列出的所有超类的变量名。

搜索属性时,Python会由左至右搜索类首行中的超类,直到找到相符者。从技术上来讲,因为任何超类本身可能还有一些其他的超类,对于更大的类树,这个搜索可以更复杂一点。

·在传统类中(默认的类,直到Python 3.0),属性搜索处理对所有路径深度优先,直到继承树的顶端,然后从左到右进行。

·在新式类(以及Python 3.0的所有类中),属性搜索处理沿着树层级、以更加广度优先的方式进行(参见下一章中关于新式类的介绍)。

不管哪种方式,当一个类拥有多个超类的时候,它们会根据class语句头部行中列出的顺序从左到右查找。

通常意义上讲,多重继承是建模属于一个集合以上的对象的好办法。例如,一个人可以是工程师、作家、音乐家等,因此,可继承这些集合的特性。使用多重继承,对象获得了所有其超类中行为的组合。

也许多重继承最常见的用法是作为“混合”超类的通用方法。这类超类一般都称为混合类:它们提供方法,你可通过继承将其加入应用类。例如,Python打印类实例对象的默认方式并不是很好用。从某种意义上讲,混合类类似于模块:它们提供方法的包,以便在其客户子类中使用。然而,和模块中的简单函数不同,混合类中的方法也能够访问self实例,以使用状态信息和其他方法。下一小节展示了这些工具的一种常见使用方法。

编写混合显示类

正如我们所见到的,Python的默认方式来打印一个类实例对象,并不是难以置信的有用:

就像我们在第29章学习运算符重载的时候所见到的,你可以提供一个__str__或__repr___方法,以实现制定后的字符串表达形式。但是,如果不在每个你想打印的类中编写__repr___,为什么不在一个通用工具类中编写一次,然后在所有的类中继承呢?

这就是混合类的用处。在混合类中定义一个显示方法一次,使得我们能够在想要看到一个定制显示格式的任何地方重用它。我们已经看到了做相关工作的工具:

·第27章的AttrDisplay类在一个通用的__str__方法中格式化了实例属性,但是,它没有爬升类树,并且只是用于单继承模式中。

·第28章的classtree.py定义了函数以爬升和遍历类树,但是,它没有显示对象属性,并且没有架构为一个可继承类。

这里,我们将继续回顾这些示例的技术,并且在它们的基础上扩展编码一组3个混合类,这3个类充当通用的显示工具,以列出一个类树上所有对象的实例属性、继承属性和属性。我们还将在多继承模式中使用我们的工具,并利用编码技术使得类更好地适合于用作通用工具。

用__dict__列出实例属性

让我们从一个简单的例子开始——列出附加给一个实例的属性。如下的类编写在文件lister.py中,它定义了一个名为ListInstance的混合类,它对于将其包含到头部行的所有类都重载了__str__方法。由于ListInstance编写为一个类,所以它成为一个通用工具,其格式化逻辑可以用于任何子类的实例:

ListInstance使用前面介绍的一些技巧来提取实例的类名和属性:

·每个实例都有一个内置的__class__属性,它引用自己所创建自的类;并且每个类都有一个__name__属性,它引用了头部中的名称,因此,表达式self.__class__.__name__获取了一个实例的类的名称。

·这个类通过直接扫描实例的属性字典(别忘了,它从__dict__中导出),以构建一个字符串来显示所有实例属性的名称和值,从而完成其主要工作。字典的键通过排序,以避免Python跨版本的任何排序差异。

在这些方面,ListInstance类似于第27章的属性显示;实际上,它很大程度上只是一个主题的变体。这里,我们的类显示了两种其他技术:

·它通过调用id内置函数显示了实例的内存地址,该函数返回任何对象的地址(根据定义,这是一个唯一的对象标识符,在随后对这一代码的修改中有用)。

·它针对其工作方法使用伪私有命名模式:__attrnames。正如我们在本章前面所了解到的,Python通过扩展属性名称以包含类名,从而把这样的名称本地化到其包含类中(在这个例子中,它变成了_ListInstance__attrnames)。对于附加到self的类属性(如方法)和实例属性,都是如此。这种行为在这样的通用工具中很有用,因为它确保了其名称不会与其客户子类中使用的任何名称冲突。

由于ListInstance定义了一个__str__运算符重载方法,所以派生自这个类的实例在打印的时候自动显示其属性,只给定了比简单地址多一点的信息。如下是使用中的类,在单继承模式中(这段代码在Python 3.0和Python 2.6下都同样工作):

我们可以把列表输出获取为一个字符串,而不用str打印出它,并且交互响应仍然使用默认格式:

ListInstance对于我们所编写的任何类都很有用,即便类已经有一个或多个超类。这就是多继承的用武之地,通过把ListInstance添加到一个类头部的超类列表中(例如,混合进去),我们可以在仍然继承自己有超类的同时“自由地”获得__str__。文件testmixin.py展示如下:

这里,Sub从Super和ListInstance继承了名称,它是自己的名称与其超类中名称的组合。当我们生成一个Sub实例并打印它,就会自动获得从ListInstance混合进去的定制表示(在这个例子中,这段脚本的输出在Python 3.0和Python 2.6下都是相同的,除了对象地址不同):

ListInstance在它混入的任何类中都有效,因为self引用拉入了这个类的子类的一个实例,而不管它可能是什么。从某种意义上讲,混合类是模块的类等价形式——它是在各种客户中有用的方法包。例如,下面是再次在单继承模式中工作的Lister,它作用于一个不同的类实例之上,使用import,并且带有类之外的属性设置:

它们除了提供这一工具,还像所有的类一样,混入了优化代码维护。例如,如果你稍后决定扩展ListInstance的__str__也打印出一个实例继承的所有类属性,你是安全的;因为它是一个集成的方法,修改_str__自动地更新导入该类和混合该类的每个子类的显示。这会儿真的很晚了,我们将在下一小节看看这样的扩展。

使用dir列出继承的属性

我们的Lister混合类只显示实例属性(例如,附加到实例对象自身的名称)。扩展该类以显示从一个实例可以访问的所有属性,这也是很容易的——这包括它自己以及它所继承自的类。技巧是使用dir内置函数,而不是扫描实例的__dict__字典,后者只是保存了实例属性,但是,在Python 2.2及以后的版本中,前者也收集了所有继承的属性。

如下修改后的代码实现了这一方案,我们已经将其重新命名,以便使得测试更简单,但是,如果用这个替代最初的版本,所有已有的客户类将自动选择新的显示:

注意,这段代码省略了__X__名称的值;这些大部分都是内部名称,我们通常不会在这样的通用列表中注意到。这个版本必须使用getattr内置函数来获取属性,通过指定字符串而不是使用实例属性字典索引——getattr使用了继承搜索协议,并且我们在这里列出的一些代码没有存储到实例自身中。

要测试新的版本,修改testmixin.py文件并使用新的类来替代:

这个文件的输出随着每个版本而变化。在Python 2.6中,我们得到如下输出。注意,名称压缩在lister的方法名中起作用(我缩减了其全部的值显示,以节省篇幅):

在Python 3.0中,更多的属性显示出来,因为所有的类都是“新式的”,并且从隐式的object超类那里继承了名称(关于object的更多内容在第31章介绍)。由于如此多的名称继承自默认的超类,我们已经在这里省略了很多。自行运行程序以得到完整的列表:

这里注意一点,既然我们也显示继承的方法,我们必须使用__str__而不是__repr__来重载打印。使用__repr__,这段代码将会循环,显示一个方法的值,该值触发了该方法的类的__repr__,从而显示该类。也就是说,如果lister的__repr__试图显示一个方法,显示该方法的类将再次触发lister的__repr__。很微妙,但确实如此!在这里,自己把__str__修改为__repr__来看看。如果你在这样的环境中使用__repr__,可以使用isinstance来比较属性值的类型和标准库中的types.MethodType,以知道省略哪些项,从而避免循环。

列出类树中每个对象的属性

让我们来看最后一个扩展。我们的lister没有告诉我们一个继承名称来自哪个类。然而,正如我们在第28章末尾的classtree.py示例中看到的,在代码中爬升类继承树很容易。如下的混合类使用这一名称技术来显示根据属性所在的类来分组的属性,它遍历了整个类树,在此过程中显示了附加到每个对象上的属性。它这样遍历继承树:从一个实例的__class__到其类,然后递归地从类的__bases__到其所有超类,一路扫描对象的__dicts__:

注意,这里使用一个生成器表达式来导向对超类的递归调用,它由嵌套的字符串join方法激活。还要注意,这个版本使用Python 3.0和Python 2.6的字符串格式化方法而不是%来格式化表达式,以使得替代更清晰。当像这样应用很多替代的时候,明确的参数数目可能使得代码更容易理解。简而言之,在这个版本中,我们把如下的第一行与第二行交换:

现在,修改testmixin.py,再次测试新类继承:

在Python 2.6中,该文件的树遍历输出如下所示:

注意,在这一输出中,方法现在在Python 2.6下是无绑定的,因为我们直接从类获取它们,而不是从实例。还注意lister的__visited表把自己的名称压缩到实例的属性字典中;除非我们很不走运,这不会与那里的其他数据冲突。

在Python 3.0下,我们再次获得了额外的属性和超类。注意,无绑定的方法在Python 3.0中是简单函数,正如本章前面的提示中所介绍的(在这里,我们再次删除了对象中的大多数内置对象以节省篇幅,请自行运行这段代码以获取完整的列表):

这个版本通过保留一个目前已经访问过的类的表来避免两次列出同样的类对象(这就是为什么一个对象的id包含其中,以充当一个之前显示项的键)。和第24章的过渡性模块重载程序一样,字典在这里用来避免重复和循环,因为类对象可能是字典键。集合也可以提供类似的功能。

这个版本还会再次通过省略__X__名称来避免较大的内部对象。如果你注释掉这些名称的测试,它们的值将会正常显示。这里是在Python 2.6下输出的摘要,带有这一临时性的修改(整个输出很大,并且在Python 3.0中这种情况甚至变得更糟,因此,这些名字可能会有所省略):

为了更有趣,尝试把这个类混合到更实质的某些内容中,例如Python的tkinter GUI工具箱模块的Button类。通常,我们想要在一个类的头部命名ListTree(最左端),因此,它的__str__会被选取;Button也有一个,并且在多继承中最左端的超类首先搜索。如下的输出相当庞大(18K个字符),因此,自己运行这段代码看看完整的列表(并且,如果你在使用Python 2.6,记住应该对模块名使用Tkinter而不是tkinter):

当然,在这里还有更多的事情可以做(遍历一个GUI中的树可能自然而然是下一步),但是,我们将把进一步的工作保留为一个推荐练习。我们将在本书这一部分末尾的练习中扩展这些代码,以在实例和类显示的开始处的括号中列出超类名称。

这里的主要核心是,OOP是完全关于代码复用的,并且混合类也是一个强大的示例。和几乎编程中所有的内容一样,多继承在应用得当的时候也是一种有用的工具。实际上,它是一种高级功能,如果不注意或滥用的话,可能变得很复杂。我们将在下一章的末尾重新回顾这一主题成为陷阱的情况。在那一章中,我们还将遇到新式类模式,它针对一种特殊的多继承情况修改搜索代码。

注意:支持slot:由于它们扫描示例词典,所以这里介绍的ListInstance和ListTree类不能直接支持存储在slot中的属性——slot是一种新的、相对很少使用的选项,我们将在下一章中介绍,在那里,示例属性将在一个__slots__类属性中声明。例如,如果在textmixin.py中,我们在Super中赋值__slots__=['data1'],在Sub中赋值__slots__=['data3'],只有data2属性通过这两个lister类显示在该实例中;ListTree也会显示data1和data3,但是是作为Super和Sub类对象的属性,并且是它们的值的一种特殊格式(从技术上讲,它们都是类级别的描述符)。

要更好地支持这些类中的slot属性,把__dict__扫描循环修改为使用下一章给出的代码来迭代_slots__列表,并且使用getattr内置函数来获取值,而不是使用__dict__索引(ListTree已经这么做了)。既然实例只继承最低的类的__slots__,当__slots__列表出现在多个超类中的时候,你可能也需要提出一种政策(ListTree已经将它们显示为类属性)。ListInherited对所有这些都是免疫的,因为dir结果组合了__dict__名称和所有类的__slots__名称。

此外,作为一项政策,我们可以直接允许代码处理基于solt的属性(就像它当前所做的那样),而不是将其复杂化为一种少用的、高级的特性。slot和常规的实例属性是不同的名称。我还将在下一章进一步介绍slot,我在这些示例中省略了对它们的介绍,以避免进一步的依赖性——这并非一项有效的设计目标,但是对本书来说是合理的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文