Comment la liste de Python est-elle implémentée?

183

Est-ce une liste chaînée, un tableau? J'ai cherché partout et je n'ai trouvé que des gens qui devinaient. Mes connaissances en C ne sont pas assez bonnes pour regarder le code source.

Greg
la source

Réponses:

58

C'est un tableau dynamique . Preuve pratique: l'indexation prend (bien sûr avec des différences extrêmement faibles (0,0013 µsecs!)) Le même temps quel que soit l'indice:

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

Je serais étonné si IronPython ou Jython utilisaient des listes chaînées - elles ruineraient les performances de nombreuses bibliothèques largement utilisées construites sur l'hypothèse que les listes sont des tableaux dynamiques.

user2357112 prend en charge Monica
la source
1
@Ralf: Je sais que mon processeur (la plupart des autres matériels aussi, d'ailleurs) est vieux et lent - du bon côté, je peux supposer que le code qui fonctionne assez vite pour moi est assez rapide pour tous les utilisateurs: D
88
@delnan: -1 Votre "preuve pratique" est un non-sens, tout comme les 6 votes positifs. Environ 98% du temps est pris à faire x=[None]*1000, laissant la mesure de toute différence d'accès à la liste plutôt imprécise. Vous devez séparer l'initialisation:-s "x=[None]*100" "x[0]"
John Machin
26
Montre qu'il ne s'agit pas d'une implémentation naïve d'une liste chaînée. Ne montre pas définitivement que c'est un tableau.
Michael Mior
6
Vous pouvez en savoir plus ici: docs.python.org/2/faq/design.html#how-are-lists-implemented
CCoder
3
Il y a bien plus de structures que de simples listes et tableaux chaînés, le timing n'est d'aucune utilité pratique pour décider entre eux.
Ross Hemsley
236

Le code C est assez simple, en fait. En développant une macro et en élaguant certains commentaires non pertinents, la structure de base est dans listobject.h, qui définit une liste comme:

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_HEADcontient un nombre de références et un identificateur de type. Donc, c'est un vecteur / tableau qui surutilisent. Le code pour redimensionner un tel tableau lorsqu'il est plein est dans listobject.c. Il ne double pas réellement le tableau, mais augmente en allouant

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

à la capacité à chaque fois, où newsizeest la taille demandée (pas nécessairement allocated + 1parce que vous pouvez extendpar un nombre arbitraire d'éléments au lieu deappend 'inger un par un).

Voir aussi la FAQ Python .

Fred Foo
la source
6
Ainsi, lors de l'itération sur des listes python, c'est aussi lent que des listes liées, car chaque entrée n'est qu'un pointeur, donc chaque élément entraînerait probablement un échec du cache.
Kr0e
9
@ Kr0e: pas si les éléments suivants sont en fait le même objet :) Mais si vous avez besoin de structures de données plus petites / plus compatibles avec le cache, le arraymodule ou NumPy sont à privilégier.
Fred Foo
@ Kr0e Je ne dirais pas que l'itération sur la liste est aussi lente que les listes liées, mais que l'itération sur les valeurs des listes liées est lente comme une liste chaînée, avec la mise en garde mentionnée par Fred. Par exemple, parcourir une liste pour la copier dans une autre devrait être plus rapide qu'une liste liée.
Ganea Dan Andrei
35

En CPython, les listes sont des tableaux de pointeurs. D'autres implémentations de Python peuvent choisir de les stocker de différentes manières.

ambre
la source
32

Cela dépend de l'implémentation, mais l'IIRC:

  • CPython utilise un tableau de pointeurs
  • Jython utilise un ArrayList
  • IronPython utilise apparemment également un tableau. Vous pouvez parcourir le code source pour le découvrir.

Ainsi, ils ont tous un accès aléatoire O (1).

NullUserException
la source
1
Implémentation dépendante comme dans un interpréteur python qui implémentait des listes sous forme de listes chaînées serait une implémentation valide du langage python? En d'autres termes: O (1) l'accès aléatoire aux listes n'est-il pas garanti? Cela ne rend-il pas impossible d'écrire un code efficace sans se fier aux détails de mise en œuvre?
sepp2k
2
@sepp Je crois que les listes en Python ne sont que des collections ordonnées; la mise en œuvre et / ou les exigences de performance de ladite implémentation ne sont pas explicitement énoncées
NullUserException
6
@ sppe2k: Puisque Python n'a pas vraiment de spécification standard ou formelle (bien qu'il y ait des documents qui disent "... est garanti à ..."), vous ne pouvez pas être sûr à 100% comme dans "ceci est garanti par un morceau de papier ". Mais comme l' O(1)indexation des listes est une hypothèse assez courante et valide, aucune implémentation n'oserait la casser.
@Paul Il ne dit rien sur la façon dont la mise en œuvre sous-jacente des listes doit être effectuée.
NullUserException
Il n'arrive tout simplement pas à spécifier le grand temps de fonctionnement O des choses. La spécification de la syntaxe du langage ne signifie pas nécessairement la même chose que les détails de l'implémentation, c'est souvent le cas.
Paul McMillan
26

Je suggérerais l'article de Laurent Luce "Implémentation de liste Python" . Cela m'a vraiment été utile car l'auteur explique comment la liste est implémentée en CPython et utilise d'excellents diagrammes à cet effet.

Structure de l'objet C de liste

Un objet de liste en CPython est représenté par la structure C suivante. ob_itemest une liste de pointeurs vers les éléments de la liste. alloué est le nombre d'emplacements alloués en mémoire.

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

Il est important de noter la différence entre les emplacements alloués et la taille de la liste. La taille d'une liste est la même que len(l). Le nombre d'emplacements alloués correspond à ce qui a été alloué en mémoire. Souvent, vous verrez que la taille allouée peut être supérieure à la taille. Cela évite d'avoir à appeler reallocchaque fois qu'un nouvel élément est ajouté à la liste.

...

Ajouter

Nous ajoutons un entier à la liste: l.append(1). Ce qui se produit?
entrez la description de l'image ici

Nous continuons en ajoutant un élément de plus: l.append(2). list_resizeest appelée avec n + 1 = 2 mais comme la taille allouée est 4, il n'est pas nécessaire d'allouer plus de mémoire. La même chose se produit lorsque nous ajoutons 2 entiers supplémentaires: l.append(3), l.append(4). Le diagramme suivant montre ce que nous avons jusqu'à présent.

entrez la description de l'image ici

...

Insérer

Insérons un nouvel entier (5) à la position 1: l.insert(1,5)et regardons ce qui se passe en interne.entrez la description de l'image ici

...

Pop

Quand vous ouvrez le dernier élément: l.pop(), listpop()est appelé. list_resizeest appelée à l'intérieur listpop()et si la nouvelle taille est inférieure à la moitié de la taille allouée, la liste est réduite.entrez la description de l'image ici

Vous pouvez observer que l'emplacement 4 pointe toujours vers l'entier, mais l'important est la taille de la liste qui est maintenant de 4. Explorons un élément de plus. Dans list_resize(), size - 1 = 4 - 1 = 3 est inférieur à la moitié des emplacements alloués, donc la liste est réduite à 6 emplacements et la nouvelle taille de la liste est maintenant de 3.

Vous pouvez observer que les emplacements 3 et 4 pointent toujours vers des nombres entiers mais l'important est la taille de la liste qui est maintenant 3.entrez la description de l'image ici

...

Retirer objet de liste Python a une méthode pour supprimer un élément spécifique: l.remove(5).entrez la description de l'image ici

Lesya
la source
Merci, je comprends plus maintenant la partie lien de la liste. La liste Python est un aggregation, pas composition. J'aimerais qu'il y ait aussi une liste de composition.
shuva
22

Selon la documentation ,

Les listes de Python sont en fait des tableaux de longueur variable, pas des listes liées de style Lisp.

ravi77o
la source
5

Comme d'autres l'ont indiqué ci-dessus, les listes (lorsqu'elles sont sensiblement grandes) sont mises en œuvre en allouant une quantité fixe d'espace et, si cet espace doit se remplir, en allouant une plus grande quantité d'espace et en copiant les éléments.

Pour comprendre pourquoi la méthode est O (1) amortie, sans perte de généralité, supposons que nous ayons inséré a = 2 ^ n éléments, et nous devons maintenant doubler notre tableau à 2 ^ (n + 1). Cela signifie que nous faisons actuellement 2 ^ (n + 1) opérations. Dernière copie, nous avons fait 2 ^ n opérations. Avant cela, nous avons fait 2 ^ (n-1) ... jusqu'à 8,4,2,1. Maintenant, si nous additionnons ces derniers, nous obtenons 1 + 2 + 4 + 8 + ... + 2 ^ (n + 1) = 2 ^ (n + 2) - 1 <4 * 2 ^ n = O (2 ^ n) = O (a) insertions totales (c'est-à-dire O (1) temps amorti). De plus, il convient de noter que si la table permet les suppressions, la réduction de la table doit être effectuée à un facteur différent (par exemple 3x)

RussellStewart
la source
Autant que je sache, il n'y a pas de copie d'éléments plus anciens. Plus d'espace est alloué, mais le nouvel espace n'est pas contigu à l'espace déjà utilisé, et seuls les nouveaux éléments à insérer sont copiés dans le nouvel espace. S'il vous plait corrigez moi si je me trompe.
Tushar Vazirani le
1

Une liste en Python est quelque chose comme un tableau, dans lequel vous pouvez stocker plusieurs valeurs. La liste est modifiable, ce qui signifie que vous pouvez la modifier. La chose la plus importante que vous devez savoir, lorsque nous créons une liste, Python crée automatiquement un reference_id pour cette variable de liste. Si vous le modifiez en affectant d'autres variables, la liste principale sera modifiée. Essayons avec un exemple:

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

Nous ajoutons my_listmais notre liste principale a changé. La liste de ce moyen n'a pas été assignée comme une liste de copie assignée comme référence.

hasib
la source
0

Dans CPython, la liste est implémentée en tant que tableau dynamique, et par conséquent, lorsque nous ajoutons à ce moment-là, non seulement une macro est ajoutée, mais un peu plus d'espace est alloué afin que chaque fois un nouvel espace ne soit pas ajouté.

gaurav
la source