Come posso determinare la dimensione di un oggetto in Python?
La risposta "Usa solo sys.getsizeof" non è una risposta completa.
Quella risposta non lavoro per incorporato oggetti direttamente, ma non tiene conto di ciò che questi oggetti possono contenere, in particolare, quali tipi, come ad esempio oggetti personalizzati, tuple, liste, dicts e set contengono. Possono contenere istanze reciproche, nonché numeri, stringhe e altri oggetti.
Una risposta più completa
Usando Python 3.6 a 64 bit dalla distribuzione Anaconda, con sys.getsizeof, ho determinato la dimensione minima dei seguenti oggetti, e noto che set e dadi preallocano lo spazio in modo che quelli vuoti non crescano di nuovo fino a dopo un determinato importo (che può varia in base all'implementazione della lingua):
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.
Come lo interpreti? Bene, supponiamo che tu abbia un set con 10 oggetti. Se ogni elemento è di 100 byte ciascuno, quanto è grande l'intera struttura dei dati? Il set è 736 stesso perché è stato ridimensionato una volta a 736 byte. Quindi aggiungi la dimensione degli elementi, quindi sono 1736 byte in totale
Alcune avvertenze per le definizioni di funzioni e classi:
Nota ogni definizione di classe ha una struttura proxy __dict__
(48 byte) per attrs di classe. Ogni slot ha un descrittore (come a property
) nella definizione della classe.
Le istanze con slot iniziano con 48 byte sul loro primo elemento e aumentano di 8 ogni ulteriore. Solo gli oggetti con slot vuoti hanno 16 byte e un'istanza senza dati non ha molto senso.
Inoltre, ogni definizione di funzione ha oggetti di codice, forse dotstring e altri possibili attributi, persino a __dict__
.
Si noti inoltre che utilizziamo sys.getsizeof()
perché ci preoccupiamo dell'utilizzo dello spazio marginale, che include l'overhead della garbage collection per l'oggetto, dai documenti :
getsizeof () chiama il __sizeof__
metodo dell'oggetto e aggiunge un overhead del Garbage Collector aggiuntivo se l'oggetto è gestito dal Garbage Collector.
Si noti inoltre che il ridimensionamento degli elenchi (ad es. Aggiungendoli ripetutamente) li induce a preallocare lo spazio, in modo simile a set e dadi. Dal codice sorgente 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);
Dati storici
Analisi Python 2.7, confermata con guppy.hpy
e 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.
Nota che i dizionari ( ma non i set ) hanno ottenuto una rappresentazione più compatta in Python 3.6
Penso che 8 byte per ogni elemento aggiuntivo a cui fare riferimento abbiano molto senso su una macchina a 64 bit. Questi 8 byte indicano il punto in cui si trova l'oggetto contenuto. I 4 byte sono a larghezza fissa per Unicode in Python 2, se ricordo bene, ma in Python 3, str diventa un Unicode di larghezza uguale alla larghezza massima dei caratteri.
(E per ulteriori informazioni sulle slot, vedi questa risposta )
Una funzione più completa
Vogliamo una funzione che cerchi gli elementi in elenchi, tuple, insiemi, dicts, obj.__dict__
's e obj.__slots__
, così come altre cose che potremmo non aver ancora pensato.
Vogliamo fare affidamento su gc.get_referents
questa ricerca perché funziona a livello C (rendendolo molto veloce). Il rovescio della medaglia è che get_referents può restituire membri ridondanti, quindi dobbiamo assicurarci di non raddoppiare il conteggio.
Classi, moduli e funzioni sono singoli, esistono una volta nella memoria. Non siamo così interessati alle loro dimensioni, poiché non c'è molto che possiamo fare al riguardo: fanno parte del programma. Quindi eviteremo di contarli se si verificano riferimenti.
Utilizzeremo una lista nera di tipi in modo da non includere l'intero programma nel conteggio delle dimensioni.
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
Per contrastare questo con la seguente funzione autorizzata, la maggior parte degli oggetti sa come attraversarsi ai fini della garbage collection (che è approssimativamente ciò che stiamo cercando quando vogliamo sapere quanto costosi sono in memoria determinati oggetti. Questa funzionalità è utilizzata da gc.get_referents
.) Tuttavia, questa misura avrà una portata molto più ampia di quanto intendessimo se non stiamo attenti.
Ad esempio, le funzioni sanno molto sui moduli in cui sono create.
Un altro punto di contrasto è che le stringhe che sono chiavi nei dizionari sono di solito internate in modo da non essere duplicate. Il controllo id(key)
ci consentirà anche di evitare il conteggio dei duplicati, cosa che facciamo nella sezione successiva. La soluzione della lista nera salta il conteggio delle chiavi che sono del tutto stringhe.
Tipi autorizzati, Visitatore ricorsivo (implementazione precedente)
Per coprire la maggior parte di questi tipi da solo, invece di fare affidamento sul modulo gc, ho scritto questa funzione ricorsiva per provare a stimare la dimensione della maggior parte degli oggetti Python, inclusi la maggior parte dei builtin, i tipi nel modulo collezioni e i tipi personalizzati (scanalati e non) .
Questo tipo di funzione offre un controllo molto più preciso sui tipi che contiamo per l'utilizzo della memoria, ma ha il pericolo di escludere i tipi:
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)
E l'ho testato piuttosto casualmente (dovrei smontarlo):
>>> 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
Questa implementazione si suddivide in definizioni di classe e definizioni di funzioni perché non seguiamo tutti i loro attributi, ma poiché dovrebbero esistere solo una volta in memoria per il processo, le loro dimensioni non contano molto.