Que serait un «dict gelé»?


158
  • Un ensemble congelé est un ensemble frozenset.
  • Une liste figée peut être un tuple.
  • Que serait un dict gelé? Un dict immuable et hachable.

Je suppose que ça pourrait être quelque chose comme collections.namedtuple, mais c'est plus comme un dict à touches gelées (un dict à moitié gelé). N'est-ce pas?

Un « frozendict » devrait être un dictionnaire congelé, il devrait avoir keys, values, get, etc., et le soutien in, foretc.

mise à jour:
* la voilà : https://www.python.org/dev/peps/pep-0603

Réponses:


120

Python n'a pas de type frozendict intégré. Il s'avère que cela ne serait pas utile trop souvent (même si cela le serait probablement encore plus souvent frozenset).

La raison la plus courante de vouloir un tel type est lorsque la fonction de mémorisation appelle des fonctions avec des arguments inconnus. La solution la plus courante pour stocker un équivalent hachable d'un dict (où les valeurs sont hachables) est quelque chose comme tuple(sorted(kwargs.iteritems())).

Cela dépend du fait que le tri n'est pas un peu fou. Python ne peut pas promettre que le tri aboutira à quelque chose de raisonnable ici. (Mais cela ne peut pas promettre grand-chose d'autre, alors ne vous inquiétez pas trop.)


Vous pouvez facilement créer une sorte de wrapper qui fonctionne un peu comme un dict. Cela pourrait ressembler à quelque chose comme

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

Cela devrait très bien fonctionner:

>>> 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
Je ne sais pas de quel niveau de sécurité les threads s'inquiètent avec ce genre de choses, mais à cet égard, votre __hash__méthode pourrait être légèrement améliorée. Utilisez simplement une variable temporaire lors du calcul du hachage et ne la définissez qu'une self._hashfois que vous avez la valeur finale. De cette façon, un autre thread obtenant un hachage pendant que le premier calcule fera simplement un calcul redondant, plutôt que d'obtenir une valeur incorrecte.
Jeff DQ le

22
@Jeff En règle générale, tout le code partout n'est pas thread-safe, et vous devriez l'enrouler autour de certaines structures de synchronisation afin d'utiliser ce code en toute sécurité. De plus, votre notion particulière de sécurité des threads repose sur l'atomicité de l'attribution d'attributs d'objet, qui est loin d'être garantie.
Devin Jeanpierre le

9
@Anentropic, ce n'est pas du tout vrai.
Mike Graham

17
Attention: ce "FrozenDict" n'est pas forcément figé. Rien ne vous empêche de mettre une liste modifiable en tant que valeur, auquel cas le hachage générera une erreur. Il n'y a rien de mal à cela, mais les utilisateurs doivent être conscients. Autre chose: cet algorithme de hachage est mal choisi, très sujet aux collisions de hachage. Par exemple {'a': 'b'} hache la même chose que {'b': 'a'} et {'a': 1, 'b': 2} hache la même chose que {'a': 2, ' b ': 1}. Un meilleur choix serait self._hash ^ = hash ((key, value))
Steve Byrnes

6
Si vous ajoutez une entrée mutable dans un objet immuable, les deux comportements possibles consistent à générer une erreur lors de la création de l'objet ou à générer une erreur lors du hachage de l'objet. Les tuples font le dernier, frozenset fait le premier. Je pense vraiment que vous avez pris la bonne décision d'adopter cette dernière approche, tout bien considéré. Néanmoins, je pense que les gens pourraient voir que FrozenDict et frozenset ont des noms similaires, et sauter à la conclusion qu'ils devraient se comporter de la même manière. Je pense donc qu'il vaut la peine d'avertir les gens de cette différence. :-)
Steve Byrnes

63

Curieusement, bien que nous ayons le rarement utile frozenseten python, il n'y a toujours pas de mappage figé. L'idée a été rejetée dans PEP 416 - Ajouter un type intégré frozendict . L'idée peut être revisitée en Python 3.9, voir PEP 603 - Ajout d'un type de Frozenmap aux collections .

Donc, la solution python 2 à cela:

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

Semble toujours être un peu boiteux:

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

En python3, vous avez l'option de ceci :

from types import MappingProxyType

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

def foo(config=DEFAULTS):
    ...

Maintenant, la configuration par défaut peut être mise à jour de manière dynamique, mais reste immuable là où vous voulez qu'elle soit immuable en passant le proxy à la place.

Donc les changements dans la default_configmise à jourDEFAULTS comme prévu, mais vous ne pouvez pas écrire dans l'objet proxy de mappage lui-même.

Certes, ce n'est pas tout à fait la même chose qu'un "dict immuable, hachable" - mais c'est un substitut décent étant donné le même genre de cas d'utilisation pour lesquels nous pourrions vouloir un frozendict.


2
Y a-t-il une raison particulière de stocker le proxy dans une variable de module? Pourquoi pas juste def foo(config=MappingProxyType({'a': 1})):? Votre exemple permet toujours une modification globale default_config.
jpmc26

De plus, je soupçonne que la double affectation config = default_config = {'a': 1}est une faute de frappe.
jpmc26

21

En supposant que les clés et les valeurs du dictionnaire sont elles-mêmes immuables (par exemple des chaînes), alors:

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

Il s'agit d'une bonne représentation canonique et immuable d'un dict (sauf si un comportement de comparaison insensé perturbe le tri).
Mike Graham

6
@devin: d'accord dans son intégralité, mais je vais laisser mon message montrer qu'il existe souvent un moyen encore meilleur.
msw

14
Mieux encore serait de le mettre dans un frozenset, qui ne nécessite pas que les clés ou les valeurs aient un ordre cohérent défini.
asmeurer

8
Un seul problème avec cela: vous n'avez plus de cartographie. Ce serait tout l'intérêt d'avoir le dict gelé en premier lieu.
Mad Physicist

2
Cette méthode est vraiment agréable pour revenir à un dict. simplementdict(t)
codythecoder

12

Il n'y en a pas fronzedict, mais vous pouvez utiliser MappingProxyTypecelui qui a été ajouté à la bibliothèque standard avec 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})

avec la mise en garde:TypeError: can't pickle mappingproxy objects
Radu

J'aime l'idée de ça. Je vais essayer.
Doug

Le problème avec ceci MappingProxyTypeest toujours insurmontable.
Jab il y a

10

Voici le code que j'utilise. J'ai sous-classé frozenset. Les avantages de ceci sont les suivants.

  1. C'est un objet vraiment immuable. Ne comptez pas sur le bon comportement des futurs utilisateurs et développeurs.
  2. Il est facile de faire des va-et-vient entre un dictionnaire normal et un dictionnaire figé. FrozenDict (orig_dict) -> dictionnaire gelé. dict (Frozen_dict) -> dict régulier.

Mise à jour du 21 janvier 2015: le morceau de code original que j'ai publié en 2014 utilisait une boucle for pour trouver une clé qui correspondait. C'était incroyablement lent. Maintenant, j'ai mis en place une implémentation qui tire parti des fonctionnalités de hachage de frozenset. Les paires clé-valeur sont stockées dans des conteneurs spéciaux où les fonctions __hash__et __eq__sont basées uniquement sur la clé. Ce code a également été officiellement testé à l'unité, contrairement à ce que j'ai publié ici en août 2014.

Licence de type 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
Notez que vous l'avez également licencié sous CC BY-SA 3.0, en le publiant ici. Du moins, c'est l' opinion qui prévaut . Je suppose que la base légale pour cela est d'accepter certaines conditions générales lors de votre première inscription.
Evgeni Sergeev

1
Je me suis brisé la tête en essayant de trouver un moyen de rechercher le hachage de clé sans dict. Redéfinir le hachage du Itempour être le hachage de la clé est un hack soigné!
clacke

Malheureusement, le temps d'exécution de diff(diff({key}))est toujours linéaire dans la taille de FrozenDict, tandis que le temps d'accès régulier aux dict est constant dans le cas moyen.
Dennis

6

Je pense à frozendict à chaque fois que j'écris une fonction comme celle-ci:

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

6
Chaque fois que je vois un commentaire comme celui-ci, je suis sûr que j'ai foiré quelque part et que j'ai mis {} par défaut, et que je reviens en arrière et que je regarde mon code récemment écrit.
Ryan Hiebert

1
Ouais, c'est un sale piège que tout le monde rencontre, tôt ou tard.
Mark Visser

8
Formulation plus simple:optional_dict_parm = optional_dict_parm or {}
Emmanuel

2
Dans ce cas, vous pouvez utiliser comme valeur par défaut pour l'argument. types.MappingProxyType({})
GingerPlusPlus

@GingerPlusPlus pourriez-vous écrire cela comme une réponse?
jonrsharpe

5

Vous pouvez utiliser frozendictfrom utilspiepackage comme:

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

Selon le document :

frozendict (dict_obj) : Accepte les obj de type dict et retourne un dict hachable et immuable



3

Oui, c'est ma deuxième réponse, mais c'est une approche complètement différente. La première implémentation était en python pur. Celui-ci est en Cython. Si vous savez comment utiliser et compiler des modules Cython, c'est aussi rapide qu'un dictionnaire ordinaire. Environ 0,04 à 0,06 micro-s pour récupérer une seule valeur.

Ceci est le fichier "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']

Voici le fichier "setup.py"

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

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

Si Cython est installé, enregistrez les deux fichiers ci-dessus dans le même répertoire. Accédez à ce répertoire dans la ligne de commande.

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

Et vous devriez avoir fini.


3

Le principal inconvénient de namedtuple est qu'il doit être spécifié avant d'être utilisé, il est donc moins pratique pour les cas à usage unique.

Cependant, il existe une solution de contournement pratique qui peut être utilisée pour gérer de nombreux cas de ce type. Disons que vous voulez avoir un équivalent immuable du dict suivant:

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

Cela peut être émulé comme ceci:

from collections import namedtuple

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

Il est même possible d'écrire une fonction auxiliaire pour automatiser cela:

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

Bien sûr, cela ne fonctionne que pour les dicts plats, mais il ne devrait pas être trop difficile d'implémenter une version récursive.


1
Même problème qu'avec l'autre réponse tuple: il faut faire à la getattr(fa, x)place de fa[x], aucune keysméthode au bout de vos doigts, et toutes les autres raisons pour lesquelles un mapping peut être souhaitable.
Mad Physicist

1

Sous-classement dict

Je vois ce modèle dans la nature (github) et je voulais le mentionner:

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

exemple d'utilisation:

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

Avantages

  • appuyer pour get(), keys(), items()( iteritems()sur py2) et toutes les friandises dedict sortir de la boîte sans les mettre en œuvre explicitement
  • utilise en interne dictce qui signifie performance (dict est écrit en c en CPython)
  • élégant simple et sans magie noire
  • isinstance(my_frozen_dict, dict)renvoie True - bien que python encourage l' utilisation de nombreux paquets de type canardisinstance() , cela peut économiser de nombreux ajustements et personnalisations

Les inconvénients

  • toute sous-classe peut remplacer cela ou y accéder en interne (vous ne pouvez pas vraiment protéger à 100% quelque chose en python, vous devez faire confiance à vos utilisateurs et fournir une bonne documentation).
  • si vous aimez la vitesse, vous voudrez peut-être faire __hash__un peu plus vite.

J'ai fait une comparaison de vitesse dans un autre thread et il s'avère que le remplacement __setitem__et l'héritage dictsont incroyablement rapides par rapport à de nombreuses alternatives.
Torxed le


0

J'avais besoin d'accéder à des clés fixes pour quelque chose à un moment donné pour quelque chose qui était une sorte de genre de chose globalement constant et je me suis installé sur quelque chose comme ceci:

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

Utilisez-le comme

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

AVERTISSEMENT: je ne le recommande pas pour la plupart des cas d'utilisation car cela fait des compromis assez sévères.


Ce qui suit serait égal en puissance sans les scarifices de performance. Cependant, ce n'est qu'une simplification de la réponse acceptée ... `` `` class FrozenDict: def __init __ (self, data): self._data = data def __getitem __ (self, key): return self._data [key] `` `
Yuval

@Yuval cette réponse n'est pas équivalente. Pour commencer, l'API est différente car elle a besoin de données pour démarrer. Cela implique également qu'il n'est plus accessible à l'échelle mondiale. De plus, si _data est muté, votre valeur de retour change. Je suis conscient qu'il existe des compromis importants - comme je l'ai dit, je ne le recommande pas pour la plupart des cas d'utilisation.
Adverbly le

-1

En l'absence de prise en charge de la langue native, vous pouvez le faire vous-même ou utiliser une solution existante. Heureusement, Python rend très simple l'extension de leurs implémentations de 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

Votre classe Frozen_dict n'est pas hachable
miracle173
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.