将全局变量替换为上下文安全变量和服务
项目描述
所以你正在编写一个库,你有一个这样的对象,它不断地出现在参数或属性中,尽管在给定的时间点只有一个这样的对象。你应该使用全局变量还是单例?
我们大多数人都知道我们“不应该”使用全局变量,而有些人知道单例只是另一种全局变量!但有时候,它们都显得非常诱人。它们非常容易创建和使用,尽管它们也是可测试性、可维护性、可配置性和线程安全性……等等的麻烦之源。你几乎可以称之为什么,它都会是全局变量和单例的问题。
编程专家谈论使用“依赖注入”或“控制反转”(IoC)来消除全局变量。而且有众多针对Python的依赖注入框架(包括Zope 3和peak.config)。
但问题是,这些框架通常要求你声明接口、注册服务、创建XML配置文件,并确保应用程序中的每个对象都知道如何查找服务——用一个“全局变量”问题替换另一个!这不仅使事情比必要的更复杂,还通过让你做一些不会为你的应用程序带来任何新好处的工作来打乱你的编程流程。
因此,我们大多数人最终陷入了各种难以接受的选择之间
使用全局变量并就此了结(但会感到内疚并担心将来会因我们的罪行而遭受报复),
尝试使用依赖注入框架,现在支付额外费用以确信将来事情会顺利进行,或者
使用线程局部变量,承担引入可能的线程依赖性的代价,并且仍然没有合理的测试或配置替代实现的方法。此外,线程局部变量实际上不支持异步编程或协作多任务。如果有人想在Twisted中使用你的库,并且需要为每个套接字连接使用私有实例怎么办?
但现在有一个更好的选择。
“上下文”库(peak.context)允许您创建上下文感知的伪单例和伪全局变量,它们易于替换。它们看起来和感觉就像旧式的全局变量和单例,但由于它们可以安全地扩展到线程和任务(以及可替换以进行测试或其他动态上下文),因此您无需担心“以后”会发生什么。
上下文单例甚至比线程局部变量更好,因为它们支持与微线程、协程或Twisted等框架的异步编程。简单的上下文切换API允许您立即交换一个逻辑任务的所有服务和变量,以另一个任务的服务和变量。这通常不可能使用普通的线程局部变量。
同时,“客户端”代码使用上下文感知对象时保持不变:代码只需使用“当前”对象应该使用的对象。
这不是您一开始就想做的吗?
可替换的单例
以下是一个简单的使用 peak.context 实现的“全局”计数器服务的样子
>>> from peak import context >>> class Counter(context.Service): ... value = 0 ... ... def inc(self): ... self.value += 1 ... >>> Counter.value 0 >>> Counter.inc() >>> Counter.value 1
想要使用这个全局计数器的代码只需要调用 Counter.inc() 或访问 Counter.value,它将自动使用当前线程或任务的正确 Counter 实例。想为测试使用一个新的计数器?只需这样做
with Counter.new(): # code that uses the standard count.* API
在 with 块中,任何引用 count 的代码都将使用您提供的新的 Counter 实例。如果需要支持Python 2.4,context 库还包含一个装饰器,可以模拟 with 语句
>>> Counter.value # before using a different counter 1 >>> @context.call_with(Counter.new()) ... def do_it(c): ... print Counter.value 0 >>> Counter.value # The original counter is now in use again 1
@call_with 装饰器比 with 语句要丑陋一些,但效果差不多。您还可以使用旧式的 try-finally 块,或者使用测试的 setUp() 和 tearDown() 方法等类似的前后机制来替换和恢复活动实例。
可插拔服务
想要创建同一服务的替代实现,可以将其插入以替换它?这同样很简单
>>> class DoubleCounter(context.Service): ... context.replaces(Counter) ... value = 0 ... def inc(self): ... self.value += 2
要使用它,只需这样做
with DoubleCounter.new(): # code in this block that calls ``Counter.inc()`` will be incrementing # a ``DoubleCounter`` instance by 2
或者,在Python 2.4中,您可以这样做
>>> @context.call_with(DoubleCounter.new()) ... def do_it(c): ... print Counter.value ... Counter.inc() ... print Counter.value 0 2
当然,一旦不再使用替换项,原始实例就会再次变得活跃
>>> Counter.value 1
所有这些,无需声明或注册接口,无需编写XML或配置文件。然而,如果您 想 使用配置文件来选择全局服务的实现,您仍然可以这样做:设置 Counter <<= DoubleCounter 将当前 Counter 工厂设置为 DoubleCounter,这样您就可以设置配置文件加载器,以设置您想要的任何服务。您甚至可以拍摄整个当前上下文的快照并恢复所有以前的值
with context.empty(): # code to read config file and set up services # code that uses the configured services
此代码不会与调用它的代码共享任何服务;它不仅会得到自己的私有 Counter 实例,还会得到它使用的任何其他 Service 对象的私有实例。(实例在新上下文中按需创建,所以如果您不使用某个特定服务,它永远不会被创建。)尝试用全局或线程局部变量做这件事!
除了这些简单的伪全局对象之外,peak.context 还支持其他类型的上下文敏感性,例如“当前配置”中的“设置”概念以及“当前操作”中的“资源”概念(在操作成功完成或因错误退出时被通知)。这些功能的实现和使用比早期的 peak.config 和 peak.storage 框架中的相应功能简单得多,但提供了等效或更好的功能。
有关更多详细信息,请参阅上下文开发者指南。
待办事项
- 0.7
完成开发者指南!
配置文件
具有状态绑定和 **kw 属性在 __init__ 和 .new() 上更新的组件
- 0.8
状态 __enter__ 应锁定状态到当前线程,不允许从其他线程执行 __exit__ 或 swap() 或 on_exit,以确保它们是线程安全的。
检测值计算循环
资源池/缓存
状态
此软件包正在积极开发中,但并非所有功能都稳定且已文档化。Service 对象按预期工作,支持在较旧版本的 Python 中使用类似于“with”的操作。大多数其他功能尚未以任何实际方式使用(甚至尚未文档化!),因此设计在 0.7a1 真实版本发布之前仍可能发生变化。
(尽管所有包含的代码都经过了测试,但您仍然可以从它们中挖掘技术文档;开发者指南和教程至今仍不完整。)
Contextual 的源代码分布快照每天生成,但您也可以直接从 SVN 中的 开发版本 进行更新。