当前位置: 首页 > news >正文

Python Cookbook-6.16 用 Borg 惯用法来避免“单例”模式

任务

你想保证某个类从始至终只创建了一个实例:你并不关心生成的实例的 id,只关心其状态和行为方式,而且你还想确保它具有子类化能力。

解决方案

和“单例”模式相关的应用程序可以用另一种方式来实现,即允许多个实例被创建但所有的实例都共享状态和行为方式。比起在实例的创建中做手脚,这种方式要灵活得多。可以从下面的 Borg 类派生子类:

class Borg(object):_shared_state = { }def __new__(cls,*a,**k):obj = object.__new__(cls,*a,**k)obj.__dict__ = cls._shared_statereturn obj

如果你重载了子类的__new__(只有极少数类才需要这么做),要记住使用 Borg.new,而不是 object.new。如果需要你的类实例能够相互共享状态,但却不和Borg 的其他子类实例共享,需要在类作用域中加入这样的声明:

_shared_state = { }

通过这种“数据重载”,你的类不会从 Borg 继承_shared_state 属性,而是定义自己的数据。为了允许这种“数据重载”,Borg的__new__应当使用 cls._shared_state,而不是Borg. _shared_state。

讨论

下面是一个典型的 Borg 用法:

if __name__ == '__main__':class Example(Borg):name = Nonedef __init__(self, name = None):if name is not None:self.name = namedef __str__(self):return 'name->%s' %self.namea = Example('Lara')b = Example()	#实例化b,和a共享self.nameprint a,bc = Example('John Malkovich')#让c用改过的self.name,同时也改了a&bprint a,b,cb.name = 'Seven'#设置b.name,同时也改了a&c的nameprint a,b,c

如果把这个模块当作脚本运行,输出是:

name->Lara name->Lara
name->John Malkovich name->John Malkovich name->John Malkovich
name->Seven name->Seven name->Seven

Example 的所有实例都共享状态,所以对任何实例的名字属性的设置,无论是在__init__中修改还是直接修改,所有的实例都会受到影响。然而,请注意实例的id是不同的,既然我们没有定义特殊方法__eq__和__hash__,则每个实例都可以作为字典的独立的键。因此,如果我们继续我们的示例代码:

adict = {}
j = 0
for i in a,b,c:adict[i] = jj = j + 1
for i in a,b,c:print i,adict[i]

其输出是:

name->Seven 0 
name->Seven 1
name->Seven 2

如果这种行为方式不是你想要的,可以给Example 类或者 Borg 超类增加__eq__和__hash__ 方法。增加了这些方法之后,我们的示例就能够更好地模拟单例模式了,不过这取决于应用的实际需求。比如,下面给出一个增加了这些特殊方法的 Borg 版本:

class Borg(object):_shared_state = {}def __new__(cls,*a,**k):obj = super(Borg,cls).__new__(cls,*a,**k)obj.__dict__ = cls._shared_statereturn objdef __hash__(self):return 9#任意常数def __eq__(self,other):try:return self.__dict.__ is other.__dict__except AttributeError:return False

使用这个进一步修改后的 Borg 版本,例子的输出变成了:

name->Seven 2
name->Seven 2
name->Seven 2

选 Borg,还是单例模式,还是都不用?

单例模式是一个很容易记住的名字,但不幸的是,大多数的用途的关注点和它的关注点都不太契合:它关注对象的身份,而不是对象的状态和行为。Borg准模式(Borgdesignnonpatterm)则使得所有的实例共享状态,Python的能力也使得我们可以轻易地实现这样的模式。

在很多需要考虑使用单例还是 Borg 的时候,你其实可能并不需要用它们中的任何一种。可以直接写一个 Python 模块,带有函数和模块作用域中的全局变量,而不需要定义一个带有方法以及属性的类。只有当需要从类派生或者利用类的特性定义特殊方法时才应当使用类(见6.2节展示的一种综合类和模块的优点的方法)。即使你确实需要类通常也没必要在类中加入一些代码来迫使其不支持多个实例。其实,更简单的用法通常也更好用。举个例子:

class froober(object):def __init__(self):etc, etc
froober = froober()

很自然地,现在 foober 就是它自己类的唯一实例,这是因为名字“froober”已经被重新绑定到了实例,而不是类。当然,别人也可以调用foober.class(),但在阻止别人故意滥用你的设计上付出太多的努力并不值得。无论你在任何可能的滥用上花多少精力来防范,总会有人找到办法绕过去。采取措施阻止一些意外和偶然的误用就已经足够了。如果最后展示的简单代码片段能够满足你的需要,就用它好了,不管是单例还是 Borg。要记住:在能正确工作的前提下做最简单的事。在少数情况下,这种简单的用法也可能无法工作,那么你就需要考虑得多一些了。

单例模式(在前面6.15 节介绍过)仅仅是为了保证某个类最多只能有一个实例。根据我的经验,单例模式通常并不是它试图解决的问题的最佳方案,而且它还带来了和不同的对象模式有关的各种问题。一种典型的做法是我们可以允许多个实例的创建,但这些实例共享状态。我们需要关心身份吗?我们只关心状态(和行为)。这种可选的模式基于状态共享,正是为了解决单例模式试图解决的问题,这种方式也被称为Monostate。顺便说一句,我喜欢称单例模式为“Highlander”,因为实例是唯一的。

在 Python 中,可以用很多方法实现 Monostate,但 Borg 准模式常常是最好的。简洁是Borg 的最大的优点。由于任何实例的__dict__都可以被重新绑定,Borg在它的__new__中将它的每个实例的__dict__,重新绑定到一个类属性字典。现在,对一个实例属性的引用或者绑定都将立刻影响到所有的实例。感谢 David Ascher 为这个模式所建议的名字 Borg。Borg 也许只能说是准模式,因为在它第一次公开的时候,没人听说过关于它的实际使用的案例(当然现在有一些案例了):两个或者更多的应用案例被认为是成为一种设计模式的前提条件之一。更多的细节讨论请参看 http://www.aleax.it/Sep.html.

Robert Martin关于单例和Monostate有一篇很好的文章,请参看http://wwwobjectmentor.com/resources/articles/SingletonAndMonostate.pdf,请注意,绝大多数 Martin提到的关于 Monostate 的劣势都可以归于他考虑过的语言的限制,如 C++和 Java,但这些劣势在 Python的 Borg中都消失了。比如,Martin 指出,Monostate 的第一个主要的缺点是“一个非 Monostate 的类无法通过派生转化为 Monostate 类”,但对于 Borg,通过多继承完成转化的难度可以说是微不足道。
Borg 的一些零碎
在 Borg的操作中,getattr__和__setattr__特殊方法并未被涉及。为此,可以在你的子类中独立地定义它们,以满足你的各种需求,或者也可以放任其不管。这两种方式都不是问题,因为在重新绑定实例的__dict__属性时,Python 并不会调用__setattr

Borg 对于那种把所有或者部分状态保存在__dict__之外的类并不太适用。因此,在 Borg的子类中,避免定义__slots__以优化内存占用并没有什么意义,虽然它是为那些有很多实例的类设计的,但 Borg 的子类会只拥有一个实例。而且,不要从内建类型如 list或 dict 派生,你的 Borg 子类应该进行一些封装工作并自动委托,如前面 6.5 节所示(我把后者称为“DeleBorg”,读者可以在 http://www,aleax.it/Sep.html 读到文章。)

Borg 就是单例这种说法是很荒谬的,就好比说门廊是雨伞一样。从设计模式的角度看它们有着相似的目的(让可以雨中漫步又不至于淋湿),解决相似的问题,但是它们本质上是用不同的方式实现的,它们不是同一种模式的实例。如果说有什么相似之处,我们前面已经提到,Borg和作为单例的一种替换方案的Monostate有一些相似。不过Monostate 是一种模式,而 Borg还不完全是;另外,不用成为 Borg,Python 的 Monostate也可以独立存在。我们可以说Borg是一种惯用法,可以比较容易和高效地在Python中实现 Monostate。

由于某些对我来说完全无法理解的原因,人们总是把 Borg 和单例的问题与其他一些独立的问题——如访问控制,尤其是多线程的访问——混在一起。如果需要控制对一个对象的访问,无论这个对象的类有一个实例还是二十个实例,也无论这些实例在共享或不共享状态,你要做的事情都是一样的。有一种能够高效解决问题的方法,被称为分而治之(divide and conquer),能够将问题分解成不同方面的子问题,从而极大地简化了求解难度。而这种把各方面的问题聚在一起从而成功地增加了问题的难度的方法也许可以被称为聚而苦之(unite and suffer)。

相关文章:

  • 系统思考与第一性原理
  • XCTF-pwn(二)
  • 从 Eclipse Papyrus / XText 转向.NET —— SCADE MBD技术的演化
  • MATLAB绘制局部放大图
  • 环境搭建:开启 Django 开发之旅
  • C++11新特性_标准库_正则表达式库
  • 如何理解 MCP 和 A2A 的区别?|AI系统架构科普
  • AI算法可视化:如何用Matplotlib与Seaborn解释模型?
  • 读懂 Vue3 路由:从入门到实战
  • maven install时报错:【无效的目标发行版: 17】
  • MIT XV6 - 1.2 Lab: Xv6 and Unix utilities - pingpong
  • 每日一题洛谷P8635 [蓝桥杯 2016 省 AB] 四平方和c++
  • 移动端开发中设备、分辨率、浏览器兼容性问题
  • ICCV2021 | 重新思考并改进视觉 Transformer 的相对位置编码
  • 专题二十二:DHCP协议
  • 使用PyMongo连接MongoDB的基本操作
  • 4.2 math模块
  • 力扣面试150题--分隔链表
  • 【第21节 常见攻击】
  • 西游记4:从弼马温到齐天大圣;太白金星的计划;
  • 我的诗歌阅读史
  • 陈逸飞《黄河颂》人物造型与借鉴影像意义
  • 澎湃读报丨解放日报8个版聚焦:牢记嘱托,砥砺奋进
  • 解放日报:让算力像“水电煤”赋能千行百业
  • 铁路迎来节前出行高峰,今日全国铁路预计发送旅客1870万人次
  • 李乐成任工业和信息化部部长