Python consome muita memória ou como reduzir o tamanho dos objetos?

Um problema de memória pode surgir quando um grande número de objetos está ativo na RAM durante a execução de um programa, especialmente se houver restrições na quantidade total de memória disponível.


Abaixo está uma visão geral de alguns métodos para reduzir o tamanho dos objetos, o que pode reduzir significativamente a quantidade de RAM necessária para programas em Python puro.


Nota: Esta é a versão em inglês da minha postagem original (em russo).


Para simplificar, consideraremos estruturas em Python para representar pontos com as coordenadas x , y , z com acesso aos valores das coordenadas por nome.


Dict


Em pequenos programas, especialmente em scripts, é bastante simples e conveniente usar o dict para representar informações estruturais:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

Com o advento de uma implementação mais compacta no Python 3.6 com um conjunto ordenado de chaves, o dict se tornou ainda mais atraente. No entanto, vejamos o tamanho de sua presença na RAM:


 >>> print(sys.getsizeof(ob)) 240 

É preciso muita memória, especialmente se você precisar criar repentinamente um grande número de instâncias:


Número de instânciasTamanho dos objetos
1.000.000240 Mb
10.000.0002,40 Gb
100.000.00024 gb

Instância de classe


Para quem gosta de vestir tudo nas classes, é preferível definir estruturas como uma classe com acesso pelo nome do atributo:


 class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y 

A estrutura da instância da classe é interessante:


CampoTamanho (bytes)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL:56.

Aqui __weakref__ é uma referência à lista das chamadas referências fracas a esse objeto, o campo __dict__ é uma referência ao dicionário de instância da classe, que contém os valores dos atributos da instância (observe que a plataforma de referências de 64 bits ocupa 8 bytes). A partir do Python 3.3, o espaço compartilhado é usado para armazenar chaves no dicionário para todas as instâncias da classe. Isso reduz o tamanho do rastreamento da instância na RAM:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

Como resultado, um grande número de instâncias de classe tem um espaço menor na memória que um dicionário comum ( dict ):


Número de instânciasTamanho
1.000.000168 Mb
10.000.0001,68 Gb
100.000.00016,8 Gb

É fácil ver que o tamanho da instância na RAM ainda é grande devido ao tamanho do dicionário da instância.


Instância da classe com __slots__


Uma redução significativa no tamanho de uma instância de classe na RAM é obtida com a eliminação de __dict__ e __weakref__ . Isso é possível com a ajuda de um "truque" com __slots__ :


 class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64 

O tamanho do objeto na RAM tornou-se significativamente menor:


CampoTamanho (bytes)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL:64

O uso de __slots__ na definição de classe faz com que o espaço ocupado por um grande número de instâncias na memória seja reduzido significativamente:


Número de instânciasTamanho
1.000.00064 Mb
10.000.000640 Mb
100.000.0006,4 Gb

Atualmente, esse é o principal método de reduzir substancialmente o espaço ocupado pela memória de uma instância de uma classe na RAM.


Essa redução é alcançada pelo fato de que na memória após o título do objeto, as referências do objeto são armazenadas - os valores dos atributos e o acesso a eles é realizado usando descritores especiais que estão no dicionário de classes:


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

Para automatizar o processo de criação de uma classe com __slots__ , existe uma biblioteca [namedlist] ( https://pypi.org/project/namedlist ). A função namedlist.namedlist cria uma classe com __slots__ :


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

Outro pacote [attrs] ( https://pypi.org/project/attrs ) permite automatizar o processo de criação de classes com e sem __slots__ .


Tuple


O Python também possui uma tuple tipo embutida para representar estruturas de dados imutáveis. Uma tupla é uma estrutura ou registro fixo, mas sem nomes de campos. Para acesso ao campo, o índice do campo é usado. Os campos da tupla são associados de uma vez por todas aos objetos de valor no momento da criação da instância da tupla:


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR 

Instâncias de tupla são bastante compactas:


 >>> print(sys.getsizeof(ob)) 72 

Eles ocupam 8 bytes na memória mais do que instâncias de classes com __slots__ , pois o rastreamento da tupla na memória também contém vários campos:


CampoTamanho (bytes)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
TOTAL:72

Namedtuple


Como a tupla é usada muito amplamente, um dia houve uma solicitação de que você ainda poderia ter acesso aos campos e também pelo nome. A resposta para esta solicitação foi o módulo collections.namedtuple .


A função namedtuple foi projetada para automatizar o processo de geração dessas classes:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

Ele cria uma subclasse de tupla, na qual os descritores são definidos para acessar os campos pelo nome. Para o nosso exemplo, seria algo parecido com isto:


  class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_z(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

Todas as instâncias de tais classes têm um espaço de memória idêntico ao de uma tupla. Um grande número de instâncias deixa um espaço de memória um pouco maior:


Número de instânciasTamanho
1.000.00072 Mb
10.000.000720 Mb
100.000.0007,2 Gb

Classe de gravação: mutável nomeado duplo sem GC cíclico


Como a tuple e, consequentemente, as namedtuple nomeados namedtuple pares geram objetos imutáveis, no sentido de que o atributo ob.x não pode mais ser associado a outro objeto de valor, surgiu uma solicitação para uma variante mutável denominada por casal. Como não existe um tipo interno no Python que seja idêntico à tupla que suporta atribuições, muitas opções foram criadas. Vamos nos concentrar em [recordclass] ( https://pypi.org/project/recordclass ), que recebeu uma classificação de [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tuple-em- python / 29419745). Além disso, pode ser usado para reduzir o tamanho dos objetos na RAM em comparação com o tamanho dos objetos semelhantes a tuple .


O pacote recordclass apresenta o tipo recordclass.mutabletuple , que é quase idêntico à tupla, mas também suporta atribuições. Basicamente, são criadas subclasses quase completamente idênticas aos nomeados, mas também suportam a atribuição de novos valores aos campos (sem criar novas instâncias). A função recordclass , como a função namedtuple , permite automatizar a criação dessas classes:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

As instâncias de classe têm a mesma estrutura que a tuple , mas apenas sem PyGC_Head :


CampoTamanho (bytes)
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL:48.

Por padrão, a função recordclass cria uma classe que não participa do mecanismo de coleta de lixo cíclico. Normalmente, namedtuple e recordclass são usados ​​para gerar classes representando registros ou estruturas de dados simples (não recursivas). Usá-los corretamente no Python não gera referências circulares. Por esse motivo, na sequência de instâncias de classes geradas pela recordclass de recordclass , por default, the fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the structure, corresponding to the created class, in the PyTypeObject structure, corresponding to the created class, in the field, by default, the flag sinalizadores field, by default, the flag Py_TPFLAGS_HAVE_GC` não está definido).


O tamanho do espaço ocupado pela memória de um grande número de instâncias é menor que o das instâncias da classe com __slots__ :


Número de instânciasTamanho
1.000.00048 Mb
10.000.000480 Mb
100.000.0004,8 Gb

Dataobject


Outra solução proposta na biblioteca de classe de registro é baseada na idéia: use a mesma estrutura de armazenamento na memória que nas instâncias de classe com __slots__ , mas não participe do mecanismo de coleta de lixo cíclico. Essas classes são geradas usando a função recordclass.make_dataclass :


  >>> Point = make_dataclass('Point', ('x', 'y', 'z')) 

A classe criada dessa maneira, por padrão, cria instâncias mutáveis.


Outra maneira - use a declaração de classe com herança de recordclass.dataobject :


 class Point(dataobject): x:int y:int z:int 

As classes criadas dessa maneira criarão instâncias que não participam do mecanismo de coleta de lixo cíclico. A estrutura da instância na memória é a mesma do caso com __slots__ , mas sem o PyGC_Head :


CampoTamanho (bytes)
PyObject_HEAD16
x8
y8
y8
TOTAL:40.

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

Para acessar os campos, descritores especiais também são usados ​​para acessar o campo por seu deslocamento desde o início do objeto, localizado no dicionário de classes:


 mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>}) 

O tamanho da área ocupada por memória de um grande número de instâncias é o mínimo possível para o CPython:


Número de instânciasTamanho
1.000.00040 Mb
10.000.000400 Mb
100.000.0004,0 Gb

Cython


Há uma abordagem baseada no uso do [Cython] ( https://cython.org ). Sua vantagem é que os campos podem assumir os valores dos tipos atômicos da linguagem C. Descritores para acessar campos do Python puro são criados automaticamente. Por exemplo:


 cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z 

Nesse caso, as instâncias têm um tamanho de memória ainda menor:


 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32 

O rastreio da instância na memória possui a seguinte estrutura:


CampoTamanho (bytes)
PyObject_HEAD16
x4
y4
y4
está vazio4
TOTAL:32.

O tamanho da área de cobertura de um grande número de cópias é menor:


NúmeroTamanho
1.000.00032 Mb
10.000.000320 Mb
100.000.0003,2 Gb

No entanto, deve-se lembrar que ao acessar do código Python, uma conversão de int para um objeto Python e vice-versa será realizada sempre.


Numpy


O uso de matrizes multidimensionais ou matrizes de registros para uma grande quantidade de dados fornece um ganho de memória. No entanto, para um processamento eficiente em Python puro, você deve usar métodos de processamento focados no uso de funções do pacote numpy .


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

Uma matriz de N elementos, inicializada com zeros, é criada usando a função:


  >>> points = numpy.zeros(N, dtype=Point) 

O tamanho da matriz na memória é o mínimo possível:


Número de objetosTamanho
1.000.00012 Mb
10.000.000120 Mb
100.000.0001,20 Gb

O acesso normal aos elementos e linhas da matriz exigirá a conversão de um objeto Python para um valor C int e vice-versa. A extração de uma única linha resulta na criação de uma matriz que contém um único elemento. Seu rastreio não será mais tão compacto:


  >>> sys.getsizeof(points[0]) 68 

Portanto, como observado acima, no código Python, é necessário processar matrizes usando funções do pacote numpy .


Conclusão


Em um exemplo claro e simples, foi possível verificar se a comunidade de desenvolvedores e usuários da linguagem de programação Python (CPython) tem possibilidades reais de uma redução significativa na quantidade de memória usada pelos objetos.

Source: https://habr.com/ru/post/pt458518/


All Articles