Comment déterminer la taille d'un objet en Python?
La réponse, "Il suffit d'utiliser sys.getsizeof" n'est pas une réponse complète.
Cette réponse fait le travail pour les objets directement builtin, mais il ne tient pas compte de ce que ces objets peuvent contenir, en particulier, quels types, tels que des objets personnalisés, tuples, listes, dicts et ensembles contiennent. Ils peuvent contenir des instances entre elles, ainsi que des nombres, des chaînes et d'autres objets.
Une réponse plus complète
À l'aide de Python 3.6 64 bits de la distribution Anaconda, avec sys.getsizeof, j'ai déterminé la taille minimale des objets suivants, et notez que les ensembles et les images préallouent de sorte que les espaces vides ne se développent plus qu'après une quantité définie (ce qui peut varient selon la mise en œuvre de la langue):
Python 3:
Empty
Bytes type scaling notes
28 int +4 bytes about every 30 powers of 2
37 bytes +1 byte per additional byte
49 str +1-4 per additional character (depending on max width)
48 tuple +8 per additional item
64 list +8 for each additional
224 set 5th increases to 736; 21nd, 2272; 85th, 8416; 341, 32992
240 dict 6th increases to 368; 22nd, 1184; 43rd, 2280; 86th, 4704; 171st, 9320
136 func def does not include default args and other attrs
1056 class def no slots
56 class inst has a __dict__ attr, same scaling as dict above
888 class def with slots
16 __slots__ seems to store in mutable tuple-like structure
first slot grows to 48, and so on.
Comment interprétez-vous cela? Disons que vous avez un ensemble de 10 articles. Si chaque élément fait 100 octets chacun, quelle est la taille de la structure de données entière? L'ensemble est 736 lui-même car il a dimensionné une fois à 736 octets. Ensuite, vous ajoutez la taille des éléments, ce qui fait 1736 octets au total
Quelques mises en garde pour les définitions de fonction et de classe:
Notez que chaque définition de classe a une __dict__
structure proxy (48 octets) pour les attr de classe. Chaque emplacement a un descripteur (comme a property
) dans la définition de classe.
Les instances fendues commencent avec 48 octets sur leur premier élément et augmentent de 8 chacune supplémentaires. Seuls les objets fendus vides ont 16 octets, et une instance sans données n'a que très peu de sens.
De plus, chaque définition de fonction a des objets de code, peut-être des docstrings et d'autres attributs possibles, même a __dict__
.
Notez également que nous utilisons sys.getsizeof()
parce que nous nous soucions de l'utilisation de l'espace marginal, qui inclut la surcharge de la récupération de place pour l'objet, à partir des documents :
getsizeof () appelle la __sizeof__
méthode de l'objet et ajoute une surcharge supplémentaire au garbage collector si l'objet est géré par le garbage collector.
Notez également que le redimensionnement des listes (par exemple en les ajoutant de manière répétitive) les oblige à préallouer de l'espace, de la même manière que les ensembles et les dict. À partir du code source listobj.c :
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
* Note: new_allocated won't overflow because the largest possible value
* is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
*/
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
Données historiques
Analyse Python 2.7, confirmée par guppy.hpy
et sys.getsizeof
:
Bytes type empty + scaling notes
24 int NA
28 long NA
37 str + 1 byte per additional character
52 unicode + 4 bytes per additional character
56 tuple + 8 bytes per additional item
72 list + 32 for first, 8 for each additional
232 set sixth item increases to 744; 22nd, 2280; 86th, 8424
280 dict sixth item increases to 1048; 22nd, 3352; 86th, 12568 *
120 func def does not include default args and other attrs
64 class inst has a __dict__ attr, same scaling as dict above
16 __slots__ class with slots has no dict, seems to store in
mutable tuple-like structure.
904 class def has a proxy __dict__ structure for class attrs
104 old class makes sense, less stuff, has real dict though.
Notez que les dictionnaires ( mais pas les ensembles ) ont une représentation plus compacte en Python 3.6
Je pense que 8 octets par élément supplémentaire à référencer ont beaucoup de sens sur une machine 64 bits. Ces 8 octets indiquent l'endroit en mémoire où se trouve l'élément contenu. Les 4 octets sont à largeur fixe pour unicode en Python 2, si je me souviens bien, mais en Python 3, str devient un unicode de largeur égale à la largeur maximale des caractères.
(Et pour en savoir plus sur les machines à sous, voir cette réponse )
Une fonction plus complète
Nous voulons une fonction qui recherche les éléments dans les listes, les tuples, les ensembles, les dict, obj.__dict__
les, et obj.__slots__
, ainsi que d'autres choses auxquelles nous n'avons peut-être pas encore pensé.
Nous voulons nous fier gc.get_referents
à cette recherche car elle fonctionne au niveau C (ce qui la rend très rapide). L'inconvénient est que get_referents peut renvoyer des membres redondants, nous devons donc nous assurer de ne pas compter deux fois.
Les classes, modules et fonctions sont des singletons - ils existent une fois dans la mémoire. Nous ne sommes pas tellement intéressés par leur taille, car nous ne pouvons pas faire grand-chose à leur sujet - ils font partie du programme. Nous éviterons donc de les compter s'ils sont référencés.
Nous allons utiliser une liste noire de types afin de ne pas inclure l'intégralité du programme dans notre nombre de tailles.
import sys
from types import ModuleType, FunctionType
from gc import get_referents
# Custom objects know their class.
# Function objects seem to know way too much, including modules.
# Exclude modules as well.
BLACKLIST = type, ModuleType, FunctionType
def getsize(obj):
"""sum size of object & members."""
if isinstance(obj, BLACKLIST):
raise TypeError('getsize() does not take argument of type: '+ str(type(obj)))
seen_ids = set()
size = 0
objects = [obj]
while objects:
need_referents = []
for obj in objects:
if not isinstance(obj, BLACKLIST) and id(obj) not in seen_ids:
seen_ids.add(id(obj))
size += sys.getsizeof(obj)
need_referents.append(obj)
objects = get_referents(*need_referents)
return size
Pour contraster cela avec la fonction mise en liste blanche suivante, la plupart des objets savent comment se déplacer eux-mêmes à des fins de récupération de place (ce qui est approximativement ce que nous recherchons lorsque nous voulons savoir combien certains objets sont chers en mémoire. Cette fonctionnalité est utilisée par gc.get_referents
.) Cependant, cette mesure va être beaucoup plus étendue que nous l’avions prévu si nous ne faisons pas attention.
Par exemple, les fonctions en savent beaucoup sur les modules dans lesquels elles sont créées.
Un autre point de contraste est que les chaînes qui sont des clés dans les dictionnaires sont généralement internes afin qu'elles ne soient pas dupliquées. La vérification id(key)
nous permettra également d'éviter de compter les doublons, ce que nous faisons dans la section suivante. La solution de liste noire ignore le comptage des clés qui sont des chaînes.
Types sur liste blanche, visiteur récursif (ancienne implémentation)
Pour couvrir la plupart de ces types moi-même, au lieu de compter sur le module gc, j'ai écrit cette fonction récursive pour essayer d'estimer la taille de la plupart des objets Python, y compris la plupart des buildins, des types dans le module collections et des types personnalisés (à fentes et autres) .
Ce type de fonction donne un contrôle beaucoup plus fin sur les types que nous comptons pour l'utilisation de la mémoire, mais a le danger de laisser de côté les types:
import sys
from numbers import Number
from collections import Set, Mapping, deque
try: # Python 2
zero_depth_bases = (basestring, Number, xrange, bytearray)
iteritems = 'iteritems'
except NameError: # Python 3
zero_depth_bases = (str, bytes, Number, range, bytearray)
iteritems = 'items'
def getsize(obj_0):
"""Recursively iterate to sum size of object & members."""
_seen_ids = set()
def inner(obj):
obj_id = id(obj)
if obj_id in _seen_ids:
return 0
_seen_ids.add(obj_id)
size = sys.getsizeof(obj)
if isinstance(obj, zero_depth_bases):
pass # bypass remaining control flow and return
elif isinstance(obj, (tuple, list, Set, deque)):
size += sum(inner(i) for i in obj)
elif isinstance(obj, Mapping) or hasattr(obj, iteritems):
size += sum(inner(k) + inner(v) for k, v in getattr(obj, iteritems)())
# Check for custom object instances - may subclass above too
if hasattr(obj, '__dict__'):
size += inner(vars(obj))
if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
size += sum(inner(getattr(obj, s)) for s in obj.__slots__ if hasattr(obj, s))
return size
return inner(obj_0)
Et je l'ai testé de façon plutôt nonchalante (je devrais ne pas le tester):
>>> getsize(['a', tuple('bcd'), Foo()])
344
>>> getsize(Foo())
16
>>> getsize(tuple('bcd'))
194
>>> getsize(['a', tuple('bcd'), Foo(), {'foo': 'bar', 'baz': 'bar'}])
752
>>> getsize({'foo': 'bar', 'baz': 'bar'})
400
>>> getsize({})
280
>>> getsize({'foo':'bar'})
360
>>> getsize('foo')
40
>>> class Bar():
... def baz():
... pass
>>> getsize(Bar())
352
>>> getsize(Bar().__dict__)
280
>>> sys.getsizeof(Bar())
72
>>> getsize(Bar.__dict__)
872
>>> sys.getsizeof(Bar.__dict__)
280
Cette implémentation décompose les définitions de classe et les définitions de fonctions parce que nous ne recherchons pas tous leurs attributs, mais comme ils ne devraient exister qu'une seule fois en mémoire pour le processus, leur taille n'a vraiment pas trop d'importance.