任务
你需要对一个列表执行很频繁的成员资格检査。而in操作符的 O(n)时间复杂度对性能的影响很大,你也不能将序列转化为一个字典或者集合,因为你还需要保留原序列的元素顺序。
解决方案
假设需要给列表添加一个在该列表中不存在的元素。一个可行的方法是写这样一个函数:
def addUnique(baseList,otherList):
auxDict = dict.fromkeys(baseList)
for item in otherList:
if item not in auxDict:
baseList.append(item)
auxDict[item] = None
如果你的代码只是在 Python 2.4下运行,那么将辅助字典换成辅助集合效果是完全一样的。
讨论
下面给出一个简单(天真?)的方式,看上去相当不错:
def addUnique_simple(baselist,otherList):
for item in otherList:
if item not in baseList:
baseList.append(item)
如果列表很短的话,这个方法倒也没问题。
但是,如果列表不是很短,这个简单的方法会非常慢。当你用if item not in baseList 这样的代码进行检查时,Python只会用一种方式执行in操作:对列表 baselist 的元素进行内部的循环遍历,如果找到一个元素等于item 则返回 True,如果直到循环结束也没有发现相等的元素则返回 False。in 操作的平均执行时间是正比于1en(baseList)的。addUnique simple 执行了len(otherList)次 in操作,因此它消耗的时间正比于这两个列表长度的乘积。
而解决方案给出的 addUnique 函数,首先创建了一个辅助的字典 auxDict,这一步的时间正比于len(baseList)。然后在循环中检査 dict 的成员——这是造成巨大差异的一步,因为检查一个元素是否处于一个dict 中的时间大致是一个常数,而与 dict 中元素的数目没有关系。因此,那个for循环消耗的时间正比于len(otherList),这样,整个函数所需要的时间就正比于这两个列表的长度之和。
对于运行时间的分析还可以挖得更深一点,因为在 addUnique_simple 中 baseList 的长度并不是一个常量,每当找到一个不属于 baseList的元素,baseList的长度就会增加。但这样的分析结果不会与前面的简化版的结果有太大出入。我们可以准备一些用例进行测试。当每个列表中有10个整数且有50%的重叠时,简化版比解决方案给出的函数慢30%,这样的性能下降还可以忽略。若每个列表都有100个整数,而且仍然有 50%的重叠部分,简化版比解决方案的函数慢12倍–这种级别的减速效果就无法忽略了,而且当列表变得更长的时候,情况也变得更糟。
有时,将一个辅助的 dict和序列一起使用并封装成一个对象能提高你的应用程序的性能。但在这个例子中,必须在序列被修改时不断地维护 dict,以保证它总是和序列当前所拥有的元素保持同步。这个维护任务并不是很简单,我们有很多方法来实现同步。下面给出一种“即时”的同步方式,当需要检查某元素,或者字典的内容可能已经无法和列表内容保持同步时,我们就重新构建一个辅助 dict。由于开销很小,下面的类优化了index方法和成员检查部分的代码:
class list_with_aux_dict(list):
def __init__(self,iterable = ()):
list.__init__(self,iterable)
self._dict_ok = False
def _rebuild_dict(self):
self.dict = {}
for i,item in enumerate(self):
if item not in self._dict:
self._dict[item] = i
self._dict_ok = True
def __contains__(self,item):
if not self._dict_ok:
self._rebuild_dict()
return item in self._dict
def index(self,item):
if not self._dict_ok:
self._rebuild_dict()
try: return self,_dict[item]
except KeyError:raise ValueError
def _wrapMutatorMethod(methname):
_method = getattr(list,methname)
def wrapper(self,*args):
#重置字典的OK标志,然后委托给真正的方法
self._dict_ok = False
return method(self,*args)
#只适用于Python 2.4:wrapper.__name__ = _method.__name__
setattr(list_with_aux_dict,methname,wrapper)
for meth in 'setitem delitem setslice delslice iadd'.split():
_wrapMutatorMethod('__%s__'%meth)
for meth in 'append insert pop remove extend'.split():
_wrapMutatorMethod(meth)
del _wrapMethod#删除辅助函数,已经不再需要它了
list_with_aux_dict扩展了list,并将原 list的所有方法仍然委托给它,除了__contains__和 index。所有能够修改 list 的方法都被封装进了一个闭包,该闭包负责重置一个标志以确保辅助字典的有效性。Python的in操作符调用__contains__方法。除非标志被设置,否则 list_with_aux_dict的__contains__方法会重建辅助字典(标志被设置时,重建没有必要),而index方法仍然像原先一样工作。
上述 list_with_aux_dict 类并没有用帮助函数为列表的所有属性方法绑定和安装一个闭包,而是只取所需,我们也可以在 list_with_aux_dict 的主体中写出所有的 def语句来替代 wrapper 方法。但是上述代码有个重要的优点是消除了几余和重复(重复和啰嗦的代码让人生厌,而且容易滋生 bug)。Python 在自省和动态改变方面的能力给你提供了一个选择:可以创建一个 wrapper方法,用一种聪明而简练的方式,或者,如果你想避免使用被人称为黑魔法的类对象的自省和动态改变,也可以写一堆重复嗦的代码。
list_with_aux_dict 的结构很适合通常的使用模式,即对序列的修改操作一般总是集中出现,然后接着又会有一段时间序列无须被修改,但需要检查元素的成员资格。如果参数 baseList 不是一个普通的列表,而是list_with_aux_dict 的一个实例,早先展示的addUnique_simple 函数也不会因此得到任何性能上的提升,因为这个函数会交替地进行成员资格检查和序列修改。因此,类list_with_aux_dict 中过多的辅助字典的重建影响了函数的性能。(除非是针对某个特例,比如oterList 中绝大多数元素都已经在 baseList中出现过了,因此对序列的修改相比于对元素的检查,发生的次数要少得多。)
对这些成员资格的检查所做的优化有个重要的前提,即序列中的值必须是可哈希的(不然的话,它们不能被用来做字典的键或者集合的元素)。举个例子,元组的列表仍适用于本节的解决方案,但对于列表的列表,我们恐怕得另外想办法了。