Python Cookbook-6.17 NuIl对象设计模式的实现
任务
你想减少代码中的条件声明,尤其是针对特殊情况的检查。
解决方案
一种常见的代表“这里什么也没有”的占位符是 None,但我们还可以定义一个类,其行为方式和这种占位符相似,而且效果更好:
class Null(object):'''Null对象总是很可靠地什么也不做'''#可选的优化:确保每个子类只有一个实例#(完全是为了节省内存,功能上没有任何差异)def __new__(cls,*args,**kwargs):if '_inst' not in vars(cls):cls._inst = type.__new__(cls,*args,**kwargs)return cls._instdef __init__(self,*args,**kwars):passdef __call__(self,*args,**kwarqs):return selfdef __repr_(self):return "Null()"def __nonzero__(self):return Falsedef __getattr__(self,name):return selfdef __setattr__(self,name,value):return selfdef __delattr__(self,name):return self
讨论
可以使用 Null 类的一个实例而不是原生的 None。使用这种实例作为占位符,而不是None,就可以在你的代码中避免很多条件声明,而且能够用极少的特殊值检查来实现算法。本节解决方案是Null对象设计模式的一个实现。(参考B.Woolf,Paltern Languageso/Programming中的“The Null Object Pattern”[PLoP 96,1996年9月]。)
本节中的Null类忽略了所有在构建时和调用实例时传入的参数,也忽略了所有的设置和删除属性的操作。任何调用或者访问属性(或者方法,因为Python并不区分两者,一概调用__getattr__)的操作都返回同一个Null实例(即 self——没有什么必要创建一个新实例)。比如,假如你有这样的一个计算:
def compute(x,y):try:lots of computation here to return some appropriate objectexcept SomeError:return None
而你这样用它:
for x in xs:for y in ys:obj = compute(x,y)if obj is not None:obj.somemethod(y,x)
可以将计算修改为:
def compute(x,y):try:lots of computation here to return some appropriate objectexcept SomeError:return Null()
对它的用法可以简化为:
for x in xs:for y in ys:compute(x,y).somemethod(y,x)
其要点是你无须检查 compute 究竟是返回了一个真实的结果还是Null的一个实例:即使是后者,也可以安全无害地调用它的任何方法。下面是另一个更具体的使用例子:
log = err =Null()
if verbose:log = open('/tmp/log','w')err = open('/tmp/err','W')
log.write('blabla')
err.write('blabla error')
很明显可以避免某种代码“污染",如if verbose:这样的防护性代码,总是散布于各处可以直接调用 log.write(‘bla’),无须用以往的表达方式,如if log is not None:log.write(‘bla’)。
在新的对象模型中,对于执行某些操作所需要的特殊方法,Python 并不会对实例调用__getattr__方法(而是检查该实例的类的槽(sot))。需要谨慎地定制 Null 类来满足你的应用对空对象操作的需求,因此需要仔细设计空对象类的特殊方法,不管它们是直接嵌入基类的代码中的,还是以子类化的方式扩展的。举个例子,对于本节解决方案中的 Null,你无法索引 Null 实例,也不能取得它的长度,还无法对它迭代。如果这对于你的应用而言是个问题,可以根据需要增加一些特殊方法(在Null 中直接增加或者从 Null 派生并扩展)并提供适当的实现,比如:
class SegNull(Null):def __len__(self):return 0 def __iter__(self):return iter(())def __getitem__(self,i):return selfdef __delitem__(self,i):return selfdef __setitem__(self,i,v):return self
利用此法也可增加其他操作。
Null 对象的关键设计目标是为我们在 Python 中常用的原始的 None 提供一种智能的替代物(其他语言中用null 或者 null 指针来代表无值或空值)。这种“这里什么也没有”的标记或占位符可以用于多种情况,比如其中一种情况是,一组元素除了其中一个比较特殊其他都是类似的。这种用法经常导致到处都是条件声明,而条件声明的目的常常只是为了将普通的元素和原始的null 值(比如 None)区分开来,Null 对象则能够帮助你避免过多的条件声明。
使用 Null 对象的优势如下:
- 通过提供一个第一等的对象作为原始的 None 值的代替,过多的条件声明得以避免同时还提升了代码的可读性;
- Null对象可以作为那些行为还没有完全实现的对象的占位符;
- Null对象能够以一种多态的方式被任何其他类的实例使用(可能需要为某些特殊方法进行子类化扩展,正如前面提到过的那样);
- Null对象具有很好的可预测性。
Null的一个很大的缺点是它可能会隐藏一些错误。如果一个函数返回 None,而调用者并不期望这样的返回值,调用者极有可能会接着对 None 调用一个它并不支持的方法或者操作,从而引发一个提示异常并回溯。如果返回值是调用者并不期望获得的Null,则问题可能会被掩盖很长一段时间,当最终异常和回溯发生时,重新定位到代码最初的纰漏会更加困难。这个问题严重到影响Null 的实用性了吗?答案是,这仍然是个人选择。如果你的代码在开发中总会有一些适当的单元测试,那么这个问题可能不会发生,而如果你根本就没有单元测试,使用 Null 带来的问题通常也只会是最小的问题。但正如我说的,这是个人的取舍。我喜欢到处用Null,我非常满意它给我带来的生产率的提升。
本节展示的 Null 类使用了“单例”模式(见6.15 节)的一个简单的变体,仅仅只是为了性能优化–说白了,就是为了避免创建很多什么也不做的对象,空耗内存。这个“单例”的实现确保了 Null 的每个子类在实例化的时候都只能有一个实例,这是很关键的。很显然,子类的数量不可能多到吃掉大量的内存,而各个子类之间的区分在语义上也非常重要。