使用 Mix-ins 与 Python

作者:Chuck Esterbrook

Mix-in 编程是一种软件开发风格,其中功能单元在一个类中创建,然后与其他类混合使用。这乍听起来可能像简单的继承,但 mix-in 与传统类在一个或多个方面有所不同。通常,mix-in 不是任何给定类的“主要”超类,不关心它与哪个类一起使用,与类层次结构中分散的许多类一起使用,并在运行时动态引入。

使用 mix-in 有几个原因:它们可以在新的领域扩展现有类,而无需编辑、维护或与其源代码合并;它们保持项目组件(例如领域框架和接口框架)分离;它们通过提供一个可以根据需要组合的功能包来简化新类的创建;并且它们克服了子类化的限制,即如果原始类的对象仍在软件的其他部分中创建,则新的子类不起作用。

因此,虽然 mix-in 不是 Python 的一个独特的技术特性,但这种技术的好处值得研究。

Python 为 mix-in 开发提供了一种理想的语言,因为它支持多重继承,支持完全动态绑定,并允许对类进行动态更改。在我们深入研究 Python 之前,请允许我承认 mix-in 已经过时了。我第一次看到以 mix-in 编程命名的东西是在审查现已解散的 Taligent 项目时,该项目以其 Pink 操作系统和 CommonPoint 应用程序框架而闻名。然而,由于 C++ 不支持语言特性 #2,即完全动态绑定,或语言特性 #3,即运行时的动态更改,因此我对这种方法未能实现其发明者的所有希望并不感到惊讶。

我还看到过另一种以不同名称出现的 mix-in 编程实例。Objective-C 有一个漂亮的语言特性,称为类别,它允许您添加和替换现有类的方法,即使无法访问其源代码。

这对于修复现有系统类和扩展其功能非常有用。此外,结合动态加载库的能力,类别在改进应用程序结构和减少代码方面非常有效。

小道消息告诉我,Symbolics 的面向对象 Flavors 系统很可能是最早出现真正的 mix-in 的系统。设计师的灵感来自马萨诸塞州剑桥市的 Steve's Ice Cream Parlor,顾客从一种基本的冰淇淋口味(香草、巧克力等)开始,并添加任意组合的 mix-in(坚果、软糖、巧克力片等)。在 Symbolics 系统中,大型、独立的类被称为 flavors,而旨在增强其他类的小型辅助类被称为 mix-in。可以在 Web 上的 www.kirkrader.com/examples/cpp/mixin.htm 找到参考资料。

Objective-C:我很了解他

Python 2.0

Python 功能

在向逝者(Taligent)、半逝者(Objective-C)和传奇(Symbolics)致敬之后,让我们开始深入研究使 Python 成为 mix-in 编程的优秀语言的特性。首先,Python 支持多重继承。也就是说,在 Python 中,一个类可以继承多个类

class Server(Object, Configurable):
    pass

此外,Python 支持完全动态绑定。当向对象传递消息时,例如

obj.load(filename)
Python 将完全在运行时确定要调用哪个方法,这基于消息的名称和 obj 的类继承。这种行为按预期工作,并且易于记住。即使类继承或方法定义在运行时更改,它也会继续工作。

需要记住的一件事是关于多重继承的搜索顺序。搜索顺序从左到右遍历基类,并且对于任何给定的基类,深入到其祖先类。

当您创建 mix-in 时,请记住方法名称可能发生冲突。通过创建具有良好命名方法的独特 mix-in,您通常可以避免任何意外。最后,Python 支持对类层次结构进行动态更改。

大多数 Python “事物”,无论是列表、字典、类还是实例,都有一组可访问的属性。Python 类有一个名为 __bases__ 的属性,它是其基类的元组。与 Python 设计一致,您可以在运行时使用它。在清单 1 中看到的 Python 交互式解释器会话中,我们创建了两个类,然后在稍后更改了继承。我们在清单 1 中的人不太友善,所以让我们改变它。事实上,让我们改变所有人,这样我们就永远不会再遇到这个问题

<<< Person.__bases__ += (Friendly,)
<<< p.hello()
Hello

清单 1. 动态安装 Mix-in

上面的第一个语句更改了 Person 的基类。通过使用 +=(而不是 =),我们避免意外删除现有基类,特别是如果代码的未来版本使 Person 从另一个类继承。此外,看起来很有趣的表达式 (Friendly,) 指定了一个元组,该元组通常只是用括号括起来。但是,虽然 Python 很容易将 <If“Courier”>(x,y)<I$f$> 识别为两个元素的元组,但它将 <If“Courier”>(x)<I$f$> 识别为带括号的表达式。附加逗号强制元组识别。

MySQLdb Cursor Mix-ins

应用 mix-in 最直接的方法是在模块构建期间的设计时进行应用。Python 中更著名的第三方模块之一 MySQLdb 正是这样做的。

Python 为数据库访问定义了一个标准的编程接口,名为 DB API (https://pythonlang.cn/topics/database/)。Andy Dustman 的 MySQLdb 模块实现了这个接口,以便 Python 程序员可以连接并向 MySQL 服务器发送查询。它可以在 http://dustman.net/andy/python/MySQLdb/ 找到。

MySQLdb 为它创建的游标对象提供了三个主要功能。它在必要时报告警告;它将结果集存储在客户端或根据需要在服务器端使用它们,并且它以元组(例如,不可变列表)或字典的形式返回结果。

MySQLdb 没有将所有这些组合成一个庞大的类,而是为每个类定义了 mix-in 类

class CursorWarningMixIn:
class CursorStoreResultMixIn:
class CursorUseResultMixIn
class CursorTupleRowsMixIn:
class CursorDictRowsMixIn(CursorTupleRowsMixIn):

请记住,mix-in 是类,因此它们可以利用继承,正如我们在 CursorDictRowsMixIn 中看到的那样,它继承了 CursorTupleRowsMixIn。

上面的 mix-in 都不能独立存在:BaseCursor 类为任何类型的游标提供了所需的核心功能。通过将这些 mix-in 与 BaseCursor 结合使用,MySQLdb 提供了警告、存储和结果类型的每种组合(总共八种)。创建数据库连接时,您可以传递所需的游标类

conn = MySQLdb.connection (cursorclass=MySQLdb.DictCursor)

Mix-in 不仅有助于 MySQLdb 本身的创建。它们还通过允许您为自己的自定义游标类挑选功能来使其更具可扩展性。

请注意,这些类名称都带有 MixIn 后缀,以强调它们的性质。另一个常见的约定是在名称末尾附加 “-able” 或 “-ible”,例如 Configurable 或 NamedValueAccessible。

NamedValueAccessible

让我们以上一个为例。NamedValueAccessible mix-in 将方法 valueForKey() 添加到与其连接的任何类。对于 obj.valueForKey(name),此方法将返回以下内容之一

  • obj.name( )

  • obj._name( )

  • obj.name

  • obj._name

换句话说,valueForKey() 查找方法或属性(公共或私有),以便返回给定键的值。此方法的设计反映了 Python 对象通常通过属性和方法提供信息的事实。有关实现,请参见清单 2。

清单 2. 用于统一值访问的 Mix-in

此 mix-in 的一个有用的应用是实现用于编写日志的通用代码(参见清单 3)。

清单 3. 应用 NamedValueAccessible Mix-in 进行日志记录

通过简单地向 logColumns() 方法添加新键,可以在无需修改生成日志的代码(位于 logEntry() 中)的情况下扩展日志。更重要的是,您可以想象 logColumns() 可以从一个简单的配置文件中读取其字段列表。

由于 valueForKey() 方法的灵活性,事务对象本身可以自由地通过方法或属性提供给定值。使 mix-in 具有灵活性可以提高它们的实用性,并且这是一种可以随着时间推移而发展的艺术。

事后混合

到目前为止,我们已经看到了在类构建期间使用 mix-in 的示例。但是,Python 的动态特性也允许我们在运行时混合功能。最简单的技术是修改给定类的基类,如前所述。一个函数允许我们保持此操作不透明,并在需要时稍后增强它

def MixIn(pyClass, mixInClass):
    pyClass.__bases__ += mixInClass

让我们考虑一种情况,这种情况使 MixIn() 的实用性显而易见。在互联网应用程序的构建中,通常最好将域类与接口类分开。域类表示特定应用程序的概念、数据和操作。它们独立于操作系统、用户界面、数据库等。一些作者将域对象称为业务对象、模型对象或问题空间对象。

将域和接口分开是有道理的,原因有很多。为两个主要独立领域创建了个人关注点:问题的题材是什么?以及,应该如何呈现?无需修改或重写域类即可构建新接口。实际上,可以提供多个接口。

故事发布系统的域类可能包括 Story、Author 和 Site。这些类包含基本属性(例如标题、正文、姓名、电子邮件等)和各种操作(保存、加载、发布等)。

此类系统的一个接口可以是网站,该网站允许用户创建、编辑、删除和发布这些故事。在开发此类站点时,如果我们的域类(例如 Story)具有方法 renderView() 和 renderForm(),这将非常有用,这些方法编写 HTML 以显示故事或使用表单编辑故事。

使用 mix-in,我们可以在域类之外开发此类功能

class StoryInterface:
    def renderView(self):
        # write the HTML representation of the story
        pass
    def renderForm(self):
        # write the HTML form to edit the story
        pass

并在支持网站的代码中,像这样混合使用

from MixIn import MixIn
from Domain.Story import Story
MixIn(Story, StoryInterface)
如果您决定为发布系统创建一个 GUI 界面,则无需携带 HTML 机制(反之亦然)。域类专注于提供必要的数据和操作,确保在开发 GUI 时,您将拥有所需的一切。

有人可能会争辩说,可能会创建一个新类来将两者结合在一起

class StoryInterface:
    ...
from Domain.Story import Story
class Story(Story, StoryInterface): pass

或者有人可能会争辩说,为了获得相同的好处,可以将 StoryInterface 作为 Story 的子类。但是,考虑 Story 已经有其他域子类的情况

class Story: ...
class Editorial(Story): ...
class Feature(Story): ...
class Column(Story): ...
Story 的现有子类绝不会因简单地创建新的 Story 类或子类而受到影响。但是,Story 的动态 mix-in 也会影响 Editorial、Feature 和 Column。这就是为什么在许多情况下,静态方法在实践中不起作用,从而使动态方法不仅聪明,而且是必要的。

此外,请考虑在代码的某些部分(其中 Story 是硬编码的)中创建 Story 对象的情况。虽然这种做法很糟糕,但很常见。在这种情况下,创建 Story 的子类对忽略它们的代码没有任何影响。

关于动态 mix-in 的一个警告:它们可以更改现有对象的行为(因为它们更改了这些对象的类)。这可能会导致不可预测的结果,因为大多数类在设计时都没有考虑到这种类型的更改。使用动态 mix-in 的安全方法是在应用程序首次启动时,在创建任何对象之前安装它们。

MixIn() 的高级版本

我们可以添加到 MixIn() 的第一个增强功能是检查我们是否没有两次混合同一个类

def MixIn(pyClass, mixInClass):
    if mixInClass not in pyClass.__bases__
        pyClass.__bases__ += (mixInClass,)

在实践中,我发现更多时候,我希望我的 mix-in 方法具有高优先级,即使在需要时取代继承的方法。该函数的下一个版本将 mix-in 类放在基类序列的前面,但允许您使用可选参数覆盖此行为

def MixIn(pyClass, mixInClass, makeLast=0):
  if mixInClass not in pyClass.__bases__
    if makeLast:
      pyClass.__bases__ += (mixInClass,)
    else:
      pyClass.__bases__ = (mixInClass,) + pyClass.__bases__
为了使 Python 调用更具可读性,我建议对标志使用关键字参数
# not so readable:
MixIn(Story, StoryInterface, 1)
# much better:
MixIn(Story, StoryInterface, makeLast=1)
清单 4. 我们的 MixIn 的最终版本

这个新版本仍然不允许 mix-in 中的方法覆盖实际类中的方法。为了实现这一点,mix-in 方法实际上必须安装在类中。幸运的是,Python 足够动态来完成此操作。清单 4 给出了我们的 MixIn() 最终版本的源代码。默认情况下,它会将 mix-in 的方法直接安装到目标类中,甚至注意遍历 mix-in 的基类。调用是相同的

MixIn(Story, StoryInterface)

可以为新的 MixIn() 提供额外的 makeAncestor=1 参数,以获得旧的语义(例如,使 mix-in 成为目标类的基类)。将 mix-in 放在基类末尾的能力已被删除,因为我在实践中从未需要过它。

此函数的更高级版本可以返回(可能可选地)两个之间冲突的方法列表,或者在存在重叠时引发伴随此类列表的异常。

自动安装 Mix-ins

当大量使用事后 mix-in 时,MixIn() 函数的调用变得重复。例如,GUI 应用程序可能为存在的每个域类都有一个 mix-in,因此需要为每个域类调用这样的调用

from Domain.User import User
MixIn(User, UserMixIn)

一种解决方案是按名称将 mix-in 绑定到其目标类,并让应用程序在启动时安装这些 mix-in。例如,所有 mix-in 都可以直接以它们修改的类命名,并放入 MixIns/ 目录中。清单 5 中的代码将安装它们。

清单 5. 检测和安装以其类命名的 Mix-ins

其他用途

虽然探索更高级版本的 MixIn() 函数很有趣,但最重要的关键是应用它们以改进您的软件的能力。以下是一些其他用途来激发您的想象力

  • 一个类可以在读取配置文件后使用 mix-in 增强自身。例如,Web 服务器类可以根据其配置混合使用 Threading 或 Forking。

  • 程序可以提供插件:在启动时定位和加载以增强程序的软件包。那些实现插件的人可以利用 MixIn() 来增强核心程序类。

总结

Mix-in 非常适合提高模块化和增强现有类,而无需深入了解其源代码。这反过来又支持其他设计范例,例如域和接口的分离、动态配置和插件。Python 对多重继承、动态绑定和对类的动态更改的固有支持启用了一种非常强大的技术。在您继续编写 Python 代码时,请考虑 mix-in 可以增强您的软件的方式。

Chuck Esterbrook 是一位顾问、作家和企业家,他使用 Python (https://pythonlang.cn/) 和 Webware (http://webware.sourceforge.net/)。可以通过 ChuckEsterbrook@yahoo.com 联系他。

加载 Disqus 评论