¿Qué sería un "dict congelado"?


158
  • Un conjunto congelado es un conjunto congelado.
  • Una lista congelada podría ser una tupla.
  • ¿Qué sería un dict congelado? Un dict inmutable, hashable.

Supongo que podría ser algo así collections.namedtuple, pero eso es más como un dict de teclas congeladas (un dict medio congelado). ¿No es así?

Un "frozendict" debe ser un diccionario congelada, debe tener keys, values, get, etc, y el apoyo in, foretc.

actualización:
* ahí está: https://www.python.org/dev/peps/pep-0603

Respuestas:


120

Python no tiene un tipo de congelador incorporado. Resulta que esto no sería útil con demasiada frecuencia (aunque probablemente aún sería útil con más frecuencia de lo que frozensetes).

La razón más común para querer ese tipo es cuando se memorizan llamadas de funciones para funciones con argumentos desconocidos. La solución más común para almacenar un equivalente hashable de un dict (donde los valores son hashaable) es algo así tuple(sorted(kwargs.iteritems())).

Esto depende de que la clasificación no sea un poco loca. Python no puede prometer positivamente que la clasificación dará como resultado algo razonable aquí. (Pero no puede prometer mucho más, así que no te preocupes demasiado).


Fácilmente podría hacer algún tipo de envoltura que funcione como un dict. Puede parecer algo así

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

Debería funcionar muy bien:

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

77
No sé qué nivel de seguridad de los hilos le preocupa a la gente con este tipo de cosas, pero a ese respecto, su __hash__método podría mejorarse ligeramente. Simplemente use una variable temporal al calcular el hash, y solo establezca self._hashuna vez que tenga el valor final. De esa manera, otro hilo que obtiene un hash mientras el primero está calculando simplemente hará un cálculo redundante, en lugar de obtener un valor incorrecto.
Jeff DQ

22
@Jeff Como regla general, todo el código en todas partes no es seguro para subprocesos, y debe envolverlo alrededor de algunas estructuras de sincronización para usar ese código de manera segura. Además, su noción particular de seguridad de subprocesos se basa en la atomicidad de la asignación de atributos de objeto, que está lejos de estar garantizada.
Devin Jeanpierre

9
@Anentropic, eso no es cierto en absoluto.
Mike Graham

17
Tenga cuidado: este "FrozenDict" no está necesariamente congelado. No hay nada que le impida poner una lista mutable como valor, en cuyo caso el hashing arrojará un error. No hay nada necesariamente malo en eso, pero los usuarios deben ser conscientes. Otra cosa: este algoritmo de hash está mal elegido, es muy propenso a colisiones de hash. Por ejemplo, {'a': 'b'} tiene el mismo valor que {'b': 'a'} y {'a': 1, 'b': 2} tiene el mismo valor que {'a': 2, ' b ': 1}. La mejor opción sería self._hash ^ = hash ((clave, valor))
Steve Byrnes

66
Si agrega una entrada mutable en un objeto inmutable, los dos comportamientos posibles son arrojar un error al crear el objeto, o arrojar un error al trocear el objeto. Las tuplas hacen lo último, frozenset hace lo primero. Definitivamente creo que tomaste una buena decisión para tomar el último enfoque, considerando todo. Sin embargo, creo que la gente podría ver que FrozenDict y frozenset tienen nombres similares, y llegar a la conclusión de que deberían comportarse de manera similar. Así que creo que vale la pena advertir a la gente sobre esta diferencia. :-)
Steve Byrnes

63

Curiosamente, aunque rara vez somos útiles frozenseten Python, todavía no hay mapeo congelado. La idea fue rechazada en PEP 416: agregue un tipo incorporado de congelación . La idea puede revisarse en Python 3.9, ver PEP 603 - Agregar un tipo de mapa congelado a las colecciones .

Entonces, la solución de Python 2 para esto:

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

Todavía parece ser algo cojo:

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

En python3 tienes la opción de esto :

from types import MappingProxyType

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

def foo(config=DEFAULTS):
    ...

Ahora la configuración predeterminada se puede actualizar dinámicamente, pero permanece inmutable donde desea que sea inmutable pasando el proxy en su lugar.

Por lo tanto, los cambios en el default_configse actualizarán DEFAULTScomo se esperaba, pero no puede escribir en el objeto proxy de mapeo.

Es cierto que no es exactamente lo mismo que un "dict inmutable y hashable", pero es un sustituto decente dado el mismo tipo de casos de uso para los que podríamos querer un congelado.


2
¿Hay alguna razón particular para almacenar el proxy en una variable de módulo? ¿Por qué no solo def foo(config=MappingProxyType({'a': 1})):? Su ejemplo todavía permite la modificación global default_configtambién.
jpmc26

Además, sospecho que la doble asignación en config = default_config = {'a': 1}es un error tipográfico.
jpmc26

21

Suponiendo que las claves y los valores del diccionario son inmutables (por ejemplo, cadenas), entonces:

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

Esta es una buena representación canónica e inmutable de un dict (salvo el comportamiento de comparación loco que estropea el tipo).
Mike Graham el

66
@devin: estoy totalmente de acuerdo, pero dejaré que mi publicación sea un ejemplo de que a menudo hay una forma aún mejor.
msw

14
Aún mejor sería ponerlo en un conjunto congelado, que no requiere que las claves o valores tengan un orden consistente definido.
Asmeurer

77
Solo hay un problema con esto: ya no tienes un mapeo. Ese sería el punto de tener el dict congelado en primer lugar.
Físico loco

2
Este método es realmente bueno cuando regresamos a un dict. simplementedict(t)
codythecoder

12

No existe fronzedict, pero puede usar el MappingProxyTypeque se agregó a la biblioteca estándar con Python 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})

con la advertencia:TypeError: can't pickle mappingproxy objects
Radu

Me gusta la idea de esto. Voy a probarlo.
Doug

10

Aquí está el código que he estado usando. Subclase el grupo congelado. Las ventajas de esto son las siguientes.

  1. Este es un objeto verdaderamente inmutable. No confiar en el buen comportamiento de futuros usuarios y desarrolladores.
  2. Es fácil convertir de ida y vuelta entre un diccionario normal y un diccionario congelado. FrozenDict (orig_dict) -> diccionario congelado. dict (frozen_dict) -> dict regular.

Actualización del 21 de enero de 2015: el código original que publiqué en 2014 utilizó un bucle for para encontrar una clave que coincidiera. Eso fue increíblemente lento. Ahora he creado una implementación que aprovecha las funciones de hash de frozenset. Los pares clave-valor se almacenan en contenedores especiales donde las funciones __hash__y __eq__se basan solo en la clave. Este código también se ha probado formalmente en unidades, a diferencia de lo que publiqué aquí en agosto de 2014.

Licencia de estilo 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
Tenga en cuenta que también lo ha licenciado bajo CC BY-SA 3.0, al publicarlo aquí. Al menos esa es la opinión predominante . Supongo que la base legal para eso es aceptar algunos T&C cuando te registraste por primera vez.
Evgeni Sergeev

1
Me rompí el cerebro tratando de pensar en una forma de buscar el hash clave sin un dict. ¡Redefinir el hash de Itemser el hash de la clave es un buen truco!
clacke

Desafortunadamente, el tiempo de ejecución de diff(diff({key}))sigue siendo lineal en el tamaño de FrozenDict, mientras que el tiempo de acceso de dict regular es constante en el caso promedio.
Dennis

6

Pienso en frozendict cada vez que escribo una función como esta:

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

66
Cada vez que veo un comentario como este, estoy seguro de que me equivoqué en algún lugar y puse {} como predeterminado, y vuelvo y miro mi código recientemente escrito.
Ryan Hiebert

1
Sí, es un problema desagradable con el que todos se topan, tarde o temprano.
Mark Visser el

8
Formulación más fácil:optional_dict_parm = optional_dict_parm or {}
Emmanuel

2
En este caso, puede usarlo como valor predeterminado para el argumento. types.MappingProxyType({})
GingerPlusPlus

@GingerPlusPlus, ¿podrías escribir eso como respuesta?
jonrsharpe

5

Es posible utilizar frozendictde utilspiepaquete como:

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

Según el documento :

frozendict (dict_obj) : acepta obj de tipo dict y devuelve un dict hashable e inmutable



3

Sí, esta es mi segunda respuesta, pero es un enfoque completamente diferente. La primera implementación fue en Python puro. Este está en Cython. Si sabe cómo usar y compilar módulos de Cython, esto es tan rápido como un diccionario normal. Aproximadamente .04 a .06 microsegundos para recuperar un solo valor.

Este es el archivo "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']

Aquí está el archivo "setup.py"

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

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

Si tiene instalado Cython, guarde los dos archivos anteriores en el mismo directorio. Mover a ese directorio en la línea de comando.

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

Y deberías haber terminado.


3

La principal desventaja namedtuplees que debe especificarse antes de usarse, por lo que es menos conveniente para casos de un solo uso.

Sin embargo, existe una solución práctica que se puede utilizar para manejar muchos de estos casos. Digamos que desea tener un equivalente inmutable del siguiente dict:

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

Esto se puede emular así:

from collections import namedtuple

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

Incluso es posible escribir una función auxiliar para automatizar esto:

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

Por supuesto, esto funciona solo para dictos planos, pero no debería ser demasiado difícil implementar una versión recursiva.


1
El mismo problema que con la otra respuesta de tupla: debe hacer, en getattr(fa, x)lugar de fa[x], ningún keysmétodo al alcance de la mano, y todas las otras razones por las que un mapeo puede ser deseable.
Físico loco

1

Subclases dict

Veo este patrón en la naturaleza (github) y quería mencionarlo:

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

ejemplo de uso:

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

Pros

  • soporte para get(), keys(), items()( iteritems()en la AP2) y todas las golosinas de dictsacarlo de la caja sin implementar explícitamente
  • utiliza internamente, lo dictque significa rendimiento ( dictestá escrito en c en CPython)
  • elegante simple y sin magia negra
  • isinstance(my_frozen_dict, dict) devuelve True, aunque Python fomenta utilización de muchos tipos de paquetes isinstance(), esto puede ahorrar muchos ajustes y personalizaciones

Contras

  • cualquier subclase puede anular esto o acceder a él internamente (realmente no puede proteger al 100% algo en Python, debe confiar en sus usuarios y proporcionar una buena documentación).
  • Si le importa la velocidad, es posible que desee hacer __hash__un poco más rápido.

Hice una comparación de velocidad en otro hilo y resulta que anular __setitem__y heredar dictes increíblemente rápido en comparación con muchas alternativas.
Torxed


0

Necesitaba acceder a claves fijas para algo en un punto para algo que era una especie de cosa globalmente constante y me decidí por algo como esto:

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

Úselo como

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

ADVERTENCIA: No recomiendo esto para la mayoría de los casos de uso, ya que hace algunas compensaciones bastante graves.


Lo siguiente sería igual en potencia sin las escaseces de rendimiento. Sin embargo, esto es solo una simplificación de la respuesta aceptada ... `` `class FrozenDict: def __init __ (self, data): self._data = data def __getitem __ (self, key): return self._data [key]` ` `
Yuval

@Yuval esa respuesta no es equivalente. Para empezar, la API es diferente ya que necesita datos para iniciar. Esto también implica que ya no es accesible a nivel mundial. Además, si _data está mutado, su valor de retorno cambia. Soy consciente de que hay compensaciones significativas, como dije, no lo recomiendo para la mayoría de los casos de uso.
Adverbly

-1

En ausencia de soporte de idioma nativo, puede hacerlo usted mismo o usar una solución existente. Afortunadamente, Python hace que sea muy sencillo extender sus implementaciones base.

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

Su clase frozen_dict no es hashable
miracle173
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.