Apparemment, list(a)
ne sur-utilise pas, sur- [x for x in a]
utilise à certains moments et sur- [*a]
utilise tout le temps ?
Voici les tailles n de 0 à 12 et les tailles résultantes en octets pour les trois méthodes:
0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208
Calculé comme ceci, reproductible sur repl.it , en utilisant Python 3. 8 :
from sys import getsizeof
for n in range(13):
a = [None] * n
print(n, getsizeof(list(a)),
getsizeof([x for x in a]),
getsizeof([*a]))
Donc comment ça fonctionne? Comment [*a]
surutilisation? En fait, quel mécanisme utilise-t-il pour créer la liste des résultats à partir de l'entrée donnée? Utilise-t-il un itérateur a
et utilise-t-il quelque chose comme list.append
? Où est le code source?
( Colab avec les données et le code qui ont produit les images.)
Zoom avant sur n plus petit:
Zoom arrière sur un n plus grand:
python
python-3.x
list
cpython
python-internals
Stefan Pochmann
la source
la source
[*a]
semble se comporter comme une utilisationextend
sur une liste vide.list(a)
fonctionne entièrement en C; il peut allouer le tampon interne nœud par nœud au fur et à mesure de son itérationa
.[x for x in a]
utilise justeLIST_APPEND
beaucoup, donc il suit le modèle normal "surallouer un peu, réallouer si nécessaire" d'une liste normale.[*a]
utiliseBUILD_LIST_UNPACK
, qui ... je ne sais pas ce que cela fait, à part apparemmentlist(a)
et[*a]
soient identiques, et les deux surutilisent par rapport à[x for x in a]
, donc ...sys.getsizeof
pourrait ne pas être le bon outil à utiliser ici.sys.getsizeof
c'est le bon outil, il montre juste quelist(a)
utilisé pour sur-utiliser. En fait, Quoi de neuf en Python 3.8 le mentionne: "Le constructeur de liste ne sur-utilise pas [...]" .Réponses:
[*a]
fait en interne l'équivalent C de :list
newlist.extend(a)
list
.Donc, si vous étendez votre test à:
Essayez-le en ligne!
vous verrez les résultats pour
getsizeof([*a])
etl = []; l.extend(a); getsizeof(l)
sont les mêmes.C'est généralement la bonne chose à faire; lorsque
extend
vous vous attendez généralement à en ajouter plus tard, et de même pour le déballage généralisé, il est supposé que plusieurs éléments seront ajoutés les uns après les autres.[*a]
n'est pas le cas normal; Python suppose qu'il y a plusieurs éléments ou itérables ajoutés aulist
([*a, b, c, *d]
), donc la surutilisation économise du travail dans le cas commun.En revanche, un
list
construit à partir d'un seul, itérable prédéfini (aveclist()
) peut ne pas croître ou rétrécir pendant l'utilisation, et la surutilisation est prématurée jusqu'à preuve du contraire; Python a récemment corrigé un bogue qui faisait que le constructeur surutilisait même pour les entrées de taille connue .En ce qui concerne les
list
compréhensions, elles sont effectivement équivalentes à desappend
s répétés , vous voyez donc le résultat final du modèle de croissance de surutilisation normal lors de l'ajout d'un élément à la fois.Pour être clair, rien de tout cela n'est une garantie de langue. C'est juste comment CPython l'implémente. La spécification du langage Python n'est généralement pas concernée par les schémas de croissance spécifiques de
list
(en plus de garantir lesO(1)
append
s etpop
s amortis de la fin). Comme indiqué dans les commentaires, la mise en œuvre spécifique change à nouveau en 3.9; bien que cela n'affecte pas[*a]
, cela pourrait affecter d'autres cas où ce qui était "créer un temporairetuple
d'éléments individuels puisextend
avec letuple
" devient maintenant plusieurs applications deLIST_APPEND
, qui peuvent changer lorsque la surutilisation se produit et quels nombres entrent dans le calcul.la source
BUILD_LIST_UNPACK
, il utilise_PyList_Extend
comme l'équivalent C de l'appelextend
(juste directement, plutôt que par recherche de méthode). Ils l'ont combiné avec les chemins pour construire untuple
avec déballage;tuple
s ne surutilisent pas bien pour la construction fragmentaire, donc ils décompressent toujours vers unlist
(pour bénéficier de la surutilisation) et se convertissenttuple
à la fin quand c'est ce qui a été demandé.BUILD_LIST
,LIST_EXTEND
pour chaque chose à décompresser,LIST_APPEND
pour des éléments uniques), au lieu de tout charger sur la pile avant de construire le toutlist
avec une instruction de code à un octet (cela permet compilateur pour effectuer des optimisations que l'instruction tout-en-un n'a pas permis, comme la mise en œuvre[*a, b, *c]
commeLIST_EXTEND
,LIST_APPEND
,LIST_EXTEND
w / o avoir besoin d'envelopperb
dans un de untuple
pour répondre aux exigences deBUILD_LIST_UNPACK
).Image complète de ce qui se passe, en s'appuyant sur les autres réponses et commentaires (en particulier la réponse de ShadowRanger , qui explique également pourquoi c'est fait comme ça).
Démontage montre qui
BUILD_LIST_UNPACK
s'utilise:Cela est géré dans
ceval.c
, qui crée une liste vide et l'étend (aveca
):_PyList_Extend
utiliselist_extend
:Qui appelle
list_resize
avec la somme des tailles :Et cela sur- utilise comme suit:
Vérifions cela. Calculez le nombre attendu de spots avec la formule ci-dessus et calculez la taille d'octet attendue en le multipliant par 8 (comme j'utilise ici Python 64 bits) et en ajoutant la taille d'octet d'une liste vide (c'est-à-dire la surcharge constante d'un objet de liste) :
Production:
Correspond à l'exception de
n = 0
, qui enlist_extend
fait des raccourcis , donc en fait cela correspond aussi:la source
Ce seront des détails d'implémentation de l'interpréteur CPython, et peuvent donc ne pas être cohérents entre les autres interprètes.
Cela dit, vous pouvez voir où la compréhension et les
list(a)
comportements entrent en jeu ici:https://github.com/python/cpython/blob/master/Objects/listobject.c#L36
Spécifiquement pour la compréhension:
Juste en dessous de ces lignes, il y en a
list_preallocate_exact
qui est utilisé lors de l'appellist(a)
.la source
[*a]
n'ajoute pas les éléments individuels un par un. Il a son propre bytecode dédié, qui effectue l'insertion en masse viaextend
.[*a]