2018-03-01数据结构和算法(2)
1.6字典中的键映射多个值
一个字典就是一个键对应一个单值的映射。如果你想要一个键映射多个值,那么你就需要将这多个值放到另外的容器中, 比如列表或者集合里面。比如,你可以像下面这样构造这样的字典:
d = { 'a' : [1, 2, 3], 'b' : [4, 5] } e = { 'a' : {1, 2, 3}, 'b' : {4, 5} }
选择使用列表还是集合取决于你的实际需求。如果你想保持元素的插入顺序就应该使用列表, 如果想去掉重复元素就使用集合(并且不关心元素的顺序问题)。
你可以很方便的使用 collections
模块中的 defaultdict
来构造这样的字典。 defaultdict
的一个特征是它会自动初始化每个 key
刚开始对应的值,所以你只需要关注添加元素操作了。比如:
from collections import defaultdict d = defaultdict(list) d['a'].append(1) d['a'].append(2) d['b'].append(4) d = defaultdict(set) d['a'].add(1) d['a'].add(2) d['b'].add(4)
需要注意的是, defaultdict
会自动为将要访问的键(就算目前字典中并不存在这样的键)创建映射实体。 如果你并不需要这样的特性,你可以在一个普通的字典上使用 setdefault()
方法来代替。比如:
d = {} # 一个普通的字典 d.setdefault('a', []).append(1) d.setdefault('a', []).append(2) d.setdefault('b', []).append(4)
可以这样初始化:
d = {} for key, value in pairs: if key not in d: d[key] = [] d[key].append(value) #或者这种方式 d = defaultdict(list) for key, value in pairs: d[key].append(value)
1.7字典排序
为了能控制一个字典中元素的顺序,你可以使用 collections
模块中的 OrderedDict
类。 在迭代操作的时候它会保持元素被插入时的顺序,示例如下:
from collections import OrderedDict d = OrderedDict() d['foo'] = 1 d['bar'] = 2 d['spam'] = 3 d['grok'] = 4 # Outputs "foo 1", "bar 2", "spam 3", "grok 4" for key in d: print(key, d[key])
当你想要构建一个将来需要序列化或编码成其他格式的映射的时候, OrderedDict
是非常有用的。 比如,你想精确控制以 JSON 编码后字段的顺序,你可以先使用 OrderedDict
来构建这样的数据:
>>> import json >>> json.dumps(d) '{"foo": 1, "bar": 2, "spam": 3, "grok": 4}' >>>
OrderedDict
内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候, 它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
需要注意的是,一个 OrderedDict
的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。 所以如果你要构建一个需要大量 OrderedDict
实例的数据结构的时候(比如读取 100,000 行 CSV 数据到一个 OrderedDict
列表中去), 那么你就得仔细权衡一下是否使用 OrderedDict
带来的好处要大过额外内存消耗的影响。
1.8字典运算
考虑下面的股票名和价格映射字典:
prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 }
为了对字典值执行计算操作,通常需要使用 zip()
函数先将键和值反转过来。 比如,下面是查找最小和最大股票价格和股票值的代码:
min_price = min(zip(prices.values(), prices.keys())) # min_price is (10.75, 'FB') max_price = max(zip(prices.values(), prices.keys())) # max_price is (612.78, 'AAPL')
类似的,可以使用 zip()
和 sorted()
函数来排列字典数据:
prices_sorted = sorted(zip(prices.values(), prices.keys())) # prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'), # (45.23, 'ACME'), (205.55, 'IBM'), # (612.78, 'AAPL')]
执行这些计算的时候,需要注意的是 zip()
函数创建的是一个只能访问一次的迭代器。 比如,下面的代码就会产生错误:
prices_and_names = zip(prices.values(), prices.keys()) print(min(prices_and_names)) # OK print(max(prices_and_names)) # ValueError: max() arg is an empty sequence
如果你在一个字典上执行普通的数学运算,你会发现它们仅仅作用于键,而不是值。比如:
min(prices) # Returns 'AAPL' max(prices) # Returns 'IBM'
这个结果并不是你想要的,因为你想要在字典的值集合上执行这些计算。 或许你会尝试着使用字典的 values()
方法来解决这个问题:
min(prices.values()) # Returns 10.75 max(prices.values()) # Returns 612.78
不幸的是,通常这个结果同样也不是你想要的。 你可能还想要知道对应的键的信息(比如那种股票价格是最低的?)。
你可以在 min()
和 max()
函数中提供 key
函数参数来获取最小值或最大值对应的键的信息。比如:
min(prices, key=lambda k: prices[k]) # Returns 'FB' max(prices, key=lambda k: prices[k]) # Returns 'AAPL'
但是,如果还想要得到最小值,你又得执行一次查找操作。比如:
min_value = prices[min(prices, key=lambda k: prices[k])]
前面的 zip()
函数方案通过将字典”反转”为 (值,键) 元组序列来解决了上述问题。 当比较两个元组的时候,值会先进行比较,然后才是键。 这样的话你就能通过一条简单的语句就能很轻松的实现在字典上的求最值和排序操作了.
需要注意的是在计算操作中使用到了 (值,键) 对。当多个实体拥有相同的值的时候,键会决定返回结果。 比如,在执行 min()
和 max()
操作的时候,如果恰巧最小或最大值有重复的,那么拥有最小或最大键的实体会返回:
>>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 } >>> min(zip(prices.values(), prices.keys())) (45.23, 'AAA') >>> max(zip(prices.values(), prices.keys())) (45.23, 'ZZZ') >>>
总结一下:一般会根据键返回min()或者max(),使用zip()则可以返回值键对
1.9查找两字典相同点
考虑下面两个字典:
a = { 'x' : 1, 'y' : 2, 'z' : 3 } b = { 'w' : 10, 'x' : 11, 'y' : 2 }
为了寻找两个字典的相同点,可以简单的在两字典的 keys()
或者 items()
方法返回结果上执行集合操作。比如:
# Find keys in common a.keys() & b.keys() # { 'x', 'y' } # Find keys in a that are not in b a.keys() - b.keys() # { 'z' } # Find (key,value) pairs in common a.items() & b.items() # { ('y', 2) }
这些操作也可以用于修改或者过滤字典元素。 比如,假如你想以现有字典构造一个排除几个指定键的新字典。 下面利用字典推导来实现这样的需求:
# Make a new dictionary with certain keys removed c = {key:a[key] for key in a.keys() - {'z', 'w'}} # c is {'x': 1, 'y': 2}
一个字典就是一个键集合与值集合的映射关系。 字典的 keys()
方法返回一个展现键集合的键视图对象。 键视图的一个很少被了解的特性就是它们也支持集合操作,比如集合并、交、差运算。 所以,如果你想对集合的键执行一些普通的集合操作,可以直接使用键视图对象而不用先将它们转换成一个 set。
字典的 items()
方法返回一个包含 (键,值) 对的元素视图对象。 这个对象同样也支持集合操作,并且可以被用来查找两个字典有哪些相同的键值对。
尽管字典的 values()
方法也是类似,但是它并不支持这里介绍的集合操作。 某种程度上是因为值视图不能保证所有的值互不相同,这样会导致某些集合操作会出现问题。 不过,如果你硬要在值上面执行这些集合操作的话,你可以先将值集合转换成 set,然后再执行集合运算就行了。
1.10删除序列相同的元素并保持顺序
如果序列上的值都是 hashable
类型,那么可以很简单的利用集合或者生成器来解决这个问题。比如:
def dedupe(items): seen = set() for item in items: if item not in seen: yield item seen.add(item)
这个方法仅仅在序列中元素为 hashable
的时候才管用。 如果你想消除元素不可哈希(比如 dict
类型)的序列中重复元素的话,你需要将上述代码稍微改变一下,就像这样:
def dedupe(items, key=None): seen = set() for item in items: val = item if key is None else key(item) if val not in seen: yield item seen.add(val)
这里的key参数指定了一个函数,将序列元素转换成 hashable
类型。下面是它的用法示例:
>>> a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}] >>> list(dedupe(a, key=lambda d: (d['x'],d['y']))) [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}] >>> list(dedupe(a, key=lambda d: d['x'])) [{'x': 1, 'y': 2}, {'x': 2, 'y': 4}] >>>
如果你想基于单个字段、属性或者某个更大的数据结构来消除重复元素,第二种方案同样可以胜任。
如果你仅仅就是想消除重复元素,通常可以简单的构造一个集合。比如:
>>> a [1, 5, 2, 1, 9, 1, 5, 10] >>> set(a) {1, 2, 10, 5, 9} >>>
在本节中我们使用了生成器函数让我们的函数更加通用,不仅仅是局限于列表处理。 比如,如果如果你想读取一个文件,消除重复行,你可以很容易像这样做:
with open(somefile,'r') as f: for line in dedupe(f): ...
上述key函数参数模仿了 sorted()
, min()
和 max()
等内置函数的相似功能.