Como determino o tamanho de um objeto em Python?
A resposta "Apenas use sys.getsizeof" não é uma resposta completa.
Essa resposta faz trabalho para builtin objetos diretamente, mas não leva em conta o que esses objetos podem conter, especificamente, quais os tipos, tais como objetos personalizados, tuplas, listas, dicts e conjuntos contêm. Eles podem conter instâncias um do outro, além de números, seqüências de caracteres e outros objetos.
Uma resposta mais completa
Usando o Python 3.6 de 64 bits da distribuição Anaconda, com sys.getsizeof, determinei o tamanho mínimo dos seguintes objetos e observe que os conjuntos e os dicionários pré-alocam espaço para que os vazios não cresçam novamente até depois de um determinado valor (o que pode variar de acordo com a implementação do idioma):
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.
Como você interpreta isso? Bem, digamos que você tenha um conjunto com 10 itens. Se cada item tem 100 bytes cada, qual é o tamanho de toda a estrutura de dados? O conjunto é o próprio 736 porque foi dimensionado uma vez para 736 bytes. Então você adiciona o tamanho dos itens, totalizando 1736 bytes
Algumas advertências para definições de função e classe:
Observe que cada definição de classe possui uma estrutura de proxy __dict__
(48 bytes) para atributos de classe. Cada slot possui um descritor (como a property
) na definição de classe.
Instâncias com slot começam com 48 bytes no primeiro elemento e aumentam em 8 a cada adicional. Somente objetos com espaço vazio têm 16 bytes, e uma instância sem dados faz muito pouco sentido.
Além disso, cada definição de função possui objetos de código, talvez docstrings e outros atributos possíveis, até a __dict__
.
Observe também que usamos sys.getsizeof()
porque nos preocupamos com o uso do espaço marginal, que inclui a sobrecarga da coleta de lixo para o objeto, nos documentos :
getsizeof () chama o __sizeof__
método do objeto e adiciona uma sobrecarga adicional do coletor de lixo se o objeto for gerenciado pelo coletor de lixo.
Observe também que o redimensionamento de listas (por exemplo, anexando-as repetidamente) faz com que pré-alocem espaço, da mesma forma que sets e dict. A partir do código-fonte 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);
Data histórica
Análise do Python 2.7, confirmada com 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.
Observe que os dicionários ( mas não os conjuntos ) obtiveram uma representação mais compacta no Python 3.6
Eu acho que 8 bytes por item adicional para referência faz muito sentido em uma máquina de 64 bits. Esses 8 bytes apontam para o local na memória em que o item contido está. Os 4 bytes têm largura fixa para unicode no Python 2, se bem me lembro, mas no Python 3, str se torna um unicode de largura igual à largura máxima dos caracteres.
(E para saber mais sobre slots, veja esta resposta )
Uma função mais completa
Queremos uma função que pesquise os elementos em listas, tuplas, conjuntos, ditados, obj.__dict__
's e obj.__slots__
, além de outras coisas que talvez ainda não tenhamos pensado.
Queremos contar gc.get_referents
com essa pesquisa porque ela funciona no nível C (tornando-a muito rápida). A desvantagem é que get_referents pode retornar membros redundantes, portanto, precisamos garantir que não contemos duas vezes.
Classes, módulos e funções são singletons - eles existem uma vez na memória. Não estamos tão interessados em seu tamanho, pois não há muito o que fazer sobre eles - eles fazem parte do programa. Portanto, evitaremos contá-los se eles forem referenciados.
Usaremos uma lista negra de tipos para não incluir o programa inteiro em nossa contagem de tamanhos.
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
Para contrastar isso com a seguinte função na lista de permissões, a maioria dos objetos sabe como se mover para fins de coleta de lixo (que é aproximadamente o que estamos procurando quando queremos saber o quão caro são na memória certos objetos. Essa funcionalidade é usada por gc.get_referents
.) No entanto, essa medida terá um alcance muito mais amplo do que pretendíamos se não tomarmos cuidado.
Por exemplo, as funções sabem bastante sobre os módulos em que são criadas.
Outro ponto de contraste é que as seqüências de caracteres que são chaves nos dicionários geralmente são internadas para que não sejam duplicadas. A verificação id(key)
também permitirá evitar a contagem de duplicatas, o que faremos na próxima seção. A solução da lista negra ignora a contagem de chaves que são seqüências de caracteres por completo.
Tipos de lista de permissões, visitante recursivo (implementação antiga)
Para cobrir a maioria desses tipos, em vez de depender do módulo gc, escrevi essa função recursiva para tentar estimar o tamanho da maioria dos objetos Python, incluindo a maioria dos componentes internos, tipos no módulo de coleções e tipos personalizados (com ou sem fenda) .
Esse tipo de função fornece muito mais controle refinado sobre os tipos que contaremos para uso de memória, mas tem o risco de deixar os tipos de fora:
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 eu testei casualmente (eu deveria unittest):
>>> 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
Essa implementação divide-se nas definições de classe e nas definições de função porque não seguimos todos os seus atributos, mas como eles só devem existir uma vez na memória para o processo, seu tamanho realmente não importa muito.