Czy ktoś wie, jak zaimplementowano wbudowany słownik w Pythonie? Rozumiem, że jest to jakaś tabela haszująca, ale nie udało mi się znaleźć żadnej ostatecznej odpowiedzi.
python
data-structures
dictionary
ricree
źródło
źródło
Odpowiedzi:
Oto wszystko na temat słowników Python, które udało mi się zebrać (prawdopodobnie więcej niż ktokolwiek chciałby wiedzieć; ale odpowiedź jest wyczerpująca).
dict
używa otwartego adresowania do rozwiązywania kolizji mieszających (wyjaśnione poniżej) (patrz dictobject.c: 296-297 ).O(1)
wyszukiwać według indeksu).Poniższy rysunek przedstawia logiczną reprezentację tabeli skrótów Pythona. Na poniższym rysunku
0, 1, ..., i, ...
po lewej stronie znajdują się wskaźniki szczelin w tablicy skrótów (służą one wyłącznie celom ilustracyjnym i oczywiście nie są przechowywane razem ze stołem!).Po zainicjowaniu nowego nagrania zaczyna się on od 8 miejsc . (patrz dictobject.h: 49 )
i
, który jest oparty na skrócie klucza. CPython początkowo używai = hash(key) & mask
(gdziemask = PyDictMINSIZE - 1
, ale to nie jest tak naprawdę ważne). Pamiętaj tylko, że początkowy slot,i
który jest sprawdzany, zależy od skrótu klucza.<hash|key|value>
). Ale co, jeśli ten slot jest zajęty !? Najprawdopodobniej, ponieważ inny wpis ma ten sam skrót (kolizja skrótu!)==
porównanie, a nieis
porównanie) wpisu w gnieździe z skrótem i kluczem bieżącego wpisu do wstawienia ( dictobject.c : 337,344-345 ) odpowiednio. Jeśli oba pasują, oznacza to, że dany wpis już istnieje, poddaje się i przechodzi do następnego wpisu, który ma zostać wstawiony. Jeśli skrót lub klucz się nie zgadzają, zaczyna się sondowanie .i+1, i+2, ...
i użyć pierwszego dostępnego (to jest sondowanie liniowe). Ale z powodów wyjaśnionych pięknie w komentarzach (patrz dictobject.c: 33-126 ), CPython używa losowego sondowania . W losowym badaniu kolejne miejsce jest wybierane w pseudolosowej kolejności. Wpis zostanie dodany do pierwszego pustego miejsca. W tej dyskusji faktyczny algorytm użyty do wybrania następnego gniazda nie jest tak naprawdę ważny (patrz algorytm sondowania na dictobject.c: 33-126 ). Ważne jest, aby gniazda były badane do momentu znalezienia pierwszego pustego gniazda.dict
rozmiar zostanie zmieniony, jeśli jest zapełniony w dwóch trzecich. Pozwala to uniknąć spowolnienia wyszukiwania. (patrz dictobject.h: 64-65 )UWAGA: Przeprowadziłem badania dotyczące implementacji Python Dict w odpowiedzi na moje własne pytanie dotyczące tego, jak wiele wpisów w dykcie może mieć takie same wartości skrótu. Opublikowałem tutaj nieco zredagowaną wersję odpowiedzi, ponieważ wszystkie badania są również bardzo istotne dla tego pytania.
źródło
Oto krótki kurs:
Uporządkowany aspekt jest nieoficjalny w Pythonie 3.6 (aby dać innym implementacjom szansę na dotrzymanie kroku), ale oficjalny w Pythonie 3.7 .
Słowniki Pythona to tabele skrótów
Przez długi czas działało dokładnie tak. Python wstępnie przydzieli 8 pustych wierszy i użyje skrótu, aby określić, gdzie należy przykleić parę klucz-wartość. Na przykład, jeśli skrót dla klucza zakończył się na 001, umieściłby go w indeksie 1 (tj. Drugim) (jak w przykładzie poniżej).
Każdy wiersz zajmuje 24 bajty w architekturze 64-bitowej, 12 w 32-bitowej. (Pamiętaj, że nagłówki kolumn są tutaj tylko etykietami do naszych celów - tak naprawdę nie istnieją w pamięci).
Jeśli skrót zakończy się tak samo jak skrót istniejącego klucza, jest to kolizja, a następnie umieściłby parę klucz-wartość w innym miejscu.
Po zapisaniu 5 kluczy-wartości podczas dodawania kolejnej pary klucz-wartość prawdopodobieństwo kolizji skrótu jest zbyt duże, więc rozmiar słownika jest podwojony. W procesie 64-bitowym przed zmianą rozmiaru mamy 72 bajty puste, a następnie marnujemy 240 bajtów z powodu 10 pustych wierszy.
Zajmuje to dużo miejsca, ale czas wyszukiwania jest dość stały. Algorytm porównywania kluczy polega na obliczeniu wartości skrótu, przejściu do oczekiwanej lokalizacji, porównaniu identyfikatora klucza - jeśli są one tym samym obiektem, są równe. Jeśli nie, to porównanie wartości hash, jeśli są nie takie same, nie są one równe. W przeciwnym razie w końcu porównujemy klucze dla równości, a jeśli są równe, zwróć wartość. Ostateczne porównanie w zakresie równości może być dość powolne, ale wcześniejsze kontrole zwykle skracają końcowe porównanie, dzięki czemu wyszukiwanie jest bardzo szybkie.
Kolizje spowalniają działanie, a atakujący mógłby teoretycznie użyć kolizji mieszających, aby przeprowadzić atak typu „odmowa usługi”, więc losowo zainicjowaliśmy funkcję skrótu, tak aby obliczała różne wartości skrótu dla każdego nowego procesu Pythona.
Opisana powyżej zmarnowana przestrzeń doprowadziła nas do modyfikacji implementacji słowników, z nową ekscytującą funkcją, że słowniki są teraz porządkowane przez wstawienie.
Nowe kompaktowe tabele skrótów
Zamiast tego zaczynamy od wstępnego przydzielenia tablicy dla indeksu wstawienia.
Ponieważ nasza pierwsza para klucz-wartość znajduje się w drugim gnieździe, indeksujemy w następujący sposób:
A nasza tabela jest zapełniana według kolejności wstawiania:
Kiedy więc szukamy klucza, używamy skrótu, aby sprawdzić oczekiwaną pozycję (w tym przypadku przechodzimy prosto do indeksu 1 tablicy), a następnie przechodzimy do tego indeksu w tablicy skrótów (np. Indeks 0 ), sprawdź, czy klucze są równe (używając tego samego algorytmu opisanego wcześniej), a jeśli tak, zwróć wartość.
Zachowujemy stały czas wyszukiwania, z niewielkimi stratami prędkości w niektórych przypadkach i zyskami w innych, z zaletami, które oszczędzamy sporo miejsca w stosunku do wcześniejszej implementacji i zachowujemy kolejność wstawiania. Jedyne zmarnowane miejsce to puste bajty w tablicy indeksu.
Raymond Hettinger wprowadził to na Python-dev w grudniu 2012 roku. W końcu dostał się do CPython w Python 3.6 . Kolejność wstawiania była uważana za szczegół implementacji 3.6, aby umożliwić innym implementacjom Pythona szansę nadrobienia zaległości.
Klucze wspólne
Kolejną optymalizacją w celu zaoszczędzenia miejsca jest implementacja dzieląca klucze. Dlatego zamiast redundantnych słowników, które zajmują całą tę przestrzeń, mamy słowniki, które ponownie wykorzystują wspólne klucze i skróty kluczy. Możesz myśleć o tym w ten sposób:
W przypadku komputera 64-bitowego może to zaoszczędzić do 16 bajtów na klucz na dodatkowy słownik.
Wspólne klucze dla niestandardowych obiektów i alternatyw
Te dyktaje z kluczem wspólnym są przeznaczone do użycia w obiektach niestandardowych ”
__dict__
. Aby uzyskać takie zachowanie, uważam, że musisz zakończyć__dict__
wypełnianie swojego pliku, zanim utworzysz kolejny obiekt ( patrz PEP 412 ). Oznacza to, że powinieneś przypisać wszystkie swoje atrybuty do__init__
lub__new__
, w przeciwnym razie możesz nie uzyskać oszczędności miejsca.Jeśli jednak znasz wszystkie swoje atrybuty w momencie ich
__init__
wykonania, możesz również podać__slots__
swój obiekt i zagwarantować, że__dict__
w ogóle nie zostanie utworzony (jeśli nie jest dostępny w rodzicach), lub nawet pozwolić,__dict__
ale zagwarantować, że twoje przewidywane atrybuty są i tak przechowywane w automatach. Aby uzyskać więcej informacji__slots__
, zobacz moją odpowiedź tutaj .Zobacz też:
**kwargs
funkcji.źródło
find_empty_slot
: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - a od linii 134 jest proza, która to opisuje.Słowniki Python używają otwartego adresowania ( odniesienie w pięknym kodzie )
NB! Otwartego adresowania , czyli zamkniętego haszowania, nie należy, jak zauważono w Wikipedii, nie mylić z jego przeciwstawnym otwartym haszowaniem!
Otwarte adresowanie oznacza, że dykt korzysta ze szczelin tablicowych, a gdy podstawowa pozycja obiektu jest przyjmowana w dyktonie, miejsce obiektu jest szukane pod innym indeksem w tej samej tablicy, przy użyciu schematu „perturbacji”, w którym wartość skrótu obiektu odgrywa rolę .
źródło