什么是“冻结命令”?


158
  • 冻结集是冻结集。
  • 冻结列表可能是一个元组。
  • 冻结的字典是什么?一个不变的,可哈希的字典。

我猜可能是collections.namedtuple,但是更像是冰冻的字典(半冻结​​的字典)。是不是

A“frozendict”应该是一个冰冻的字典,它应该有keysvaluesget,等,并支持infor等等。

更新:
*它是:https : //www.python.org/dev/peps/pep-0603

Answers:


120

Python没有内置的Frozendict类型。事实证明,这并不是太有用了(尽管它可能仍然比以前有用frozenset)。

想要这种类型的最常见原因是在记忆函数调用具有未知参数的函数时。存储dict的可哈希等效项(值是可哈希的)的最常见解决方案是tuple(sorted(kwargs.iteritems()))

这取决于排序是否有点疯狂。Python无法肯定地承诺排序将在这里产生合理的结果。(但是,它不能承诺其他任何事情,因此请不要流汗过多。)


您可以轻松地制作某种类似于dict的包装器。它可能看起来像

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:
            hash_ = 0
            for pair in self.items():
                hash_ ^= hash(pair)
            self._hash = hash_
        return self._hash

它应该很棒:

>>> 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'

7
我不知道人们会对这种事情担心什么级别的线程安全,但是在这方面,您的__hash__方法可能会有所改善。在计算哈希值时只需使用一个临时变量,并且只有self._hash在获得最终值后才进行设置。这样,另一个线程在第一个线程正在计算时获取哈希将仅执行冗余计算,而不是获取不正确的值。
Jeff DQ

22
@Jeff通常,所有代码都不是线程安全的,因此应将其包装在某些同步结构中,以便安全地使用该代码。同样,您对线程安全的特定概念依赖于对象属性分配的原子性,这远不能保证。
Devin Jeanpierre

9
@Anentropic,那根本不是真的。
Mike Graham

17
请注意:此“ FrozenDict”不一定是冻结的。没有什么可以阻止您将可变列表作为值放置的,在这种情况下,哈希将引发错误。这不一定没有错,但是用户应该注意。另一件事:该哈希算法选择不当,非常容易发生哈希冲突。例如,{'a':'b'}的哈希值与{'b':'a'}相同,而{'a':1,'b':2}的哈希值则与{'a':2,' b':1}。更好的选择是self._hash ^ = hash((key,value))
史蒂夫·伯恩斯

6
如果在不可变对象中添加可变条目,则两种可能的行为是在创建对象时引发错误,或在对对象进行哈希处理时引发错误。元组做后者,frozenset做前者。考虑到所有因素,我绝对认为您是采取后一种方法的明智决定。尽管如此,我认为人们可能会看到FrozenDict和Frozenset具有相似的名称,并得出结论认为它们的行为应相似。因此,我认为有必要就这种差异向人们发出警告。:-)
史蒂夫·伯恩斯

62

奇怪的是,尽管我们很少frozenset在python中有用,但仍然没有冻结的映射。这个想法在PEP 416中被拒绝-添加一个Frozendict内置类型。可以在Python 3.9中重新考虑这个想法,请参阅PEP 603-向collections添加一个Frozenmap类型

因此,python 2解决方案:

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

似乎还是有些la脚:

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

在python3您的选择这个

from types import MappingProxyType

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

def foo(config=DEFAULTS):
    ...

现在,默认配置可以动态更新,但是可以通过传递代理来保持默认配置不变。

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

诚然,这与“不可变,可哈希的字典”不是完全一样的东西,但是考虑到我们可能希望使用“冻结字典”的相同用例,它是一个不错的替代品。


2
是否有任何特殊原因将代理存储在模块变量中?为什么不只是def foo(config=MappingProxyType({'a': 1})):呢?您的示例仍然允许通过进行全局修改default_config
jpmc26 '18 / 05/26

另外,我怀疑双重任务config = default_config = {'a': 1}是错字。
jpmc26 '18

21

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

>>> 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

这是字典的良好,规范的,不变的表示形式(除非疯狂的比较行为使排序混乱)。
Mike Graham

6
@devin:完全同意,但我以我的帖子为例,通常有更好的方法。
msw 2010年

14
更好的方法是将其置于冻结状态,该状态不需要键或值具有定义的一致顺序。
asmeurer 2012年

7
唯一的问题是:您不再有映射。那就是将冻结的字典放在首位的全部意义。
疯狂物理学家

2
回到字典时,此方法非常好。简单地dict(t)
codythecoder

12

没有fronzedict,但是您可以使用MappingProxyTypePython 3.3中添加到标准库中的:

>>> 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})

请注意:TypeError: can't pickle mappingproxy objects
Radu

我喜欢这个主意。我将尝试一下。
道格

10

这是我一直在使用的代码。我把Frozenset归为一类。其优点如下。

  1. 这是一个真正的不变的对象。不依赖未来用户和开发人员的良好行为。
  2. 在常规字典和冻结字典之间来回转换很容易。FrozenDict(orig_dict)->冻结的字典。dict(frozen_dict)->常规字典

2015年1月21日更新:我在2014年发布的原始代码使用了for循环来查找匹配的键。那太慢了。现在,我整理了一个利用Frozenset的哈希功能的实现。键值对存储在特殊的容器中,其中__hash__和和__eq__函数仅基于键。与我在2014年8月发布的代码不同,该代码也已经过正式的单元测试。

MIT样式的许可证。

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)

1
请注意,您还通过在此处发布CC BY-SA 3.0对其进行了许可。至少那是普遍的观点。我想这是您首次注册时同意某些条款和条件的法律依据。
Evgeni Sergeev

1
我不知所措,试图思考一种无需命令即可查找密钥哈希的方法。重新定义的哈希为Item键的哈希是一个很好的技巧!
clacke

不幸的是,的运行时间diff(diff({key}))在FrozenDict的大小上仍然是线性的,而常规dict的访问时间在一般情况下是恒定的。
丹尼斯,

6

每当我编写这样的函数时,我都会想起Frozendict:

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

6
每次看到这样的评论时,我都确定会在某个地方搞砸,并将{}设置为默认值,然后回头查看我最近编写的代码。
瑞安·希伯特

1
是的,迟早每个人都会碰到这个讨厌的陷阱。
Mark Visser 2012年

8
更容易制定:optional_dict_parm = optional_dict_parm or {}
Emmanuel 2012年

2
在这种情况下,您可以将其用作参数的默认值。types.MappingProxyType({})
GingerPlusPlus

@GingerPlusPlus您能以答案的形式写下来吗?
jonrsharpe

5

您可以将frozendictfrom utilspie包用作:

>>> 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):接受dict类型的obj并返回一个可哈希且不可变的 dict



3

是的,这是我的第二个答案,但这是一种完全不同的方法。第一个实现是在纯python中实现的。这是在Cython中。如果您知道如何使用和编译Cython模块,这与常规词典一样快。大约.04到.06毫秒,以检索单个值。

这是文件“ frozen_dict.pyx”

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”

from distutils.core import setup
from Cython.Build import cythonize

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

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

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

并且应该完成。


3

其主要缺点namedtuple是在使用前需要先指定它,因此对于单次使用的情况不太方便。

但是,有一种实际的解决方法可用于处理许多此类情况。假设您想拥有以下字典的不变的等同物:

MY_CONSTANT = {
    'something': 123,
    'something_else': 456
}

可以这样模拟:

from collections import namedtuple

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

甚至有可能编写一个辅助函数来自动执行此操作:

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

当然,这仅适用于简单的命令,但实现递归版本并不难。


1
与其他元组答案相同的问题:您必须要做getattr(fa, x)而不是fa[x],没有任何keys方法在您的指尖,并且所有其他原因都希望有一个映射。
疯狂物理学家,2016年

1

子类化 dict

我在野外(github)看到了这种模式,想提一下:

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

用法示例:

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()上PY2)和所有从东西dict开箱没有明确执行这些
  • 在内部使用dict这意味着性能(dict用CPython用c编写)
  • 优雅简约,无黑魔法
  • isinstance(my_frozen_dict, dict)返回True-尽管python鼓励使用鸭式键入许多软件包isinstance(),但这可以节省许多调整和自定义

缺点

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

我在另一个线程中进行了速度比较,结果表明,与许多替代方法相比,覆盖__setitem__和继承dict的速度非常快。
6


0

我需要在某一时刻访问某种东西的固定键,这是一种全球稳定的东西,因此我选择了以下方式:

class MyFrozenDict:
    def __getitem__(self, key):
        if key == 'mykey1':
            return 0
        if key == 'mykey2':
            return "another value"
        raise KeyError(key)

像这样使用

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

警告:对于大多数用例,我不建议这样做,因为这会带来一些非常严重的折衷。


以下各项在功率上没有影响,但均相等。但是,这仅仅是对已接受答案的简化...```类FrozenDict:def __init __(self,data):self._data =数据def __getitem __(self,key):返回self._data [key] `
Yuval

@Yuval的答案不相同。对于初学者来说,api是不同的,因为它需要数据进行初始化。这也意味着它不再可以全局访问。此外,如果_data发生突变,则返回值也会更改。我知道有很多折衷方案-就像我说的那样,我不建议在大多数用例中这样做。
副词

-1

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

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

你frozen_dict类不是可哈希
miracle173
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.