Livelli multipli di 'collection.defaultdict' in Python


176

Grazie ad alcune persone fantastiche su SO, ho scoperto le possibilità offerte collections.defaultdict, in particolare in termini di leggibilità e velocità. Li ho usati con successo.

Ora vorrei implementare tre livelli di dizionari, di cui i due principali sono quelli defaultdictpiù bassi int. Non trovo il modo appropriato per farlo. Ecco il mio tentativo:

from collections import defaultdict
d = defaultdict(defaultdict)
a = [("key1", {"a1":22, "a2":33}),
     ("key2", {"a1":32, "a2":55}),
     ("key3", {"a1":43, "a2":44})]
for i in a:
    d[i[0]] = i[1]

Ora funziona, ma il seguente, che è il comportamento desiderato, non funziona:

d["key4"]["a1"] + 1

Sospetto che avrei dovuto dichiarare da qualche parte che il secondo livello defaultdictè di tipo int, ma non ho trovato dove o come farlo.

Il motivo che sto usando defaultdictin primo luogo è quello di evitare di dover inizializzare il dizionario per ogni nuova chiave.

Qualche suggerimento più elegante?

Grazie pitoni!

Risposte:


341

Uso:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

Questo creerà un nuovo defaultdict(int)ogni volta che si accede a una nuova chiave d.


2
L'unico problema è che non si rovinerà, il che significa che multiprocessingnon è contento di inviarli avanti e indietro.
Noah,

19
@Noah: si decomporrà se si utilizza una funzione a livello di modulo anziché un lambda.
Interjay

4
@ScienceFriction Qualcosa di specifico con cui hai bisogno di aiuto? Quando d[new_key]si accede, chiamerà il lambda che creerà un nuovo defaultdict(int). E quando d[existing_key][new_key2]si accede, intverrà creato un nuovo .
Interjay

11
Questo e spettacolare. Sembra che ogni giorno rinnovo i miei voti coniugali a Python.
mVChr,

3
Alla ricerca di maggiori dettagli sull'uso di questo metodo multiprocessinge su quale sia una funzione denominata a livello di modulo? Questa domanda fa seguito.
Cecilia,

32

Un altro modo per creare un defaultdict nidificato selezionabile è utilizzare un oggetto parziale anziché un lambda:

from functools import partial
...
d = defaultdict(partial(defaultdict, int))

Questo funzionerà perché la classe defaultdict è accessibile a livello globale a livello di modulo:

"Non è possibile decapare un oggetto parziale a meno che la funzione [o in questo caso, la classe] che avvolge non sia accessibile a livello globale ... sotto il suo __name__ (nel suo __module__)" - Decapaggio delle funzioni parziali avvolte


12

Guardate la risposta di nosklo qui per una soluzione più generale.

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            value = self[item] = type(self)()
            return value

test:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Produzione:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

Grazie per il link @ miglia82 (e la modifica, @voyager). Quanto è pitone e sicuro questo approccio?
Morlock,

2
Sfortunatamente questa soluzione non conserva la parte più pratica di defaultdict, che è il potere di scrivere qualcosa come D ['key'] + = 1 senza preoccuparsi dell'esistenza della chiave. Questa è la caratteristica principale per cui uso defaultdict per ... ma posso immaginare che anche i dizionari con approfondimento dinamico siano abbastanza utili.
rschwieb,

2
@rschwieb puoi aggiungere il potere di scrivere + = 1 aggiungendo il metodo add .
spazm

5

Come da richiesta di @ rschwieb D['key'] += 1, possiamo espandere il precedente sovrascrivendo l'aggiunta definendo il __add__metodo, per far sì che questo si comporti più come uncollections.Counter()

In primo luogo __missing__verrà chiamato per creare un nuovo valore vuoto, che verrà passato in __add__. Testiamo il valore, contando su valori vuoti per essere False.

Vedere emulazione di tipi numerici per ulteriori informazioni sulla sostituzione.

from numbers import Number


class autovivify(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition for numeric types when self is empty """
        if not self and isinstance(x, Number):
            return x
        raise ValueError

    def __sub__(self, x):
        if not self and isinstance(x, Number):
            return -1 * x
        raise ValueError

Esempi:

>>> import autovivify
>>> a = autovivify.autovivify()
>>> a
{}
>>> a[2]
{}
>>> a
{2: {}}
>>> a[4] += 1
>>> a[5][3][2] -= 1
>>> a
{2: {}, 4: 1, 5: {3: {2: -1}}}

Invece di controllare l'argomento è un numero (molto non python, amirite!) Potremmo semplicemente fornire un valore 0 predefinito e quindi tentare l'operazione:

class av2(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition when self is empty """
        if not self:
            return 0 + x
        raise ValueError

    def __sub__(self, x):
        """ override subtraction when self is empty """
        if not self:
            return 0 - x
        raise ValueError

questi dovrebbero generare NotImplemented piuttosto che ValueError?
spazm

5

In ritardo alla festa, ma per profondità arbitraria mi sono appena trovato a fare qualcosa del genere:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

Il trucco qui è fondamentalmente quello di rendere l' DeepDictistanza stessa una fabbrica valida per costruire valori mancanti. Ora possiamo fare cose del genere

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9

1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

Ancora nessun errore. Non importa quanti livelli nidificati. anche nessun errore pop

dd = defaultdict ({ "1": 333333})

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.