包接口的以下操作很有用:知道一个包是否为空,用一次操作就清空一个包,判断一个给定的项是否在包中,以及查看包中的每一项而不用清空包等等。
用户的包操作 | Bag类中的方法 |
---|---|
b = |
__init__(self,sourceCollection = None) |
b.isEmpty() | isEmpty(self) |
len(b) | __len__(self) |
str(b) | __str__(self) |
item in b | __contains__(self,item); 如果包含了__iter__,就不需要该方法 |
b1 + b2 | __add__(self,other) |
b == anyObject | __eq__(self,other) |
b.clear() | clear(self) |
b.add(item) | add(self,item) |
b.remove(item) | remove(self,item) |
由于这是一个基于数组的实现,所以ArrayBag类型的每一个对象,都包含了该包中的一个数组。这个数组可以是已经实现的Array类的一个实例,也可以是另一个基于数组的集合(例如Python的list类型)。
"""
File: arrays.py
An Array is a restricted list whose clients can use
only [], len, iter, and str.
To instantiate, use
= array(, )
The fill value is None by default.
"""
class Array(object):
"""Represents an array."""
def __init__(self, capacity, fillValue = None):
"""Capacity is the static size of the array.
fillValue is placed at each position."""
self._items = list()
for count in range(capacity):
self._items.append(fillValue)
def __len__(self):
"""-> The capacity of the array."""
return len(self._items)
def __str__(self):
"""-> The string representation of the array."""
return str(self._items)
def __iter__(self):
"""Supports iteration over a view of an array."""
return iter(self._items)
def __getitem__(self, index):
"""Subscript operator for access at index."""
return self._items[index]
def __setitem__(self, index, newItem):
"""Subscript operator for replacement at index."""
self._items[index] = newItem
__init__方法负责设置集合的初始状态。该方法以一个初始的、默认的容量(DEFAULT_CAPACITY)创建了一个数组,并且将这个数组赋给一个名为self._items的实例变量。由于包的逻辑大小可能和数组的容量有所不同,每个ArrayBag对象必须在一个单独的实例变量中记录其逻辑大小。因此,__init__方法将这个名为self._size的变量初始设置为0。
在初始化了两个实例变量之后,__init__方法必须处理一种可能发生的情况,即调用者提供了源集合参数,我们必须把源集合的所有数据添加到ArrayBag对象中。
from arrays import Array
class ArrayBag(object):
"""An array-base bag implementation."""
# Class variable
DEFAULT_CAPACITY = 30 #注意:这里设置10的话,之后testbench的b+b会导致超出范围
# Constructor
def __init__(self,sourceCollection = None):
"""Sets the initial state of self, which includes the contents of sourceCollection, if it's present."""
self._items = Array(ArrayBag.DEFAULT_CAPACITY)
self._size = 0
if sourceCollection:
for item in sourceCollection:
self.add(item)
这个接口中最简单的方法是isEmpty、__len__和clear。如果暂时忽略数组变满的问题,add方法也很简单。
# Accessor methods 访问方法
def isEmpty(self):
"""Return True if len(self) == 0, or False otherwise."""
return len(self) == 0
def __len__(self):
"""Returns the number of items in self."""
return self._size
# Mutator methods 改变对象属性方法
def clear(self):
"""Makes self become empty."""
self._size = 0
self._items = Array(ArrayBag.DEFAULT_CAPACITY)
def add(self,item):
"""Adds item to self."""
# check array memory here and increase it if necessary
self._items[len(self)] = item
self._size += 1
迭代器
当Python出现for循环被用于一个可迭代的对象的时候,它会运行对象的__iter__方法。根据前面的Array类的__iter__方法,你会发现它只是在底层的列表对象上调用iter函数,并且返回结果。然而,这可能会导致一个很严重的错误,数组可能并不是填满的,但是其迭代器会访问所有的位置,包括那些包含了垃圾值的位置。
为了解决这个问题,新的__iter__方法维护了一个游标,当游标达到了包的长度的时候,__iter__方法的while循环终止。
def __iter__(self):
"""Supports iteration over a view of self."""
cursor = 0
while cursor < len(self):
yield self._items[cursor]
cursor += 1
以下是使用迭代器的方法
__add__方法将两个集合连接起来,__str__方法使用map和join操作来构建字符串,__eq__方法判断两个对象是否相同。这里的每一个方法,都依赖于包对象是可迭代的这一事实。
def __str__(self):
"""Returns the string representation of self."""
return "{" + ", ".join(map(str,self)) + "}"
def __add__(self,other):
"""Returns a new bag containing the contents of self and other."""
result = ArrayBag(self)
for item in other:
result.add(item)
return result
def __eq__(self,other):
"""Returns True if self equals other, or False otherwise."""
if self is other:
return True
if type(self) != type(other) or len(self) != len(other):
return False
for item in self:
if not item in other:
return False
return True
in运算符和__contains__方法
当Python识别到集合中使用in运算符的时候,它会运行集合类中的__contains__方法。然而,如果这个类的编写者没有包含这个方法,Python会自动生成一个默认的方法,这个方法在self上使用for循环,针对目标项执行一次简单的顺序搜索,由于对包的搜索的平均性能可能不比线性好,所以我们这里就用__contains__的默认实现。
remove方法
首先检查先验条件,如果不满足就返回一个异常。然后,搜索底层数组以查找目标,最后,将数组中的项向左移动,以填补删除的那项留下的空间,将包的大小减小1。如果有必要的话,还要调整数组的大小(节约空间)。
def remove(self,item):
"""Precondition: item is in self.
Raises: KeyError if item is not in self.
Postcondition: item is removed from self."""
# Check precondition and raise if necessary
if not item in self:
raise KeyError(str(item) + " not in the bag")
# Search for index of target item
targetIndex = 0
for targetItem in self:
if targetItem == item:
break
targetIndex += 1
# Shift items to the left of target up by one position
for i in range(targetIndex,len(self) - 1):
self._items[i] = self._items[i+1]
# Decrement logical size
self._size -= 1
# Check array memory here and decrease it if necessary
看一下ArrayBag类,其中isEmpty、__len__、__add__、__eq__和__str__方法都没有直接访问数组变量,这样在基于链表的实现的时候也不需要做出任何修改,如果一个方法没有访问数组变量,它也就不必访问链表结构变量。所以我们应该尝试将数据结构隐藏在所要实现的对象的方法调用中。
"""
File: node.py
"""
class Node(object):
"""Represents a singly linked node."""
def __init__(self, data, next = None):
self.data = data
self.next = next
初始化数据结构
这里的两个变量是链表结构和逻辑大小,而不是数组和逻辑大小,为了保持一致性,可以使用和前面相同的变量,然而,self._items现在是外部指针而不是数组。默认容量的类变量被忽略了。
from node import Node
class LinkedBag(object):
"""An link-based bag implementation."""
# Constructor
def __init__(self,sourceCollection = None):
"""Sets the initial state of self, which includes the contents of sourceCollection, if it's present."""
self._items = None
self._size = 0
if sourceCollection:
for item in sourceCollection:
self.add(item)
迭代器
LinkedBag类和ArrayBag类的__iter__方法都使用了基于游标的循环来生成项,不一样的是,游标现在是指向链表结构中节点的一个指针,游标最初设置为外部指针self._items,并且当其变为None的时候停止循环。
def __iter__(self):
"""Supports iteration over a view of self."""
cursor = self._items
while not cursor is None:
yield cursor.data
cursor = cursor.next
clear和add方法
ArrayBag中的add方法利用了以常数时间访问数组的逻辑末尾项的优点,如果需要的话,在方法执行之后它会调整数组的大小。LinkedBag中的add方法也是通过将新的项放置在链表结构的头部,从而利用了常数访问时间的优点。
# Mutator methods
def clear(self):
"""Makes self become empty."""
self._size = 0
self._items = None
def add(self,item):
"""Adds item to self."""
self._items = Node(item,self._items)
self._size += 1
remove方法
类似于ArrayBag中的remove方法,LinkedBag的remove方法必须首先处理先验条件,然后针对目标项进行顺序搜索,当找到了包含目标项的节点的时候,需要考虑以下两种情况:
1.它是位于链表结构的头部的节点,在这种情况下,必须将self._items变量重置为这个节点的next链接。
2.这是第一个节点之后的某个节点。在这种情况下,其之前的节点的next链接必须重置为目标项的节点的next链接。
def remove(self,item):
"""Precondition: item is in self.
Raises: KeyError if item is not in self.
Postcondition: item is removed from self."""
# Check precondition and raise if necessary
if not item in self:
raise KeyError(str(item) + " not in the bag")
# Search for index of target item
# probe will point to the target node, and trailer will point to the one before it, if it exists
probe = self._items
trailer = None
for targetItem in self:
if targetItem == item:
break
trailer = probe
probe = probe.next
# unlock the node to be detected, either the first one or the one thereafter
if probe == self._items:
self._items = self._items.next
else:
trailer.next = probe.next
# decrement logical size
self._size -= 1
无需更改的几种方法
# Accessor methods
def isEmpty(self):
"""Return True if len(self) == 0, or False otherwise."""
return len(self) == 0
def __len__(self):
"""Returns the number of items in self."""
return self._size
def __str__(self):
"""Returns the string representation of self."""
return "{" + ", ".join(map(str,self)) + "}"
def __add__(self,other):
"""Returns a new bag containing the contents of self and other."""
result = LinkedBag(self)
for item in other:
result.add(item)
return result
def __eq__(self,other):
"""Returns True if self equals other, or False otherwise."""
if self is other:
return True
if type(self) != type(other) or len(self) != len(other):
return False
for item in self:
if not item in other:
return False
return True
from ArrayBag import ArrayBag
from LinkedBag import LinkedBag
def test(bagType):
"""Expects a bag type as an argument and runs some tests on objects of that type."""
lyst = [2013,61,1973]
print("The list of items added is:",lyst)
b1 = bagType(lyst)
print("Expect 3:",len(b1))
print("Expect the bag's string:",b1)
print("Expect True:",2013 in b1)
print("Expect False:",2012 in b1)
print("Expect the items on seperate lines:")
for item in b1:
print(item)
b1.clear()
print("Expect {}:",b1)
b1.add(25)
b1.remove(25)
print("Expect {}:",b1)
b1 = bagType(lyst)
b2 = bagType(b1)
print("Expect True:",b1 == b2)
print("Expect False:",b1 is b2)
print("Expect two of each item:",b1 + b2)
for item in lyst:
b1.remove(item)
print("Expect {}:",b1)
print("Expect crash with KeyError:")
b2.remove(99)
if __name__ == '__main__':
#test(ArrayBag)
test(LinkedBag)
注意:在测试程序中,你可以对任何的包类型运行相同的方法(那些位于包接口中的方法)。这是接口的要旨所在:尽管实现可以变化,但是它保持不变。
in和remove两个操作在实现上都需要线性时间,因为它们都加入了一个顺序搜索。ArrayBag上的remove操作,还必须完成在数组中移动数据项的额外工作,但是总的效果不会比线性阶还差。+、str和iter操作是线性的,==操作的运行时间有几种不同的情况,剩下的操作都是常数时间的(其中ArrayBag的add偶尔因为调整数组大小而达到了线性时间水平)。
当ArrayBag中的数组要好于填满一半的情况的时候,它使用的内存比相同逻辑大小的LinkedBag所使用的内存要小。相比于LinkedBag上对应的操作,ArrayBag的添加操作通常要更快一点,但是删除操作则会慢一些。
本篇文章来自于:数据结构(Python语言描述)这本书的第5章.接口、实现和多态。