一、字典与映射
Python的dict
类代表了一种高效的数据结构,这种数据结构建立了唯一的键和其对应值之间的映射(Mapping),这种数据结构在需要对键值对进行存储和查找的场景非常高效。如下面代码显示了如何使用字典统计一篇文章中每个单词出现的次数:
freq = {}
for piece in open(filename).read().lower().split(): # 根据空格分割文章
word = ''.join(c for c in piece if c.isalpha()) # 仅保留字符
if word:
freq[word] = 1 + freq.get(word, 0)
max_word = ''
max_count = 0
for (word, count) in freq.items():
if count > max_count:
max_word = word
max_count = c
print('出现次数最多的单词是:', max_word)
print('该单词的出现次数是:', max_count)
实际上,Java语言中的HashMap
和TreeMap
两个类和Python中的dict
类功能比较类似,其中HashMap
底层使用哈希(也叫散列)技术实现,TreeMap
底层使用树实现。
本文将使用列表实现一个与dict
功能类似的类。需要注意的是,在名称上为了和Python内建的字典区分开来,本文将手动实现的类似功能的类称为映射。
二、基于列表实现映射
1. 映射ADT
为了便于本文讨论,下面首先给出后续实现的映射类所有ADT方法,这些方法和Python的dict
类的方法保持一致,下面是映射类最主要的5个ADT方法,即对于映射类的实例对象M
:
方法名称 | 方法描述 |
---|---|
__getitem__() | 实现该方法后,当M 中存在键k 时,可使用M[k] 语法返回和键k 关联的值v ,否则抛出KeyError 异常。 |
__setitem__() | 实现该方法后,当M 中存在键k 时,可使用M[k] = v 语法替换和键k 原先关联的值v ,否则在M 中插入一条键值对记录。 |
__delitem__() | 实现该方法后,当M 中存在键k 时,可使用del M[k] 语法删除一条键值对记录,否则抛出KeyError 异常。 |
__len__() | 实现该方法后,可使用len(M) 语法返回映射中记录条目数。 |
__iter__() | 实现该方法后,可使用for k in M 语法生成映射所有键的一个迭代。 |
上面5个方法代表了映射对象的最核心功能,即:增、删、改、查。除此之外,下面是映射类其他实用方法,这些方法均可通过使用上述5个方法来实现,即对于映射类的实例对象M
:
方法名称 | 方法描述 |
---|---|
__contains__(k) | 实现该方法后,可以使用k in M 的语法判断映射中是否存在键为k 的键值对。 |
get(k,d=None) | 如果键k 存在映射中则返回M[k] ,否则返回默认值d ,该方法可避免使用M[k] 语法时可能抛出的KeyError 异常。 |
setdefault(k,d) | 如果键k 存在,则返回M[k] ,否则执行M[k] = d 且返回d 。 |
pop(k,d=None) | 从映射中删除和键k 关联的键值对并返回值v ,如果k 不存在映射中则返回默认值d (如果d 为None 则抛出KeyError 异常)。 |
popitem() | 从映射中删除任意键值对且返回(k, v) ,如果映射为空则抛出KeyError 异常。 |
clear() | 清空映射中的所有键值对。 |
keys() | 以集合形式返回映射中所有键。 |
values() | 以集合形式返回映射中所有值。 |
items() | 返回映射中所有(k, v) 形式的记录。 |
update(M2) | 对映射M2 中所有(k, v) ,使用M[k] = v 语法为M 赋值。 |
2. 内建抽象基类MutableMapping
实际上,Python的collections.abc
模块中提供了两个官方用于实现字典的类,即Mapping
和MutableMapping
,其中:
Mapping
类包含了上述ADT中所有的非修改类方法,如:__getitem__()
、keys()
、items()
、values()
等;MutableMapping
类继承了Mapping
后新增了所有修改类方法,如:__setitem__(k, v)
、__delitem__(k)
、popitem()
、clear()
等。
上述两个类存在的重要意义在于,其为用户自定义映射类提供了框架,具体地,在MutableMapping
类中,除__getitem__
、__setitem__
、__delitem__
、__len__
、__iter__
这5个是抽象方法(可理解为Java中抽象类中的抽象方法),其余均为基于这5个抽象方法实现的一般性具体方法。
因此,不管后续使用列表、哈希表还是二叉搜索树实现与dict
功能相同的映射类时,只需要先继承MutableMapping
类,然后在自定义映射类中实现这5个抽象方法即可。
为进一步理解为什么继承MutableMapping
的自定义映射类只要实现这5个抽象方法即可,可以研究本文附录的MutableMapping
源码。例如:
- 对于支持
k in M
语法的__contains__(k)
方法,其在被自定义映射类继承的MutableMapping
中的实现依赖于__getitem__
方法的具体实现:
def __contains__(self, key):
try:
self[key] # 调用__getitem__方法
except KeyError:
return False
else:
return True
- 对于
setdefault(k,d)
方法,其在被自定义映射类继承的MutableMapping
中的实现依赖于__setitem__
方法的具体实现:
def setdefault(self, key, default=None):
try:
return self[key] # 依赖于__getitem__方法的具体实现
except KeyError:
self[key] = default # 依赖于__setitem__方法的具体实现
return default
3. 自定义映射基类MapBase
虽然本文将分别基于列表实现自定义映射类,后续还将使用哈希表来实现,根据处理哈希碰撞时的不同方式又可以得到不同的具体实现类,因此为了提高代码复用度下面首先定义一个映射基类MapBase
。
实际上,定义MapBase
类的另一个原因是为不同的映射具体实现类表示每一个键值对提供一种统一的方式,即通过组合设计模式使用一个嵌套定义且具有key
和value
两个实例属性的_Item
类。
from collections.abc import MutableMapping
class MapBase(MutableMapping):
"""提供用于保存键值对记录类的自定义映射基类"""
class _Item:
__slots__ = 'key', 'value'
def __init__(self, key, value):
self.key = key
self.value = value
def __eq__(self, other):
return self.key == other.key # 使用'=='语法基于键比较两个键值对是否相等
def __ne__(self, other):
return not (self == other) # 使用'!='语法基于键比较两个键值对是否不等
def __lt__(self, other):
return self.key < other.key # 使用'<'语法基于键比较两个键值对
4. 列表实现自定义映射类UnsortedListMap
下面通过继承MapBase
类,且使用列表保存所有键值对的方式来实现第一个具体映射类UnsortedListMap
:
__init__
def __init__(self):
"""创建一个空的映射对象"""
self._table = [] # 映射中的键值对记录保存在列表中
__getitem__
def __getitem__(self, key):
"""返回与键key关联的值value,当键key不存在则抛出KeyError异常"""
for item in self._table:
if key == item.key:
return item.value
raise KeyError('key error: ', repr(key))
__setitem__
def __setitem__(self, key, value):
"""将key-value添加至映射对象中,当存在键值key时将其值替换为value"""
for item in self._table: # 遍历查询映射中是否存在键key
if key == item.key:
item.value = value
return
self._table.append(self._Item(key, value)) # 映射中不存在键key
__delitem__
def __delitem__(self, key):
"""删除键key代表的键值对,当键key不存在则抛出KeyError异常"""
for j in range(len(self._table)): # 遍历查询映射中是否存在键key
if key == self._table[j].key:
self._table.pop(j)
return
raise KeyError('key error: ', repr(key)) # 映射中不存在键key
__len__
def __len__(self):
"""返回映射中键值对数量"""
return len(self._table)
__iter__
def __iter__(self):
"""生成一个映射中所有键的迭代"""
for item in self._table:
yield item.key
三、自定义映射类UnsortedListMap
效率分析
方法名称 | 时间复杂度 |
---|---|
__getitem__() | O ( n ) O(n) O(n) |
__setitem__() | O ( n ) O(n) O(n) |
__delitem__() | O ( n ) O(n) O(n) |
__len__() | O ( 1 ) O(1) O(1) |
__iter__() | O ( n ) O(n) O(n) |
四、自定义映射类UnsortedListMap
代码测试
# map.py
from collections.abc import MutableMapping
class MapBase(MutableMapping):
"""提供用于保存键值对记录类的自定义映射基类"""
class _Item:
__slots__ = 'key', 'value'
def __init__(self, key, value):
self.key = key
self.value = value
def __eq__(self, other):
return self.key == other.key # 使用'=='语法基于键比较两个键值对是否相等
def __ne__(self, other):
return not (self == other) # 使用'!='语法基于键比较两个键值对是否不等
def __lt__(self, other):
return self.key < other.key # 使用'<'语法基于键比较两个键值对
class UnsortedListMap(MapBase):
def __init__(self):
"""创建一个空的映射对象"""
self._table = [] # 映射中的键值对记录保存在列表中
def __getitem__(self, key):
"""返回与键key关联的值value,当键key不存在则抛出KeyError异常"""
for item in self._table:
if key == item.key:
return item.value
raise KeyError('key error: ', repr(key))
def __setitem__(self, key, value):
"""将key-value添加至映射对象中,当存在键值key时将其值替换为value"""
for item in self._table: # 遍历查询映射中是否存在键key
if key == item.key:
item.value = value
return
self._table.append(self._Item(key, value)) # 映射中不存在键key
def __delitem__(self, key):
"""删除键key代表的键值对,当键key不存在则抛出KeyError异常"""
for j in range(len(self._table)): # 遍历查询映射中是否存在键key
if key == self._table[j].key:
self._table.pop(j)
return
raise KeyError('key error: ', repr(key)) # 映射中不存在键key
def __len__(self):
"""返回映射中键值对数量"""
return len(self._table)
def __iter__(self):
"""生成一个映射中所有键的迭代"""
for item in self._table:
yield item.key
def __str__(self):
"""返回映射对象的字符串表示形式"""
return str(dict(self.items()))
if __name__ == '__main__':
m = UnsortedListMap()
print(m) # {}
m['K'] = 2
print(m) # {'K': 2}
m['B'] = 4
print(m) # {'K': 2, 'B': 4}
m['U'] = 2
print(m) # {'K': 2, 'B': 4, 'U': 2}
m['V'] = 8
print(m) # {'K': 2, 'B': 4, 'U': 2, 'V': 8}
m['K'] = 9
print(m) # {'K': 9, 'B': 4, 'U': 2, 'V': 8}
print(m['B']) # 4
print(m.get('F')) # None
print(m.get('F', 5)) # 5
print(m.get('K', 5)) # 9
print(len(m)) # 4
del m['V']
print(m) # {'K': 9, 'B': 4, 'U': 2}
print(m.pop('K')) # 9
print(m) # {'B': 4, 'U': 2}
print(m.setdefault('B', 1)) # 4
print(m.setdefault('A', 1)) # 1
print(m) # {'B': 4, 'U': 2, 'A': 1}
附录一:Mapping
和MutableMapping
源码
1. Mapping
源码
class Mapping(Collection):
__slots__ = ()
"""A Mapping is a generic container for associating key/value
pairs.
This class provides concrete generic implementations of all
methods except for __getitem__, __iter__, and __len__.
"""
@abstractmethod
def __getitem__(self, key):
raise KeyError
def get(self, key, default=None):
'D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.'
try:
return self[key]
except KeyError:
return default
def __contains__(self, key):
try:
self[key]
except KeyError:
return False
else:
return True
def keys(self):
"D.keys() -> a set-like object providing a view on D's keys"
return KeysView(self)
def items(self):
"D.items() -> a set-like object providing a view on D's items"
return ItemsView(self)
def values(self):
"D.values() -> an object providing a view on D's values"
return ValuesView(self)
def __eq__(self, other):
if not isinstance(other, Mapping):
return NotImplemented
return dict(self.items()) == dict(other.items())
2. MutableMapping
源码
class MutableMapping(Mapping):
__slots__ = ()
"""A MutableMapping is a generic container for associating
key/value pairs.
This class provides concrete generic implementations of all
methods except for __getitem__, __setitem__, __delitem__,
__iter__, and __len__.
"""
@abstractmethod
def __setitem__(self, key, value):
raise KeyError
@abstractmethod
def __delitem__(self, key):
raise KeyError
__marker = object()
def pop(self, key, default=__marker):
'''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
If key is not found, d is returned if given, otherwise KeyError is raised.
'''
try:
value = self[key]
except KeyError:
if default is self.__marker:
raise
return default
else:
del self[key]
return value
def popitem(self):
'''D.popitem() -> (k, v), remove and return some (key, value) pair
as a 2-tuple; but raise KeyError if D is empty.
'''
try:
key = next(iter(self))
except StopIteration:
raise KeyError from None
value = self[key]
del self[key]
return key, value
def clear(self):
'D.clear() -> None. Remove all items from D.'
try:
while True:
self.popitem()
except KeyError:
pass
def update(self, other=(), /, **kwds):
''' D.update([E, ]**F) -> None. Update D from mapping/iterable E and F.
If E present and has a .keys() method, does: for k in E: D[k] = E[k]
If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v
In either case, this is followed by: for k, v in F.items(): D[k] = v
'''
if isinstance(other, Mapping):
for key in other:
self[key] = other[key]
elif hasattr(other, "keys"):
for key in other.keys():
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key, value in kwds.items():
self[key] = value
def setdefault(self, key, default=None):
'D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D'
try:
return self[key]
except KeyError:
self[key] = default
return default