Quale sarebbe un "dict congelato"?


158
  • Un set congelato è un frozenset.
  • Un elenco congelato potrebbe essere una tupla.
  • Cosa sarebbe un dict ghiacciato? Un dict immutabile e sfuggente.

Immagino che potrebbe essere qualcosa di simile collections.namedtuple, ma è più simile a un dict con tasti congelati (un dict mezzo congelato). No?

A "frozendict" dovrebbe essere un dizionario congelato, dovrebbe avere keys, values, get, ecc, e il sostegno in, forecc

aggiornamento:
* eccolo : https://www.python.org/dev/peps/pep-0603

Risposte:


120

Python non ha un tipo frozendict incorporato. Si scopre che questo non sarebbe utile troppo spesso (anche se probabilmente sarebbe utile più spesso di quanto lo frozensetsia).

Il motivo più comune per desiderare un tale tipo è quando la funzione di memorizzazione della memoria richiede funzioni con argomenti sconosciuti. La soluzione più comune per memorizzare un equivalente hash di un dict (in cui i valori sono hash) è qualcosa di simile tuple(sorted(kwargs.iteritems())).

Questo dipende dal fatto che l'ordinamento non è un po 'folle. Python non può promettere positivamente che l'ordinamento si tradurrà in qualcosa di ragionevole qui. (Ma non può promettere molto altro, quindi non sudare troppo.)


Potresti facilmente creare una sorta di wrapper che funzioni in modo molto simile a un dict. Potrebbe assomigliare a qualcosa

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

Dovrebbe funzionare alla grande:

>>> 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
Non so a quale livello di sicurezza del thread le persone si preoccupano di questo tipo di cose, ma a questo proposito il tuo __hash__metodo potrebbe essere leggermente migliorato. Basta usare una variabile temporanea per calcolare l'hash e impostarla solo self._hashuna volta ottenuto il valore finale. In questo modo un altro thread che ottiene un hash mentre il primo sta calcolando farà semplicemente un calcolo ridondante, piuttosto che ottenere un valore errato.
Jeff DQ,

22
@Jeff Di norma, tutto il codice ovunque non è thread-safe e dovresti avvolgerlo in alcune strutture di sincronizzazione per poterlo utilizzare in modo sicuro. Inoltre, la tua particolare nozione di sicurezza del thread si basa sull'atomicità dell'assegnazione degli attributi dell'oggetto, che è tutt'altro che garantita.
Devin Jeanpierre,

9
@Anentropico, non è affatto vero.
Mike Graham,

17
Attenzione: questo "FrozenDict" non è necessariamente congelato. Non c'è nulla che ti impedisca di inserire un elenco modificabile come valore, nel qual caso l'hash genererà un errore. Non c'è nulla di necessariamente sbagliato in questo, ma gli utenti dovrebbero essere consapevoli. Un'altra cosa: questo algoritmo di hash è scelto male, molto incline alle collisioni di hash. Ad esempio, gli hash {'a': 'b'} sono uguali a quelli di {'b': 'a'} e {'a': 1, 'b': 2} hanno lo stesso hash di {'a': 2, ' b ': 1}. La scelta migliore sarebbe self._hash ^ = hash ((chiave, valore))
Steve Byrnes

6
Se si aggiunge una voce modificabile in un oggetto immutabile, i due comportamenti possibili sono generare un errore durante la creazione dell'oggetto o generare un errore nell'hashing dell'oggetto. Le tuple fanno la seconda, frozenset fa la prima. Penso che tu abbia preso una buona decisione per adottare quest'ultimo approccio, tutto sommato. Tuttavia, penso che la gente potrebbe vedere che FrozenDict e frozenset hanno nomi simili e saltare alla conclusione che dovrebbero comportarsi in modo simile. Quindi penso che valga la pena avvertire le persone su questa differenza. :-)
Steve Byrnes l'

63

Curiosamente, anche se abbiamo raramente utile frozensetin Python, non esiste ancora alcuna mappatura congelata. L'idea è stata respinta in PEP 416 - Aggiungi un tipo incorporato frozendict . L'idea può essere rivisitata in Python 3.9, vedere PEP 603 - Aggiunta di un tipo frozenmap alle raccolte .

Quindi la soluzione python 2 a questo:

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

Sembra ancora essere un po 'zoppo:

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

In python3 hai la possibilità di questo :

from types import MappingProxyType

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

def foo(config=DEFAULTS):
    ...

Ora la configurazione predefinita può essere aggiornata in modo dinamico, ma rimane immutabile dove vuoi che sia immutabile passando invece attorno al proxy.

Quindi le modifiche al default_configsi aggiorneranno DEFAULTScome previsto, ma non è possibile scrivere sull'oggetto proxy di mapping stesso.

Certo, non è esattamente la stessa cosa di un "dict immutabile e seccabile" - ma è un sostituto decente dato lo stesso tipo di casi d'uso per i quali potremmo volere un frozendict.


2
Esiste un motivo particolare per memorizzare il proxy in una variabile del modulo? Perché non solo def foo(config=MappingProxyType({'a': 1})):? Il tuo esempio consente comunque anche modifiche globali default_config.
jpmc26,

Inoltre, sospetto che il doppio incarico config = default_config = {'a': 1}sia un refuso.
jpmc26,

21

Supponendo che le chiavi e i valori del dizionario siano essi stessi immutabili (ad es. Stringhe), allora:

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

Questa è una buona, canonica, immutabile rappresentazione di un dict (salvo un comportamento di confronto insano che rovina il genere).
Mike Graham,

6
@devin: d'accordo per intero, ma lascerò che il mio post sia un esempio del fatto che spesso c'è un modo ancora migliore.
msw,

14
Ancora meglio sarebbe metterlo in un frozenset, che non richiede che le chiavi o i valori abbiano un ordinamento coerente definito.
asmeurer,

7
Solo un problema con questo: non hai più una mappatura. Quello sarebbe il punto principale di avere il dict ghiacciato in primo luogo.
Fisico pazzo,

2
Questo metodo è davvero bello quando si torna a un dict. semplicementedict(t)
codythecoder

12

No fronzedict, ma puoi usare MappingProxyTypequello che è stato aggiunto alla libreria standard 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 l'avvertenza:TypeError: can't pickle mappingproxy objects
Radu,

Mi piace l'idea di questo. Ho intenzione di provarlo.
Doug,

10

Ecco il codice che ho usato. Ho sottoclassato Frozenset. I vantaggi di questo sono i seguenti.

  1. Questo è un oggetto veramente immutabile. Non fare affidamento sul buon comportamento dei futuri utenti e sviluppatori.
  2. È facile convertire avanti e indietro tra un dizionario normale e un dizionario congelato. FrozenDict (orig_dict) -> dizionario congelato. dict (frozen_dict) -> dict regolare.

Aggiornamento del 21 gennaio 2015: il codice originale che ho pubblicato nel 2014 utilizzava un ciclo for per trovare una chiave corrispondente. È stato incredibilmente lento. Ora ho messo insieme un'implementazione che sfrutta le funzionalità di hashing di Frozenset. Le coppie chiave-valore sono memorizzate in contenitori speciali in cui le funzioni __hash__e __eq__si basano solo sulla chiave. Questo codice è stato anche formalmente testato dall'unità, a differenza di quello che ho pubblicato qui nell'agosto 2014.

Licenza in stile 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
Si noti che è stato anche concesso in licenza in base a CC BY-SA 3.0, pubblicandolo qui. Almeno questa è l' opinione prevalente . Immagino che la base legale per questo sia l'accettazione di alcuni T&C al momento della prima iscrizione.
Evgeni Sergeev,

1
Mi sono rotto il cervello cercando di pensare a un modo per cercare l'hash chiave senza un dict. Ridefinire l'hash di Itemessere l'hash della chiave è un trucco accurato!
clacke,

Sfortunatamente, il tempo di esecuzione di diff(diff({key}))è ancora lineare nella dimensione di FrozenDict, mentre il tempo di accesso regolare di dict è costante nel caso medio.
Dennis,

6

Penso a frozendict ogni volta che scrivo una funzione come questa:

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

6
Ogni volta che vedo un commento come questo sono sicuro di aver rovinato da qualche parte e messo {} come predefinito, e torno indietro e guardo il mio codice scritto di recente.
Ryan Hiebert,

1
Sì, è un brutto gotcha in cui tutti si imbattono, prima o poi.
Mark Visser,

8
Formulazione più semplice:optional_dict_parm = optional_dict_parm or {}
Emmanuel,

2
In questo caso è possibile utilizzare come valore predefinito per l'argomento. types.MappingProxyType({})
GingerPlusPlus

@GingerPlusPlus potresti scriverlo come risposta?
jonrsharpe,

5

È possibile utilizzare frozendictdal utilspiepacchetto come:

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

Come da documento :

frozendict (dict_obj) : accetta obj di tipo dict e restituisce un dict hashing e immutabile



3

Sì, questa è la mia seconda risposta, ma è un approccio completamente diverso. La prima implementazione è stata in puro pitone. Questo è in Cython. Se sai come utilizzare e compilare i moduli Cython, questo è veloce come un normale dizionario. Circa da 0,04 a 0,06 microsecondi per recuperare un singolo valore.

Questo è il file "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']

Ecco il file "setup.py"

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

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

Se hai installato Cython, salva i due file sopra nella stessa directory. Passa a quella directory nella riga di comando.

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

E dovresti aver finito.


3

Lo svantaggio principale namedtupleè che deve essere specificato prima di essere utilizzato, quindi è meno conveniente per i casi monouso.

Tuttavia, esiste una soluzione pratica che può essere utilizzata per gestire molti di questi casi. Diciamo che vuoi avere un equivalente immutabile del seguente dict:

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

Questo può essere emulato in questo modo:

from collections import namedtuple

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

È anche possibile scrivere una funzione ausiliaria per automatizzare questo:

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

Naturalmente questo funziona solo per i dadi piatti, ma non dovrebbe essere troppo difficile implementare una versione ricorsiva.


1
Stesso problema con l'altra risposta tupla: devi fare getattr(fa, x)invece di fa[x], nessun keysmetodo a portata di mano, e tutti gli altri motivi per cui una mappatura può essere desiderabile.
Fisico pazzo,

1

sottoclasse dict

vedo questo modello in natura (github) e volevo menzionarlo:

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

esempio di utilizzo:

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

Professionisti

  • supporto get(), keys(), items()( iteritems()su PY2) e tutte le caramelle dalla dictfuori dalla scatola senza applicazione esplicitamente
  • usa internamente dictche significa performance ( dictè scritto in c in CPython)
  • elegante semplice e senza magia nera
  • isinstance(my_frozen_dict, dict)restituisce True - sebbene Python incoraggi la tipizzazione anatra di molti pacchetti utilizzati isinstance(), questo può salvare molte modifiche e personalizzazioni

Contro

  • qualsiasi sottoclasse può ignorare questo o accedervi internamente (non puoi davvero proteggere qualcosa al 100% in Python, dovresti fidarti dei tuoi utenti e fornire una buona documentazione).
  • se ti interessa la velocità, potresti voler rendere __hash__un po 'più veloce.

Ho fatto un confronto di velocità in un altro thread e si scopre che l'override __setitem__e l'ereditarietà dictsono follemente veloci rispetto a molte alternative.
Torxed il


0

Avevo bisogno di accedere a chiavi fisse per qualcosa a un certo punto per qualcosa che era una sorta di cosa globalmente costante e mi sono deciso su qualcosa del genere:

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

Usalo come

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

ATTENZIONE: non lo consiglio per la maggior parte dei casi d'uso in quanto presenta alcuni compromessi piuttosto gravi.


Quanto segue sarebbe uguale in potenza senza le prestazioni scariche. Tuttavia, questa è solo una semplificazione della risposta accettata ... `` `classe FrozenDict: def __init __ (self, data): self._data = data def __getitem __ (self, key): return self._data [key]` ` `
Yuval,

@Yuval quella risposta non è equivalente. Per cominciare l'API è diversa in quanto ha bisogno di dati per l'inizializzazione. Ciò implica anche che non è più accessibile a livello globale. Inoltre, se _data viene modificato, il valore di ritorno cambia. Sono consapevole che ci sono importanti compromessi - come ho detto, non lo consiglio per la maggior parte dei casi d'uso.
Adverbly

-1

In assenza del supporto della lingua madre, è possibile farlo da soli o utilizzare una soluzione esistente. Fortunatamente Python semplifica notevolmente l'estensione delle implementazioni di 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

La tua classe frozen_dict non è hashish
miracle173
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.