Jak jest implementowana lista Pythona?

183

Czy to jest połączona lista, tablica? Rozejrzałem się i znalazłem tylko zgadujących ludzi. Moja znajomość języka C nie jest wystarczająco dobra, aby spojrzeć na kod źródłowy.

Greg
źródło

Odpowiedzi:

58

To tablica dynamiczna . Praktyczny dowód: indeksowanie trwa (oczywiście z bardzo małymi różnicami (0,0013 µs!)) W tym samym czasie niezależnie od indeksu:

...>python -m timeit --setup="x = [None]*1000" "x[500]"
10000000 loops, best of 3: 0.0579 usec per loop

...>python -m timeit --setup="x = [None]*1000" "x[0]"
10000000 loops, best of 3: 0.0566 usec per loop

Byłbym zdumiony, gdyby IronPython lub Jython korzystały z list połączonych - zrujnowałyby one wydajność wielu wielu powszechnie używanych bibliotek zbudowanych przy założeniu, że listy są tablicami dynamicznymi.

user2357112 obsługuje Monikę
źródło
1
@Ralf: Wiem, że mój procesor (również większość innych urządzeń również) jest stary i powolny - z drugiej strony mogę założyć, że kod, który działa wystarczająco szybko dla mnie, jest wystarczająco szybki dla wszystkich użytkowników: D
88
@delnan: -1 Twój „praktyczny dowód” to nonsens, podobnie jak 6 głosów za. Zajmuje to około 98% czasu x=[None]*1000, przez co pomiar ewentualnych różnic w dostępie do listy jest raczej nieprecyzyjny. Musisz oddzielić inicjalizację:-s "x=[None]*100" "x[0]"
John Machin
26
Pokazuje, że nie jest to naiwna implementacja połączonej listy. Nie pokazuje definitywnie, że jest to tablica.
Michael Mior
6
Możesz o tym przeczytać tutaj: docs.python.org/2/faq/design.html#how-are-lists-implemented
CCoder
3
Struktur jest znacznie więcej niż tylko połączona lista i tablica, dlatego synchronizacja nie ma praktycznego zastosowania przy podejmowaniu decyzji między nimi.
Ross Hemsley,
236

W rzeczywistości kod C jest dość prosty. Rozszerzając jedno makro i przycinając nieistotne komentarze, znajduje się podstawowa struktura listobject.h, która definiuje listę jako:

typedef struct {
    PyObject_HEAD
    Py_ssize_t ob_size;

    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     */
    Py_ssize_t allocated;
} PyListObject;

PyObject_HEADzawiera liczbę odwołań i identyfikator typu. Jest to więc wektor / tablica z nadmierną alokacją. Kod służący do zmiany rozmiaru takiej tablicy, gdy jest pełna, znajduje się w listobject.c. W rzeczywistości nie podwaja tablicy, ale rośnie poprzez przydzielanie

new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
new_allocated += newsize;

do pojemności za każdym razem, gdzie newsizejest żądany rozmiar (niekoniecznie allocated + 1dlatego, że extendzamiast tego można podać dowolną liczbę elementówappend je pojedynczo).

Zobacz także Python FAQ .

Fred Foo
źródło
6
Tak więc, kiedy iteracja po listach Pythona jest tak powolna, jak listy połączone, ponieważ każdy wpis jest tylko wskaźnikiem, więc każdy element najprawdopodobniej spowodowałby brak pamięci podręcznej.
Kr0e
9
@ Kr0e: nie, jeśli kolejne elementy są w rzeczywistości tym samym obiektem :) Ale jeśli potrzebujesz mniejszych / bardziej przyjaznych pamięci podręcznej struktur danych, arraypreferowany jest moduł lub NumPy.
Fred Foo,
@ Kr0e Nie powiedziałbym, że iteracja po liście jest tak wolna, jak w przypadku list połączonych, ale iteracja po wartościach list połączonych jest powolna, jak w przypadku listy połączonej, z zastrzeżeniem, o którym wspomniał Fred. Na przykład iterowanie po liście w celu skopiowania jej na inną powinno być szybsze niż w przypadku listy połączonej.
Ganea Dan Andrei
35

W CPythonie listy są tablicami wskaźników. Inne implementacje Pythona mogą przechowywać je na różne sposoby.

Bursztyn
źródło
32

Jest to zależne od implementacji, ale IIRC:

  • CPython używa tablicy wskaźników
  • Jython używa rozszerzenia ArrayList
  • IronPython najwyraźniej również używa tablicy. Możesz przejrzeć kod źródłowy, aby się dowiedzieć.

W ten sposób wszyscy mają dostęp losowy O (1).

NullUserException
źródło
1
Implementacja zależna od interpretera Pythona, który zaimplementował listy jako listy połączone, byłaby prawidłową implementacją języka Python? Innymi słowy: O (1) swobodny dostęp do list nie jest gwarantowany? Czy to nie uniemożliwia pisania wydajnego kodu bez polegania na szczegółach implementacji?
wrzesień
2
@sepp Wierzę, że listy w Pythonie to tylko uporządkowane kolekcje; implementacja i / lub wymagania dotyczące wydajności wspomnianej implementacji nie są wyraźnie określone
NullUserException
6
@ sppe2k: Ponieważ Python tak naprawdę nie ma standardowej ani formalnej specyfikacji (chociaż w niektórych dokumentach jest napisane „… gwarantujemy…”), nie można być w 100% pewnym, jak w „tym gwarantuje jakiś kawałek papieru ”. Ale ponieważ O(1)indeksowanie list jest dość powszechnym i słusznym założeniem, żadna implementacja nie odważyłaby się go złamać.
@Paul Nic nie mówi o tym, jak powinno być wykonane podstawowe wdrożenie list.
NullUserException
Po prostu nie zdarza się, aby określić duży czas działania rzeczy. Specyfikacja składni języka niekoniecznie oznacza to samo, co szczegóły implementacji, po prostu często tak jest.
Paul McMillan
26

Proponuję artykuł Laurenta Luce "Implementacja listy Pythona" . Było dla mnie bardzo przydatne, ponieważ autor wyjaśnia, w jaki sposób lista jest zaimplementowana w CPythonie i używa do tego doskonałych diagramów.

Lista struktury obiektu C.

Obiekt listy w CPythonie jest reprezentowany przez następującą strukturę C. ob_itemto lista wskaźników do elementów listy. alokowane to liczba gniazd przydzielonych w pamięci.

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

Należy zwrócić uwagę na różnicę między przydzielonymi gniazdami a rozmiarem listy. Rozmiar listy jest taki sam, jak len(l). Liczba przydzielonych gniazd to liczba przydzielona w pamięci. Często zobaczysz, że przydzielone może być większe niż rozmiar. Ma to na celu uniknięcie konieczności wywoływania za reallockażdym razem, gdy do listy dodawany jest nowy element.

...

Dodać

Dodajemy liczbę całkowitą do listy: l.append(1). Co się dzieje?
wprowadź opis obrazu tutaj

Nadal dodając jeden element: l.append(2). list_resizejest wywoływana z n + 1 = 2, ale ponieważ przydzielony rozmiar wynosi 4, nie ma potrzeby przydzielania większej ilości pamięci. To samo dzieje się, gdy dodamy jeszcze 2 liczby całkowite: l.append(3), l.append(4). Poniższy diagram pokazuje, co mamy do tej pory.

wprowadź opis obrazu tutaj

...

Wstawić

Wstawmy nową liczbę całkowitą (5) na pozycję 1: l.insert(1,5)i spójrzmy, co dzieje się wewnętrznie.wprowadź opis obrazu tutaj

...

Muzyka pop

Kiedy zdejmiesz ostatni element l.pop(), listpop()zostanie wywołany:. list_resizejest wywoływana wewnątrz listpop()i jeśli nowy rozmiar jest mniejszy niż połowa przydzielonego rozmiaru, lista jest zmniejszana.wprowadź opis obrazu tutaj

Możesz zauważyć, że slot 4 nadal wskazuje na liczbę całkowitą, ale ważną rzeczą jest rozmiar listy, która wynosi teraz 4. Wybierzmy jeszcze jeden element. W programie list_resize()rozmiar - 1 = 4 - 1 = 3 to mniej niż połowa przydzielonych miejsc, więc lista została zmniejszona do 6, a nowy rozmiar listy wynosi teraz 3.

Możesz zauważyć, że miejsca 3 i 4 nadal wskazują na niektóre liczby całkowite, ale ważną rzeczą jest rozmiar listy, która wynosi teraz 3.wprowadź opis obrazu tutaj

...

Usuń Python lista obiekt ma sposobu, aby usunąć element specyficzny: l.remove(5).wprowadź opis obrazu tutaj

Lesya
źródło
Dzięki, teraz lepiej rozumiem link do części listy. Lista Pythona to aggregationnie composition. Żałuję, że nie ma też listy kompozycji.
shuva
22

Zgodnie z dokumentacją ,

Listy Pythona są w rzeczywistości tablicami o zmiennej długości, a nie listami połączonymi w stylu Lisp.

ravi77o
źródło
5

Jak powiedzieli inni powyżej, listy (gdy są znacznie duże) są implementowane przez przydzielenie określonej ilości miejsca i, jeśli to miejsce powinno się wypełnić, przydzielenie większej ilości miejsca i skopiowanie elementów.

Aby zrozumieć, dlaczego metoda jest amortyzowana O (1), bez utraty ogólności, załóżmy, że wstawiliśmy a = 2 ^ n elementów, a teraz musimy podwoić naszą tabelę do rozmiaru 2 ^ (n + 1). Oznacza to, że obecnie wykonujemy 2 ^ (n + 1) operacje. W ostatniej kopii wykonaliśmy 2 ^ n operacji. Wcześniej zrobiliśmy 2 ^ (n-1) ... aż do 8,4,2,1. Teraz, jeśli dodamy to, otrzymamy 1 + 2 + 4 + 8 + ... + 2 ^ (n + 1) = 2 ^ (n + 2) - 1 <4 * 2 ^ n = O (2 ^ n) = O (a) suma wpłat (tj. O (1) amortyzowany czas). Należy również zauważyć, że jeśli tabela umożliwia usuwanie, zmniejszanie tabeli musi być wykonane przy innym współczynniku (np. 3x)

RussellStewart
źródło
O ile rozumiem, nie ma kopiowania starszych elementów. Przydzielane jest więcej miejsca, ale nowe miejsce nie sąsiaduje z przestrzenią już używaną i tylko nowsze elementy do wstawienia są kopiowane do nowej przestrzeni. Proszę, popraw mnie jeśli się mylę.
Tushar Vazirani
1

Lista w Pythonie jest czymś w rodzaju tablicy, w której można przechowywać wiele wartości. Lista jest zmienna, co oznacza, że ​​możesz ją zmienić. Co ważniejsze, powinieneś wiedzieć, kiedy tworzymy listę, Python automatycznie tworzy reference_id dla tej zmiennej listy. Jeśli zmienisz to poprzez przypisanie innym zmiennej, lista główna ulegnie zmianie. Spróbujmy na przykładzie:

list_one = [1,2,3,4]

my_list = list_one

#my_list: [1,2,3,4]

my_list.append("new")

#my_list: [1,2,3,4,'new']
#list_one: [1,2,3,4,'new']

Dodajemy, my_listale nasza główna lista uległa zmianie. Oznacza to, że lista środków nie została przypisana jako lista kopii przypisana jako odniesienie.

hasib
źródło
0

W CPythonie lista jest zaimplementowana jako tablica dynamiczna, a więc kiedy dodajemy w tym czasie nie tylko dodawane jest jedno makro, ale przydzielane jest trochę więcej miejsca, aby za każdym razem nie dodawać nowego miejsca.

gaurav
źródło