Qualcuno sa come viene implementato il tipo di dizionario integrato per Python? La mia comprensione è che si tratta di una sorta di tabella hash, ma non sono stato in grado di trovare una risposta definitiva.
Qualcuno sa come viene implementato il tipo di dizionario integrato per Python? La mia comprensione è che si tratta di una sorta di tabella hash, ma non sono stato in grado di trovare una risposta definitiva.
Risposte:
Ecco tutto ciò che riguarda i Dit Python che sono stato in grado di mettere insieme (probabilmente più di quanto chiunque vorrebbe sapere; ma la risposta è completa).
dict
utilizza l'indirizzamento aperto per risolvere le collisioni di hash (spiegate di seguito) (vedere dictobject.c: 296-297 ).O(1)
ricerca per indice).La figura seguente è una rappresentazione logica di una tabella hash Python. Nella figura in basso, 0, 1, ..., i, ...
a sinistra ci sono indici degli slot nella tabella hash (sono solo a scopo illustrativo e ovviamente non sono memorizzati insieme alla tabella!).
# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+
Quando un nuovo dict viene inizializzato, inizia con 8 slot . (vedi dictobject.h: 49 )
i
basati sull'hash della chiave. CPython inizialmente utilizza i = hash(key) & mask
(dove mask = PyDictMINSIZE - 1
, ma non è molto importante). Basta notare che lo slot iniziale i
, che viene controllato dipende dall'hash della chiave.<hash|key|value>
). Ma cosa succede se quello slot è occupato !? Molto probabilmente perché un'altra voce ha lo stesso hash (hash collision!)==
confronto non il is
confronto) della voce nello slot con l'hash e la chiave della voce corrente da inserire ( dictobject.c : 337.344-345 ) rispettivamente. Se entrambi corrispondono, allora pensa che la voce esista già, si arrende e passa alla voce successiva da inserire. Se l'hash o la chiave non corrispondono, inizia il sondaggio .i+1, i+2, ...
e usare il primo disponibile (che è il sondaggio lineare). Ma per ragioni spiegate magnificamente nei commenti (vedi dictobject.c: 33-126 ), CPython usa il sondaggio casuale . Nel sondaggio casuale, lo slot successivo viene scelto in un ordine pseudo casuale. La voce viene aggiunta al primo slot vuoto. Per questa discussione, l'algoritmo effettivo utilizzato per scegliere lo slot successivo non è molto importante (vedere dictobject.c: 33-126 per l'algoritmo per il sondaggio). Ciò che è importante è che gli slot vengano sondati fino a quando non viene trovato il primo slot vuoto.dict
verrà ridimensionato se è pieno per due terzi. Questo evita di rallentare le ricerche. (vedi dictobject.h: 64-65 )NOTA: ho svolto ricerche sull'implementazione di Python Dict in risposta alla mia domanda su come più voci in un dict possano avere gli stessi valori di hash. Ho pubblicato qui una versione leggermente modificata della risposta perché tutta la ricerca è molto rilevante anche per questa domanda.
Come vengono implementati i dizionari integrati di Python?
Ecco il breve corso:
L'aspetto ordinato non è ufficiale a partire da Python 3.6 (per dare ad altre implementazioni la possibilità di tenere il passo), ma ufficiale in Python 3.7 .
Per molto tempo ha funzionato esattamente così. Python avrebbe preallocato 8 righe vuote e usato l'hash per determinare dove incollare la coppia chiave-valore. Ad esempio, se l'hash per la chiave termina in 001, lo inserisce nell'indice 1 (cioè 2 °) (come nell'esempio seguente).
<hash> <key> <value>
null null null
...010001 ffeb678c 633241c4 # addresses of the keys and values
null null null
... ... ...
Ogni riga occupa 24 byte su un'architettura a 64 bit, 12 su 32 bit. (Nota che le intestazioni di colonna sono solo etichette per i nostri scopi qui - in realtà non esistono in memoria.)
Se l'hash terminava come l'hash di una chiave preesistente, questa è una collisione e quindi la coppia chiave-valore si attaccherebbe in una posizione diversa.
Dopo aver memorizzato 5 valori-chiave, quando si aggiunge un'altra coppia chiave-valore, la probabilità di collisioni con hash è troppo grande, quindi il dizionario ha dimensioni doppie. In un processo a 64 bit, prima del ridimensionamento, abbiamo 72 byte vuoti e, successivamente, stiamo sprecando 240 byte a causa delle 10 righe vuote.
Questo richiede molto spazio, ma il tempo di ricerca è abbastanza costante. L'algoritmo di confronto delle chiavi consiste nel calcolare l'hash, andare nella posizione prevista, confrontare l'id della chiave: se sono lo stesso oggetto, sono uguali. Se poi non confrontare i valori hash, se sono non la stessa, non sono uguali. Altrimenti, infine confrontiamo le chiavi per l'uguaglianza e, se sono uguali, restituiamo il valore. Il confronto finale per l'uguaglianza può essere piuttosto lento, ma i controlli precedenti di solito abbreviano il confronto finale, rendendo le ricerche molto veloci.
Le collisioni rallentano le cose e un attaccante potrebbe teoricamente usare le collisioni di hash per eseguire un attacco denial of service, quindi abbiamo randomizzato l'inizializzazione della funzione hash in modo tale che calcoli hash diversi per ogni nuovo processo di Python.
Lo spazio sprecato sopra descritto ci ha portato a modificare l'implementazione dei dizionari, con un'emozionante nuova funzionalità che i dizionari sono ora ordinati per inserimento.
Iniziamo invece preallocando un array per l'indice dell'inserzione.
Poiché la nostra prima coppia chiave-valore va nel secondo slot, indicizziamo in questo modo:
[null, 0, null, null, null, null, null, null]
E la nostra tabella viene popolata solo per ordine di inserzione:
<hash> <key> <value>
...010001 ffeb678c 633241c4
... ... ...
Quindi quando cerchiamo una chiave, usiamo l'hash per verificare la posizione che ci aspettiamo (in questo caso, andiamo direttamente all'indice 1 dell'array), quindi andiamo a quell'indice nella tabella hash (es. Indice 0 ), verificare che le chiavi siano uguali (utilizzando lo stesso algoritmo descritto in precedenza) e, in tal caso, restituire il valore.
Manteniamo tempi di ricerca costanti, con minori perdite di velocità in alcuni casi e guadagni in altri, con i lati positivi che risparmiamo un bel po 'di spazio sull'implementazione preesistente e manteniamo l'ordine di inserzione. L'unico spazio sprecato sono i byte null nella matrice dell'indice.
Raymond Hettinger lo ha introdotto su python-dev nel dicembre 2012. È finalmente entrato in CPython in Python 3.6 . L'ordinamento per inserzione è stato considerato un dettaglio dell'implementazione per 3.6 per consentire ad altre implementazioni di Python di recuperare.
Un'altra ottimizzazione per risparmiare spazio è un'implementazione che condivide le chiavi. Quindi, invece di avere dizionari ridondanti che occupano tutto quello spazio, abbiamo dizionari che riutilizzano le chiavi condivise e gli hash delle chiavi. Puoi pensarlo in questo modo:
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
Per una macchina a 64 bit, ciò potrebbe salvare fino a 16 byte per chiave per dizionario aggiuntivo.
Questi dicts a chiave condivisa sono destinati all'uso per oggetti personalizzati ' __dict__
. Per ottenere questo comportamento, credo che tu debba finire di popolare il tuo __dict__
prima di istanziare il tuo prossimo oggetto ( vedi PEP 412 ). Questo significa che dovresti assegnare tutti i tuoi attributi nel __init__
o __new__
, altrimenti potresti non ottenere i tuoi risparmi di spazio.
Tuttavia, se conosci tutti i tuoi attributi al momento __init__
dell'esecuzione, potresti anche fornire il __slots__
tuo oggetto e garantire che __dict__
non sia stato creato affatto (se non disponibile nei genitori), o addirittura consentire __dict__
ma garantire che i tuoi attributi previsti siano memorizzato comunque negli slot. Per ulteriori informazioni __slots__
, vedi la mia risposta qui .
**kwargs
in una funzione.find_empty_slot
: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - e partendo dalla linea 134 c'è della prosa che lo descrive.
I dizionari Python utilizzano l' indirizzamento aperto ( riferimento all'interno di Beautiful code )
NB! L'indirizzamento aperto , noto anche come hashing chiuso , non dovrebbe essere confuso, come osservato in Wikipedia, con il suo hashing aperto opposto !
L'indirizzamento aperto indica che il dict utilizza slot di array e quando la posizione principale di un oggetto viene presa nel dict, il punto dell'oggetto viene ricercato in un indice diverso nello stesso array, utilizzando uno schema "perturbazione", in cui il valore hash dell'oggetto gioca un ruolo .