Un Tuple
prend moins d'espace mémoire en Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
alors que list
s prend plus d’espace mémoire:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Que se passe-t-il en interne sur la gestion de la mémoire Python?
Je suppose que vous utilisez CPython et 64 bits (les mêmes résultats sont obtenus avec mon CPython 2.7 64 bits). Il pourrait y avoir des différences entre d’autres implémentations Python ou avec un Python 32 bits.
Quelle que soit l'implémentation, les list
sont de taille variable, tandis que Tuple
s sont de taille fixe.
Donc, Tuple
s peut stocker les éléments directement à l'intérieur de la structure, les listes ont par contre besoin d'une couche d'indirection (elle stocke un pointeur sur les éléments). Cette couche d'indirection est un pointeur, sur les systèmes 64 bits, 64 bits, donc 8 octets.
Mais list
s fait autre chose: ils sur-attribuent. Sinon _list.append
_ serait une opération O(n)
toujours - pour l'amortir O(1)
(beaucoup plus rapidement !!! ) il sur-alloue. Mais maintenant, il doit garder trace de la taille allouée et du rempli taille (Tuple
s n'a besoin de stocker qu'une taille, car les tailles allouées et remplies sont toujours identiques). Cela signifie que chaque liste doit stocker une autre "taille" qui, sur les systèmes 64 bits, est un entier 64 bits, à nouveau 8 octets.
Donc, list
s a besoin d'au moins 16 octets de mémoire de plus que Tuple
s. Pourquoi ai-je dit "au moins"? En raison de la surallocation. Une surallocation signifie qu'il alloue plus d'espace que nécessaire. Toutefois, le montant de la surallocation dépend de la manière dont vous créez la liste et de l'historique des ajouts/suppressions:
_>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4) # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96
>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1) # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2) # no re-alloc
>>> l.append(3) # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4) # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72
_
J'ai décidé de créer des images pour accompagner l'explication ci-dessus. Peut-être que ce sont utiles
Voici comment (schématiquement) il est stocké en mémoire dans votre exemple. J'ai souligné les différences avec les cycles rouges (à main levée):
Ce n'est en fait qu'une approximation, car les objets int
sont également des objets Python et CPython réutilise même de petits entiers. Par conséquent, une représentation probablement plus précise (même si elle n'est pas aussi lisible) des objets en mémoire est la suivante:
Liens utiles:
Tuple
struct dans le référentiel CPython pour Python 2.7list
struct dans le référentiel CPython pour Python 2.7int
struct dans le référentiel CPython pour Python 2.7Notez que ___sizeof__
_ ne renvoie pas vraiment la taille "correcte"! Il ne renvoie que la taille des valeurs stockées. Cependant, lorsque vous utilisez sys.getsizeof
, le résultat est différent:
_>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
_
Il y a 24 octets "extra". Ce sont réels , c'est la surcharge du récupérateur de place qui n'est pas prise en compte dans la méthode ___sizeof__
_. C’est parce que vous n’êtes généralement pas censé utiliser directement les méthodes magiques - utilisez les fonctions qui savent les manipuler, dans ce cas: sys.getsizeof
(ce qui en fait ajoute le GC overhead à la valeur renvoyée par ___sizeof__
_).
Je vais plonger plus profondément dans la base de code CPython afin de voir comment les tailles sont réellement calculées. Dans votre exemple spécifique , aucune sur-allocation n'a été effectuée, je ne vais donc pas aborder ce sujet .
Je vais utiliser les valeurs 64 bits ici, comme vous l'êtes.
La taille de list
s est calculée à partir de la fonction suivante, list_sizeof
:
_static PyObject *
list_sizeof(PyListObject *self)
{
Py_ssize_t res;
res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
return PyInt_FromSsize_t(res);
}
_
Ici Py_TYPE(self)
est une macro qui saisit le _ob_type
_ de self
(renvoyant _PyList_Type
_) tandis que __PyObject_SIZE
_ est une autre macro qui saisit tp_basicsize
de ce type. _tp_basicsize
_ est calculé comme suit: sizeof(PyListObject)
où PyListObject
est la structure de l'instance.
La structure PyListObject
comporte trois champs:
_PyObject_VAR_HEAD # 24 bytes
PyObject **ob_item; # 8 bytes
Py_ssize_t allocated; # 8 bytes
_
ceux-ci ont des commentaires (que j'ai coupés) expliquant ce qu'ils sont, suivez le lien ci-dessus pour les lire. PyObject_VAR_HEAD
se développe en trois champs de 8 octets (_ob_refcount
_, _ob_type
_ et _ob_size
_) donc une contribution de _24
_.
Donc pour l'instant res
c'est:
_sizeof(PyListObject) + self->allocated * sizeof(void*)
_
ou:
_40 + self->allocated * sizeof(void*)
_
Si l'instance de liste a des éléments qui sont alloués. la deuxième partie calcule leur contribution. _self->allocated
_, comme son nom l’indique, contient le nombre d’éléments attribués.
Sans aucun élément, la taille des listes est calculée comme suit:
_>>> [].__sizeof__()
40
_
c'est-à-dire la taille de la structure d'instance.
Les objets Tuple
ne définissent pas de fonction _Tuple_sizeof
_. Au lieu de cela, ils utilisent object_sizeof
pour calculer leur taille:
_static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
Py_ssize_t res, isize;
res = 0;
isize = self->ob_type->tp_itemsize;
if (isize > 0)
res = Py_SIZE(self) * isize;
res += self->ob_type->tp_basicsize;
return PyInt_FromSsize_t(res);
}
_
Ceci, comme pour list
s, saisit le _tp_basicsize
_ et, si l'objet a un _tp_itemsize
_ différent de zéro (ce qui signifie qu'il a des instances de longueur variable), il multiplie le nombre d'éléments dans le Tuple (qu'il obtient via Py_SIZE
) avec _tp_itemsize
_.
_tp_basicsize
_ utilise à nouveau sizeof(PyTupleObject)
où la structure PyTupleObject
contient :
_PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
_
Donc, sans aucun élément (c'est-à-dire _Py_SIZE
_ renvoie _0
_), la taille des n-uplets vides est égale à sizeof(PyTupleObject)
:
_>>> ().__sizeof__()
24
_
hein? Eh bien, voici une particularité pour laquelle je n'ai pas trouvé d'explication: le _tp_basicsize
_ de Tuple
s est en fait calculé comme suit:
_sizeof(PyTupleObject) - sizeof(PyObject *)
_
pourquoi un _8
_ octets supplémentaire est supprimé de _tp_basicsize
_ est quelque chose que je n'ai pas pu trouver. (Voir le commentaire de MSeifert pour une explication possible)
Mais, c’est fondamentalement la différence dans votre exemple spécifique . list
s conserve également un certain nombre d’éléments alloués, ce qui aide à déterminer le moment opportun pour effectuer une nouvelle surallocation.
Désormais, lorsque des éléments supplémentaires sont ajoutés, les listes effectuent effectivement cette sur-allocation afin d’obtenir O(1) addends. Il en résulte des tailles plus grandes que celles de MSeifert dans sa réponse.
La réponse de MSeifert le couvre au sens large; pour rester simple, vous pouvez penser à:
Tuple
est immuable. Une fois qu'il est défini, vous ne pouvez pas le changer. Vous savez donc à l'avance combien de mémoire vous devez allouer pour cet objet.
list
est modifiable. Vous pouvez ajouter ou supprimer des éléments. Il doit en connaître la taille (pour les impl. Internes). Il redimensionne au besoin.
Il n'y a pas de repas gratuits - ces fonctionnalités ont un coût. D'où la surcharge en mémoire pour les listes.
La taille du tuple est préfixée, ce qui signifie qu’à l’initialisation du tuple, l’interprète alloue suffisamment d’espace aux données contenues, ce qui en fait un élément immuable (non modifiable), alors qu’une liste est un objet modifiable, ce qui implique une dynamique. allocation de mémoire, afin d'éviter d'allouer de l'espace chaque fois que vous ajoutez ou modifiez la liste (allouez suffisamment d'espace pour contenir les données modifiées et copiez les données dessus), il alloue de l'espace supplémentaire pour les ajouts, les modifications, etc. résume.