IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> 【Python 每日一技】使用 ChainMap 合并多个字典或映射及 ChainMap 源码详解 -> 正文阅读

[开发测试]【Python 每日一技】使用 ChainMap 合并多个字典或映射及 ChainMap 源码详解

1 问题

你有多个字典或映射,你希望可以先将它们在逻辑上合并为一个映射,然后对其进行值的查找或者检查键是否存在。

2. 解决方案

假设你有下列两个字典:

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }

假定你需要执行对两个字典的查找操作,即先查找字典 a ,如果没找到再查找字典 b 。最简单的方式是使用 collections 模块的 ChainMap 类。例如:

>>> from collections import ChainMap
>>> c = ChainMap(a, b)
>>> c
ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
>>> c['x']
1
>>> c['y']
2
>>> c['z']
3

3. 讨论

使用 ChainMap 将多个字典或映射合并得到单个映射的操作是逻辑上的,实际上,使用 ChainMap 创建对象时,其底层实际上创建了一个列表,列表中的每个元素都是“合并”前的字典或映射。除此之外,ChainMap 还重写了字典操作的常见方法,这些方法会扫描列表中的每个字典或映射再做对应查询或修改操作。例如:

>>> len(c)
3
>>> c.keys()
KeysView(ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4}))
>>> list(c.keys())
['y', 'z', 'x']
>>> c.values()
ValuesView(ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4}))
>>> list(c.values())
[2, 3, 1]
>>> c['z']
3

由上述案例可知,如果有多个相同的键,则返回的结果中将仅包含第一个字典或映射中的该键对应的值。因此, c['z'] 返回的结果来自字典 a 而不是字典 b

实际上,对于 ChainMap 对象的修改类操作只会影响其中的第一个字典或映射。例如:

>>> c
ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
>>> c['z'] = 10
c
ChainMap({'x': 1, 'z': 10}, {'y': 2, 'z': 4})
>>> c['w'] = 40
>>> c
ChainMap({'x': 1, 'z': 10, 'w': 40}, {'y': 2, 'z': 4})
>>> a
{'x': 1, 'z': 10, 'w': 40}
>>> del c['x']
>>> c
>>> ChainMap({'z': 10, 'w': 40}, {'y': 2, 'z': 4})
>>> a
{'z': 10, 'w': 40}
>del c['y']
Traceback (most recent call last):
  ...
KeyError: "Key not found in the first mapping: 'y'"

对于本文开头提到的需求,你也可以考虑使用 update() 方法对字典进行合并。例如:

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> merged = dict(b)
>>> merged.update(a)
>>> merged
{'y': 2, 'z': 3, 'x': 1}

上述实现方式也是可行的,只是该方式要么需要你创建一个全新的字典对象或在其中一个字典的基础上进行修改,此时,如果在原始字典上进行修改,则该修改不会反应在合并后的字典上。例如:

>>> a['x'] = 13
>>> merged['x']
1

ChainMap 的实例会使用原始的字典,即其中列表的每个元素引用原始的字典。例如:

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> merged = ChainMap(a, b)
>>> merged['x']
1
>>> a['x'] = 42
>>> merged['x']
42

4. 源码分析

本文最后的附录是 collections.ChainMap 的源码,通过分析源码,我们除了能深入理解 ChainMap 的实现机制外,还能了解一个 ChainMap 对象的其他几个实用的方法,例如:new_child()parents 等。

4.1 __init__

def __init__(self, *maps):
    '''Initialize a ChainMap by setting *maps* to the given mappings.
    If no mappings are provided, a single empty dictionary is used.

    '''
    self.maps = list(maps) or [{}]  # always at least one map

创建 ChainMap 对象时调用的初始化方法支持接收多个字典或映射,然后会将其打包进实例属性 self.maps 中,如果创建对象时未指定任何字典或映射,此时形参 maps 为空元组,那么 self.maps 将会被初始化为 [{}] ,这一点可以通过下列代码来验证:

>>> list(tuple()) or [{}]
[{}]

4.2 __getitem__

def __getitem__(self, key):
    for mapping in self.maps:
        try:
            return mapping[key]  # can't use 'key in mapping' with defaultdict
        except KeyError:
            pass
    return self.__missing__(key)  # support subclasses that define __missing__

该方法用于支持任意的 ChainMap 对象 c 可以按照 c[key] 的语法查找值,根据源码也可以发现, ChainMap 底层的确会挨个查找多个字典或映射,即使有多个字典或映射中都存在某 key ,该方法也仅返回其中第一个字典或映射中 key 对应的值。

此外,当根据 key 在任何字典或映射中都无法查找成功,那么该方法内部会调用 __missing__ 方法,后者会抛出 KeyError 异常。

4.3 get

def get(self, key, default=None):
    return self[key] if key in self else default

该方法用于根据 key 查找值,而且该方法还接受一个默认为 Nonedefault 参数,如果查找失败则返回 default

4.4 __len__

def __len__(self):
    return len(set().union(*self.maps))  # reuses stored hash values if possible

支持通过 len(c) 的语法返回多个字典或映射中的键所组成集合的长度,对于该方法可以通过下列案例进行理解:

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> set().union(a, b)
{'y', 'x', 'z'}

4.5 __iter__

def __iter__(self):
    d = {}
    for mapping in reversed(self.maps):
        d.update(mapping)  # reuses stored hash values if possible
    return iter(d)

支持通过 iter(c) 的语法返回一个迭代器。值得注意的是,该方法内部是先将列表 self.maps 进行反转然后依次调用字典的 update 方法,此举可以保证当 self.maps 中多个字典或映射中都包含某键时,最终通过迭代器得到的该键值对是来自第一个字典或映射中的,这样就保证了 ChainMap 对象的行为一致性。

4.6 __contains__

def __contains__(self, key):
    return any(key in m for m in self.maps)

支持通过 key in c 的语法判断键 key 是否存在于 c 中,其中 any 函数接受可迭代对象作为参数,如果迭代参数时任何一个元素 x 的 bool(x) 返回为 True ,那么该函数就返回为 True

4.7 __bool__

def __bool__(self):
    return any(self.maps)

支持根据 bool(c) 语法判断是否为 True 。需要指出的是,当 c = ChainMap() 时,使用 bool(c) 返回为 False,因为此时 self.maps[{}] ,此时 any 函数接受的可迭代对象就是一个列表,列表中仅有一个空字典 {} ,而 bool({}) 返回 False

4.8 __repr__

@_recursive_repr()
def __repr__(self):
    return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})'

用于返回 ChainMap 对象的无歧义表现形式。

4.9 fromkeys

@classmethod
def fromkeys(cls, iterable, *args):
    'Create a ChainMap with a single dict created from the iterable.'
    return cls(dict.fromkeys(iterable, *args))

用于先根据字典的类方法 dict.fromkeys() 创建一个字典,然后通过该字典创建一个 ChainMap

4.10 new_child

def new_child(self, m=None):  # like Django's Context.push()
    '''New ChainMap with a new map followed by all previous maps.
    If no map is provided, an empty dict is used.
    '''
    if m is None:
        m = {}
    return self.__class__(m, *self.maps)

用于创建一个新的 ChainMap 对象,创建后的对象中,列表 self.maps 的第一个字典或映射由键值对参数 m 指定(如果未指定 m 则默认为空字典 {} ),后续所有字典或映射和之前保持一致。需要注意的是,这里的 *self.maps 表示解包,即传入 __init__ 方法中的参数可以理解为是 [m] + self.maps

4.11 parents

@property
def parents(self):                          # like Django's Context.pop()
    'New ChainMap from maps[1:].'
    return self.__class__(*self.maps[1:])

用于根据 self.maps[1:] 创建一个新的 ChainMap 对象。

4.12 __setitem__

def __setitem__(self, key, value):
    self.maps[0][key] = value

用于根据 c[key] 设置键值对,由源码可知,该操作仅对第一个字典或映射有效。

4.13 __delitem__

def __delitem__(self, key):
    try:
        del self.maps[0][key]
    except KeyError:
        raise KeyError('Key not found in the first mapping: {!r}'.format(key))

用于使用 del c[key] 语法从第一个字典或映射中删除键值对,如删除失败则抛出异常。

4.14 __popitem__

def popitem(self):
    'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
    try:
        return self.maps[0].popitem()
    except KeyError:
        raise KeyError('No keys found in the first mapping.')

用于从第一个字典或映射中弹出一个键值对,如果此时第一个字典或映射为空,则抛出异常。

4.15 pop

def pop(self, key, *args):
    'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
    try:
        return self.maps[0].pop(key, *args)
    except KeyError:
        raise KeyError('Key not found in the first mapping: {!r}'.format(key))

用于从第一个字典或映射中删除 key 并返回对应的值。

4.16 clear

def clear(self):
    'Clear maps[0], leaving maps[1:] intact.'
    self.maps[0].clear()

用于清空第一个字典或映射,不改变其他字典或映射。

5. 附录

########################################################################
###  ChainMap
########################################################################

class ChainMap(_collections_abc.MutableMapping):
    ''' A ChainMap groups multiple dicts (or other mappings) together
    to create a single, updateable view.

    The underlying mappings are stored in a list.  That list is public and can
    be accessed or updated using the *maps* attribute.  There is no other
    state.

    Lookups search the underlying mappings successively until a key is found.
    In contrast, writes, updates, and deletions only operate on the first
    mapping.

    '''

    def __init__(self, *maps):
        '''Initialize a ChainMap by setting *maps* to the given mappings.
        If no mappings are provided, a single empty dictionary is used.

        '''
        self.maps = list(maps) or [{}]  # always at least one map

    def __missing__(self, key):
        raise KeyError(key)

    def __getitem__(self, key):
        for mapping in self.maps:
            try:
                return mapping[key]  # can't use 'key in mapping' with defaultdict
            except KeyError:
                pass
        return self.__missing__(key)  # support subclasses that define __missing__

    def get(self, key, default=None):
        return self[key] if key in self else default

    def __len__(self):
        return len(set().union(*self.maps))  # reuses stored hash values if possible

    def __iter__(self):
        d = {}
        for mapping in reversed(self.maps):
            d.update(mapping)  # reuses stored hash values if possible
        return iter(d)

    def __contains__(self, key):
        return any(key in m for m in self.maps)

    def __bool__(self):
        return any(self.maps)

    @_recursive_repr()
    def __repr__(self):
        return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})'

    @classmethod
    def fromkeys(cls, iterable, *args):
        'Create a ChainMap with a single dict created from the iterable.'
        return cls(dict.fromkeys(iterable, *args))

    def copy(self):
        'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'
        return self.__class__(self.maps[0].copy(), *self.maps[1:])

    __copy__ = copy

    def new_child(self, m=None):  # like Django's Context.push()
        '''New ChainMap with a new map followed by all previous maps.
        If no map is provided, an empty dict is used.
        '''
        if m is None:
            m = {}
        return self.__class__(m, *self.maps)

    @property
    def parents(self):  # like Django's Context.pop()
        'New ChainMap from maps[1:].'
        return self.__class__(*self.maps[1:])

    def __setitem__(self, key, value):
        self.maps[0][key] = value

    def __delitem__(self, key):
        try:
            del self.maps[0][key]
        except KeyError:
            raise KeyError('Key not found in the first mapping: {!r}'.format(key))

    def popitem(self):
        'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
        try:
            return self.maps[0].popitem()
        except KeyError:
            raise KeyError('No keys found in the first mapping.')

    def pop(self, key, *args):
        'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
        try:
            return self.maps[0].pop(key, *args)
        except KeyError:
            raise KeyError('Key not found in the first mapping: {!r}'.format(key))

    def clear(self):
        'Clear maps[0], leaving maps[1:] intact.'
        self.maps[0].clear()
  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2022-03-30 18:55:28  更:2022-03-30 18:56:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 0:12:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码