Python 高级编程(第2版)-- 第14章 有用的设计模式

有用的设计模式

  • 创建型模式(creational patterns):这些模式用于生成具有特定行为的对象。
  • 结构型模式(structural patterns):这些模式有助于为特定用例构建代码。
  • 行为模式(behavioral patterns):这些模式有助于分配责任和封装行为。

创建型模式

创建型模式处理对象实例化机制。这样的模式可以定义如何创建对象实例或者甚至如何构造类的方式。

编译型语言(如 C 或 C ++)在运行时难以生成需要的类型。但是在运行时创建新类型在 Python 中是相当简单的。使用内置的 type 函数可以通过代码定义一个新类型的对象。

类和类型是内置工厂。可以使用元类与类和对象生成进行交互。这些特性是实现工厂(factory)设计模式的基础。

除了工厂设计模式,在 Python 中唯一值得注意的其他创建型设计模式是单例。

单例(Singleton)限制类的实例化,只能实例化一个对象。

单例模式确保给定类在应用程序中始终只有一个存活的实例。例如,当你想要将资源访问限制为该进程中的一个且仅一个内存上下文时,可以使用此方法。

这种模式可以简化很多在应用程序中处理并发的方式。提供应用程序范围的功能的通用程序通常被声明为单例。例如,在 Web 应用程序中,负责保留唯一文档 ID 的类将受益于单例模式。应该有且只有一个通用程序做这个工作。

在 Python 中,一个常用的方法是通过覆写 __new__() 方法创建单例。如果你的单例被子类化的,实现这种方式非常危险。根据你的类的使用顺序,你可能会也可能不会得到相同的结果。

更安全的方式是使用更先进的技术——元类(metaclasses)。通过覆写元类的 __call__() 方法,可以影响自定义类的创建。可以安全子类化并且与实例创建顺序无关的单例。

另一种克服琐碎单例实现问题的方法是博格(Borg)或单态(Monostate)。这种方式在行为上类似于单例,但结构上完全不同。这个想法很简单。在单例模式中真正重要的不是类的存活实例的数量,而是它们在任何时候都共享相同的状态的事实。

单例工厂是一种处理应用程序的唯一性的隐式方法。除非你在需要这种模式的 Java 框架中工作,否则请使用模块而不是类。

结构型模式

结构型模式在大型应用中非常重要。它们决定代码的组织方式,并告诉开发人员如何与应用程序的每个部分进行交互。

很长一段时间以来,在 Python 中,Zope 项目中的 Zope 组件架构(Zope Component Architecture, ZCA)提供了最知名的许多结构型模式的实现。

Python 已经通过其语法提供了一些流行的结构型模式。例如,类和函数装饰器可以被认为是装饰器模式(decoratorpattern)的应用。此外,支持创建和导入模块是模块模式(module pattern)的一种发散。

常见的结构模式有很多,主要关注 3 个最受欢迎并且公认的模式,它们是:

  • 适配器(adapter);
  • 代理(proxy);
  • 外观(facade)。

适配器

使用适配器(adapter)模式可以在另一个接口中使用现有类的接口。换句话说,适配器包装一个类或一个对象 A,以便它能在目标上下文中工作,这可以是一个类或者一个对象 B。

在Python中创建适配器实际上是非常简单的,这归应于这种语言中的类型工作原理。Python 中的类型原理通常被称为鸭子类型(duck-typing)。

根据这个规则,函数或方法接受的值,不应该取决于它的类型,而应基于其接口。所以,只要对象的行为正如预期的那样,即具有适当的方法签名和属性,它的类型被认为是兼容的。

实际上,当某些代码用于处理给定的类时,只要它们提供了代码使用的方法和属性,就可以向它提供来自另一个类的对象。

适配器模式基于这个原理,并定义了一个包装机制,其中包装类或对象,以使其在主要不是为它工作的上下文中工作。StringIO 是一个典型的例子,虽然它适配 str 类型,同样它可以作为 file 类型。

接口(interface)主要进行 API 的定义。它描述了一个应该实现需要的行为的类的方法和属性的列表。这个描述不实现任何代码,只是为希望实现接口的任何类定义了显式契约。任何类都可以以任何想要的方式实现一个或多个接口。

虽然Python更喜欢使用鸭子类型,而不是显式接口定义,但有时后者可能更好。例如,显式接口定义使框架更容易定义接口上的功能。

在许多静态类型的语言中都内置了对这种技术的支持,接口允许函数或方法限制实现给定接口的可接受参数对象的范围,无论它来自哪个类。这比将参数限制到给定类型或其子类更灵活。

Python 有一个完全不同的类型原理,所以它没有本地支持的接口。无论如何,如果你想对应用程序接口有更明确的控制,通常有两种解决方案可供选择。

  • 使用一些添加接口概念的第三方框架。
  • 使用一些高级语言特性来构建处理接口的方法。

(1)使用 zope.interface

可以使用一些框架在 Python 中构建显式接口。最值得注意的一个是 Zope 项目的一部分。就是 zope.interface 包。zope.interface 包的核心类是 Interface 类。可以通过子类化来显式地定义一个新的接口。

使用 zope.interface 定义接口时需要注意的一些重要事项如下。

  • 接口的常用命名约定是使用I作为名称后缀。
  • 接口的方法不能使用 self 参数。
  • 由于接口不提供具体的实现,它应该只包含空方法。你可以使用 pass 语句,抛出 NotImplementedError,或提供 docstring(首选)。
  • 接口还可以使用 Attribute 类指定所需的属性。

通常,接口定义了具体实现需要满足的约定。此设计模式的主要优点是能够在使用对象之前验证约定和实现之间的一致性。使用普通的鸭子类型方法,只有当运行时缺少属性或方法时,才会发现不一致。使用 zope.interface,你可以使用 zope.interface.verify 模块中的两个方法内省实际实现,已在早期发现不一致。

  • verifyClass(interface, class_object):这将验证类对象是否存在方法以及它们的签名的正确性,无需查找属性。
  • verifyObject(interface, instance):它验证方法,它们的签名以及实际对象实例的属性。

使用 zope.inteface 是一个有趣的方法,可以用来解耦你的应用程序。你可以使用它强制适当的对象接口,而不需要过度复杂的多重继承,并且还可以提前捕获不一致。然而,这种方法的最大缺点是需要你明确定义给定类遵循某些接口以便进行验证。

(2)使用函数注解与抽象基类

设计模式旨在使问题的解决更容易,而不是为你提供更多复杂的层次。zope.interface 是一个很棒的概念,可能很适合一些项目,但它不是一个妙招。通过使用它,你很快会发现自己花费更多的时间来解决第三方类的不兼容接口的问题,并永不停止地提供适配器层,而不是编写实际的实现。

Python 支持构建轻量级的替代接口。它不是一个完整的像 zope.interface 或其替代品的解决方案,但它一般提供更灵活的应用程序。可能需要写更多的代码,但最终会得到更好的扩展性,更好地处理外部类型,以及更好的前瞻性(future proof)。

注意,Python 在其核心没有明确的接口概念,可能永远不会,但有一些特性,允许你建立一些类似于接口的功能。这样的特性如下。

  • 抽象基类(Abstract Base Classes, ABC)。
  • 函数注解(function annotations)。
  • 类型注解(type annotations)。

解决方案的核心是抽象基类。直接类型的比较是有害的,而非 Python 化(pythonic)。应该总是避免如下的比较,代码如下:assert type(instance) == list

比较函数或方法中的类型,完全破坏了将类的子类型作为参数传递给函数的能力。稍微好一点的方法是使用isinstance()函数,它会考虑到继承,代码如下:assert isinstance(instance, list)

isinstance() 的另一个优点是可以使用更大范围的类型来检查类型兼容性。例如,如果你的函数期望接收某种序列作为参数,则可以与列表基本类型进行比较,代码如下:assert isinstance(instance, (list, tuple, range))

这种类型兼容性检查的方法在一些情况下是可行的,但它仍然不完美。它可以与 list, tuple 或者 range 的任何子类一起使用,但如果用户传递的参数的行为与这些序列类型之一完全相同,但不继承任何它们,则会失败。例如,让我们放宽我们的要求,你想接受任何可迭代的类作为参数。你会怎么做?可迭代的基本类型的列表有很多。你需要覆盖 list、tuple、range、str、bytes、dict、set、generator 等等。适用的内置类型有很多,即使你覆盖所有的内置类型,它仍然不允许你检查定义了 __iter__() 方法的自定义类,而这些自定义类是直接继承自 object。

对于这种情况,抽象基类(Abstract Base Classes, ABC)是合适的解决方案。ABC 是一个不需要提供具体实现的类,而是定义了可用于检查类型兼容性的蓝图类。这个概念非常类似于 C++ 语言中的抽象类和虚拟方法的概念。

抽象基类用于两个目的。

  • 检查实现完整性。
  • 检查隐式接口兼容性。

抽象基类的另外两个功能是函数注解和类型提示。函数注解允许你用任意表达式注解函数及其参数。在类级别之下,这只是一个不提供任何语法意义的功能桩。在使用此功能的标准库中没有实用程序来强制执行任何行为。无论如何,你可以使用它作为一个方便且轻量级的方式通知开发人员期望的参数接口。

类型提示建立在函数注解之上,并重用这个稍微被遗忘的 Python 3 的语法特性。它们旨在指导类型提示和检查各种未来的 Python 类型检查器。类型提示由 PEP 484 详细描述,在新的typing模块中公开,并且在 Python 3.5 中可用。

(3)使用 collections.abc

抽象基类像是创建更高抽象级别的小构建块。它们允许你实现真正可用的接口,并且非常通用,同时被设计的可以比单一的设计模式能够处理更多。但创建一些通用的并且真正可用的类可能需要很多工作。这些工作可能永远不会有回报。

所以,自定义抽象基类不太常用。尽管如此,collections.abc 模块提供了许多预定义的 ABC,这些基类可以验证许多基本的 Python 类型的接口兼容性。使用此模块中提供的基类,可以进行检查,例如给定对象是可调用的,映射还是支持迭代。结合 isinstance() 函数使用它们比基于 python 类型的比较更好。

在 collections.abc 中,你可能经常会使用的抽象基类有。

  • Container:此接口意味着对象支持in运算符并实现 __contains__() 方法。
  • Iterable:此接口意味着对象支持迭代并实现 __iter__() 方法。
  • Callable:这个接口意味着它可以像一个函数一样被调用,并实现 __call__() 法。
  • Hashable:此接口意味着对象是哈希表(可以包含在集合中,作为字典中的键),并实现 __hash__ 方法。
  • Sized:此接口意味着对象具有大小(可以是函数 len() 的主体),并实现 __len__() 方法。

代理

代理提供对昂贵或远程资源的间接访问。代理(Proxy)在客户端(Client)和主题(Subject)之间,Client -> Proxy -> Subject。它旨在优化主题的访问。例如,第12章中描述的 memoize() 和 lru_cache() 装饰器可以被视为代理。

代理还可以用于提供对主题的智能访问。例如,可以将大视频文件包装到代理中,当用户只是请求其标题时,这可以避免将它们加载到内存中。

urllib.request 模块中有这样一个例子。urlopen 是位于远程 URL 的内容的代理。创建时,可以独立于内容本身检索头信息,而无需读取响应的其余部分。

代理的另一个使用范例是数据唯一性。

例如,让我们考虑一个网站,在多个位置呈现相同的文档。文档会附加上每个位置的特定字段,例如页面访问数和几个权限设置。可以使用代理来处理特定位置相关的情况,并且是指向原始文档而不是复制它。因此,给定文档可以具有许多代理,并且如果其内容改变,则所有位置都将受益,因为它们不必处理版本同步。

一般来说,代理模式主要用于实现可能存在于其他地方的某物的本地句柄。

  • 使处理更快。
  • 避免外部资源访问。
  • 减少内存负载。
  • 确保数据唯一性。

外观

外观(facade)提供对子系统的高层次,简单地访问。外观只不过是使用应用程序功能的快捷方式,而不必处理子系统的底层复杂性。可以这样做,例如,可以通过在包级别上提供高级功能来完成。

外观通常在现有系统上使用,其中包的频繁使用是在高级功能中合成的。通常,不需要类来提供这样的模式,在 __init__.py 模块中的简单函数就足够了。

外观简化了软件包的使用。外观通常在使用反馈的几次迭代之后添加。

行为模式

行为模式旨在通过结构化它们的交互过程来简化类之间的交互。本节提供了 3 个常用的行为模式的示例,在编写 Python 代码时,你可能需要考虑它们。

  • 观察者。
  • 访问者。
  • 模板。

观察者

观察者(observer)模式用于通知列表中的对象关于被观察组件的状态改变。

使用观察者可以通过从现有代码库中解耦新功能,以可插入的方式在应用程序中添加特性。事件框架是观察者模式的典型实现。每次事件发生时,此事件的所有观察者都会收到触发该事件的主题的通知。

事件触发时创建事件。在图形用户界面应用程序中,事件驱动编程经常用于将代码关联到用户操作上。例如,一个函数可以关联到MouseMove事件上,因此每次鼠标移动到窗口时都会调用该函数。

在 GUI 应用程序的情况下,从窗口管理内部解耦代码可以简化很多工作。函数是单独编写的,然后注册为事件观察器。这种方法存在于 Microsoft 的 MFC 框架的最早版本以及所有 GUI 开发工具中,例如 Qt 或 GTK。许多框架使用信号(signals)的概念,但它们只是观察者模式的另一种表现形式。

代码也可以产生事件。基于文档工作的新特性可以将其自身注册为观察器,每次创建,修改或删除文档时,它们就会收到通知,并执行相应的工作。文档索引器可以在应用程序中以这种方式添加。当然,这要求所有负责创建,修改或删除文档的代码都会触发事件。

解耦代码是很有趣的,观察者是处理该情况的正确模式。它组件化你的应用程序,并使其更具可扩展性。如果要使用现有的工具,请尝试 Blinker 。它为Python对象提供快速并且简单的对象到对象以及广播的信号传递。

访问者

访问者(visitor)帮助分离算法与数据结构,并具有与观察者模式类似的目标。它允许扩展给定类的功能而不改变其代码。但是访问者做的更多的是,通过定义一个负责保存数据的类,并将算法推送到称为访问者的其他类。每个访问者专用于一种算法,并且可以将其应用于数据。

这种行为与 MVC 范式非常相似,其中文档是通过控制器推送到视图的被动容器,或者模型包含被控制器改变的数据。

访问者模式通过在数据类中提供可由各种访问者访问的入口点来实现。通用描述是一个 Visitable 类,它接受 Visitor 实例并调用它们。

如果你的应用程序具有由多个算法访问的数据结构,则访问者模式将有助于分离关注点。对于数据容器来说,最好只专注于提供数据访问和持有数据,而无需关心其他任何事情。

模板

模板(template)通过定义抽象步骤来帮助设计一个通用算法,这些抽象步骤由子类来实现。这种模式使用里氏替换原则(Liskov substitution principle)。维基百科中这样定义:“如果S是T的子类型,则程序中类型T的对象可以用类型S的对象替换,而无需改变该程序的任何期望属性。”

换句话说,抽象类可以通过在具体类中实现的步骤来定义算法如何工作。抽象类还可以给出算法的基本或部分实现,并允许开发人员覆写其部分。例如,可以覆写队列模块中的 Queue 类的一些方法以改变其行为。

对于可以变化并且可以被表示为独立的子步骤的算法,应当考虑模板。这可能是 Python 中最常用的模式,并不总是需要通过子类来实现。例如,许多处理算法问题的内置 Python 函数接受允许将部分实现委托给外部实现的参数。例如,sorted() 函数允许一个可选的 key 关键字参数,稍后由排序算法使用。在给定集合中找到最小值和最大值的 min() 和 max() 函数也是如此。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值