关于Python:“frozen dict”是什么?

What would a “frozen dict” be?

  • 冻结集是冻结集。
  • 冻结的列表可以是元组。
  • 冷冻口述是什么?不可变的、可散列的dict。

我想它可能有点像collections.namedtuple,但那更像是一个冻结的密钥听写(半冻结听写)。不是吗?

"frozendict"应该是冻结字典,应该有keysvaluesget等,支持infor等。


python没有内置的frozendict类型。事实证明,这种方法不太常用(尽管它可能比frozenset更常用)。

想要使用这种类型的最常见原因是当memoizing函数调用具有未知参数的函数时。存储与dict(其中值是hashable)相当的hashable的最常见解决方案类似于tuple(sorted(kwargs.iteritems()))

这取决于排序是否有点疯狂。python不能肯定地保证排序会在这里产生一些合理的结果。(但它不能承诺太多其他事情,所以不要太费吹灰之力。)

你可以很容易地制作出一种类似于dict的包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import collections

class FrozenDict(collections.Mapping):
   """Don't forget the docstrings!!"""

    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)
        self._hash = None

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __getitem__(self, key):
        return self._d[key]

    def __hash__(self):
        # It would have been simpler and maybe more obvious to
        # use hash(tuple(sorted(self._d.iteritems()))) from this discussion
        # so far, but this solution is O(n). I don't know what kind of
        # n we are going to run into, but sometimes it's hard to resist the
        # urge to optimize when it will gain improved algorithmic performance.
        if self._hash is None:
            self._hash = 0
            for pair in self.iteritems():
                self._hash ^= hash(pair)
        return self._hash

它应该很管用:

1
2
3
4
5
6
7
8
9
10
11
>>> x = FrozenDict(a=1, b=2)
>>> y = FrozenDict(a=1, b=2)
>>> x is y
False
>>> x == y
True
>>> x == {'a': 1, 'b': 2}
True
>>> d = {x: 'foo'}
>>> d[y]
'foo'


奇怪的是,尽管我们在Python中使用了很少有用的frozenset,但仍然没有冻结的映射。这一想法在PEP416中被否决了。

所以python 2的解决方案是:

1
2
def foo(config={'a': 1}):
    ...

似乎还是有点跛脚:

1
2
3
4
def foo(config=None):
    if config is None:
        config = default_config = {'a': 1}
    ...

在python3中,您可以选择:

1
2
3
4
5
6
7
from types import MappingProxyType

default_config = {'a': 1}
DEFAULTS = MappingProxyType(default_config)

def foo(config=DEFAULTS):
    ...

现在,可以动态更新默认配置,但在希望它不可变的地方,可以通过传递代理来保持不变。

因此,default_config中的更改将按预期更新DEFAULTS,但不能写入映射代理对象本身。

诚然,它与"不变的、可散列的dict"不是完全相同的东西——但是它是一个不错的替代品,因为我们可能需要一个frozendict。


假设字典的键和值本身是不可变的(例如字符串),那么:

1
2
3
4
5
6
>>> d
{'forever': 'atones', 'minks': 'cards', 'overhands': 'warranted',
 'hardhearted': 'tartly', 'gradations': 'snorkeled'}
>>> t = tuple((k, d[k]) for k in sorted(d.keys()))
>>> hash(t)
1524953596


这是我一直使用的代码。我把冻结集分成亚类。其优点如下。

  • 这是一个真正不变的对象。不依赖未来用户和开发人员的良好行为。
  • 很容易在普通字典和冻结字典之间来回转换。frozendict(orig_dict)->冻结字典。dict(冻结听写)->常规听写。
  • 2015年1月21日更新:我在2014年发布的原始代码使用for循环查找匹配的密钥。那真是太慢了。现在,我已经构建了一个利用Frozenset散列特性的实现。键值对存储在专用容器中,其中__hash____eq__函数仅基于键。与2014年8月我在这里发布的代码不同,这个代码也经过了正式的单元测试。

    MIT样式许可证。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    if 3 / 2 == 1:
        version = 2
    elif 3 / 2 == 1.5:
        version = 3

    def col(i):
        ''' For binding named attributes to spots inside subclasses of tuple.'''
        g = tuple.__getitem__
        @property
        def _col(self):
            return g(self,i)
        return _col

    class Item(tuple):
        ''' Designed for storing key-value pairs inside
            a FrozenDict, which itself is a subclass of frozenset.
            The __hash__ is overloaded to return the hash of only the key.
            __eq__ is overloaded so that normally it only checks whether the Item's
            key is equal to the other object, HOWEVER, if the other object itself
            is an instance of Item, it checks BOTH the key and value for equality.

            WARNING: Do not use this class for any purpose other than to contain
            key value pairs inside FrozenDict!!!!

            The __eq__ operator is overloaded in such a way that it violates a
            fundamental property of mathematics. That property, which says that
            a == b and b == c implies a == c, does not hold for this object.
            Here's a demonstration:
                [in]  >>> x = Item(('a',4))
                [in]  >>> y = Item(('a',5))
                [in]  >>> hash('a')
                [out] >>> 194817700
                [in]  >>> hash(x)
                [out] >>> 194817700
                [in]  >>> hash(y)
                [out] >>> 194817700
                [in]  >>> 'a' == x
                [out] >>> True
                [in]  >>> 'a' == y
                [out] >>> True
                [in]  >>> x == y
                [out] >>> False
        '''


        __slots__ = ()
        key, value = col(0), col(1)
        def __hash__(self):
            return hash(self.key)
        def __eq__(self, other):
            if isinstance(other, Item):
                return tuple.__eq__(self, other)
            return self.key == other
        def __ne__(self, other):
            return not self.__eq__(other)
        def __str__(self):
            return '%r: %r' % self
        def __repr__(self):
            return 'Item((%r, %r))' % self

    class FrozenDict(frozenset):
        ''' Behaves in most ways like a regular dictionary, except that it's immutable.
            It differs from other implementations because it doesn't subclass"dict".
            Instead it subclasses"frozenset" which guarantees immutability.
            FrozenDict instances are created with the same arguments used to initialize
            regular dictionaries, and has all the same methods.
                [in]  >>> f = FrozenDict(x=3,y=4,z=5)
                [in]  >>> f['x']
                [out] >>> 3
                [in]  >>> f['a'] = 0
                [out] >>> TypeError: 'FrozenDict' object does not support item assignment

            FrozenDict can accept un-hashable values, but FrozenDict is only hashable if its values are hashable.
                [in]  >>> f = FrozenDict(x=3,y=4,z=5)
                [in]  >>> hash(f)
                [out] >>> 646626455
                [in]  >>> g = FrozenDict(x=3,y=4,z=[])
                [in]  >>> hash(g)
                [out] >>> TypeError: unhashable type: 'list'

            FrozenDict interacts with dictionary objects as though it were a dict itself.
                [in]  >>> original = dict(x=3,y=4,z=5)
                [in]  >>> frozen = FrozenDict(x=3,y=4,z=5)
                [in]  >>> original == frozen
                [out] >>> True

            FrozenDict supports bi-directional conversions with regular dictionaries.
                [in]  >>> original = {'x': 3, 'y': 4, 'z': 5}
                [in]  >>> FrozenDict(original)
                [out] >>> FrozenDict({'x': 3, 'y': 4, 'z': 5})
                [in]  >>> dict(FrozenDict(original))
                [out] >>> {'x': 3, 'y': 4, 'z': 5}   '''


        __slots__ = ()
        def __new__(cls, orig={}, **kw):
            if kw:
                d = dict(orig, **kw)
                items = map(Item, d.items())
            else:
                try:
                    items = map(Item, orig.items())
                except AttributeError:
                    items = map(Item, orig)
            return frozenset.__new__(cls, items)

        def __repr__(self):
            cls = self.__class__.__name__
            items = frozenset.__iter__(self)
            _repr = ', '.join(map(str,items))
            return '%s({%s})' % (cls, _repr)

        def __getitem__(self, key):
            if key not in self:
                raise KeyError(key)
            diff = self.difference
            item = diff(diff({key}))
            key, value = set(item).pop()
            return value

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

        def __iter__(self):
            items = frozenset.__iter__(self)
            return map(lambda i: i.key, items)

        def keys(self):
            items = frozenset.__iter__(self)
            return map(lambda i: i.key, items)

        def values(self):
            items = frozenset.__iter__(self)
            return map(lambda i: i.value, items)

        def items(self):
            items = frozenset.__iter__(self)
            return map(tuple, items)

        def copy(self):
            cls = self.__class__
            items = frozenset.copy(self)
            dupl = frozenset.__new__(cls, items)
            return dupl

        @classmethod
        def fromkeys(cls, keys, value):
            d = dict.fromkeys(keys,value)
            return cls(d)

        def __hash__(self):
            kv = tuple.__hash__
            items = frozenset.__iter__(self)
            return hash(frozenset(map(kv, items)))

        def __eq__(self, other):
            if not isinstance(other, FrozenDict):
                try:
                    other = FrozenDict(other)
                except Exception:
                    return False
            return frozenset.__eq__(self, other)

        def __ne__(self, other):
            return not self.__eq__(other)


    if version == 2:
        #Here are the Python2 modifications
        class Python2(FrozenDict):
            def __iter__(self):
                items = frozenset.__iter__(self)
                for i in items:
                    yield i.key

            def iterkeys(self):
                items = frozenset.__iter__(self)
                for i in items:
                    yield i.key

            def itervalues(self):
                items = frozenset.__iter__(self)
                for i in items:
                    yield i.value

            def iteritems(self):
                items = frozenset.__iter__(self)
                for i in items:
                    yield (i.key, i.value)

            def has_key(self, key):
                return key in self

            def viewkeys(self):
                return dict(self).viewkeys()

            def viewvalues(self):
                return dict(self).viewvalues()

            def viewitems(self):
                return dict(self).viewitems()

        #If this is Python2, rebuild the class
        #from scratch rather than use a subclass
        py3 = FrozenDict.__dict__
        py3 = {k: py3[k] for k in py3}
        py2 = {}
        py2.update(py3)
        dct = Python2.__dict__
        py2.update({k: dct[k] for k in dct})

        FrozenDict = type('FrozenDict', (frozenset,), py2)


    没有fronzedict,但可以使用MappingProxyType

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >>> from types import MappingProxyType
    >>> foo = MappingProxyType({'a': 1})
    >>> foo
    mappingproxy({'a': 1})
    >>> foo['a'] = 2
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
    TypeError: 'mappingproxy' object does not support item assignment
    >>> foo
    mappingproxy({'a': 1})

    每当我写这样的函数时,我都会想到frozendict:

    1
    2
    3
    def do_something(blah, optional_dict_parm=None):
        if optional_dict_parm is None:
            optional_dict_parm = {}


    您可以使用utilspie包中的frozendict作为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    >>> from utilspie.collectionsutils import frozendict

    >>> my_dict = frozendict({1: 3, 4: 5})
    >>> my_dict  # object of `frozendict` type
    frozendict({1: 3, 4: 5})

    # Hashable
    >>> {my_dict: 4}
    {frozendict({1: 3, 4: 5}): 4}

    # Immutable
    >>> my_dict[1] = 5
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
      File"/Users/mquadri/workspace/utilspie/utilspie/collectionsutils/collections_utils.py", line 44, in __setitem__
        self.__setitem__.__name__, type(self).__name__))
    AttributeError: You can not call '__setitem__()' for 'frozendict' object

    根据文件:

    frozendict(dict_obj): Accepts obj of dict type and returns a hashable and immutable dict


    namedtuple的主要缺点是在使用前需要指定它,因此对于单一用例来说不太方便。

    然而,有一个实际的解决方法可以用来处理许多这样的情况。假设您希望拥有以下dict的不变等价物:

    1
    2
    3
    4
    MY_CONSTANT = {
        'something': 123,
        'something_else': 456
    }

    可以这样模拟:

    1
    2
    3
    from collections import namedtuple

    MY_CONSTANT = namedtuple('MyConstant', 'something something_else')(123, 456)

    甚至可以编写一个辅助函数来实现自动化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def freeze_dict(data):
        from collections import namedtuple
        keys = sorted(data.keys())
        frozen_type = namedtuple(''.join(keys), keys)
        return frozen_type(**data)

    a = {'foo':'bar', 'x':'y'}
    fa = freeze_dict(data)
    assert a['foo'] == fa.foo

    当然,这只适用于扁平的dict,但是实现递归版本并不太困难。


    是的,这是我的第二个答案,但这是一个完全不同的方法。第一个实现是纯Python。这个是赛通的。如果你知道如何使用和编译赛通模块,这就像一本普通字典一样快。大约0.04到0.06微秒来检索单个值。

    这是"冻结的"文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import cython
    from collections import Mapping

    cdef class dict_wrapper:
        cdef object d
        cdef int h

        def __init__(self, *args, **kw):
            self.d = dict(*args, **kw)
            self.h = -1

        def __len__(self):
            return len(self.d)

        def __iter__(self):
            return iter(self.d)

        def __getitem__(self, key):
            return self.d[key]

        def __hash__(self):
            if self.h == -1:
                self.h = hash(frozenset(self.d.iteritems()))
            return self.h

    class FrozenDict(dict_wrapper, Mapping):
        def __repr__(self):
            c = type(self).__name__
            r = ', '.join('%r: %r' % (k,self[k]) for k in self)
            return '%s({%s})' % (c, r)

    __all__ = ['FrozenDict']

    这是文件"setup.py"

    1
    2
    3
    4
    5
    6
    from distutils.core import setup
    from Cython.Build import cythonize

    setup(
        ext_modules = cythonize('frozen_dict.pyx')
    )

    如果安装了Cython,请将上面的两个文件保存到同一目录中。移动到命令行中的那个目录。

    1
    2
    python setup.py build_ext --inplace
    python setup.py install

    你应该完成。


    安装frozendict

    1
    pip install frozendict

    用它!

    1
    2
    3
    4
    from frozendict import frozendict

    def smth(param = frozendict({})):
        pass

    另一种选择是multidict包中的MultiDictProxy类。


    我需要在某一点上访问某个东西的固定密钥,这是一种全球不变的东西,我决定这样做:

    1
    2
    3
    4
    5
    6
    7
    class MyFrozenDict:
        def __getitem__(self, key):
            if key == 'mykey1':
                return 0
            if key == 'mykey2':
                return"another value"
            raise KeyError(key)

    像它一样使用它

    1
    2
    a = MyFrozenDict()
    print(a['mykey1'])

    警告:对于大多数用例,我不建议这样做,因为这会造成相当严重的权衡。


    在缺乏本机语言支持的情况下,您可以自己执行,也可以使用现有的解决方案。幸运的是,Python使扩展它们的基本实现变得非常简单。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class frozen_dict(dict):
        def __setitem__(self, key, value):
            raise Exception('Frozen dictionaries cannot be mutated')

    frozen_dict = frozen_dict({'foo': 'FOO' })
    print(frozen['foo']) # FOO
    frozen['foo'] = 'NEWFOO' # Exception: Frozen dictionaries cannot be mutated

    # OR

    from types import MappingProxyType

    frozen_dict = MappingProxyType({'foo': 'FOO'})
    print(frozen_dict['foo']) # FOO
    frozen_dict['foo'] = 'NEWFOO' # TypeError: 'mappingproxy' object does not support item assignment

    dict的子类

    我在《荒野》(Github)中看到了这种模式,并想提及它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class FrozenDict(dict):
        def __init__(self, *args, **kwargs):
            self._hash = None
            super(FrozenDict, self).__init__(*args, **kwargs)

        def __hash__(self):
            if self._hash is None:
                self._hash = hash(tuple(sorted(self.items())))  # iteritems() on py2
            return self._hash

        def _immutable(self, *args, **kws):
            raise TypeError('cannot change object - object is immutable')

        __setitem__ = _immutable
        __delitem__ = _immutable
        pop = _immutable
        popitem = _immutable
        clear = _immutable
        update = _immutable
        setdefault = _immutable

    示例用法:

    1
    2
    3
    4
    5
    d1 = FrozenDict({'a': 1, 'b': 2})
    d2 = FrozenDict({'a': 1, 'b': 2})
    d1.keys()
    assert isinstance(d1, dict)
    assert len(set([d1, d2])) == 1  # hashable

    赞成的意见

    • 支持get()keys()items()(iteritems()on py2)和dict的所有商品,不明确实施。
    • 在内部使用dict,表示性能(dict在cpython中用C写)
    • 优雅简单,没有黑色魔法
    • isinstance(my_frozen_dict, dict)返回true-尽管python鼓励duck输入许多包使用isinstance(),但这可以节省许多调整和定制

    欺骗

    • 任何子类都可以覆盖它或在内部访问它(在Python中,您不能真正100%地保护某些内容,您应该信任您的用户并提供良好的文档)。
    • 如果你关心速度,你可能想让__hash__快一点。