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

Um problema de memória pode surgir quando você precisa ter um grande número de objetos durante a execução do programa, especialmente se houver restrições no tamanho total da RAM disponível.


A seguir, é apresentada 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.


Para simplificar, consideraremos estruturas no Python para representar pontos com coordenadas x , y , z com acesso a valores de coordenadas por nome.


Dict


Em pequenos programas, especialmente 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 tornou ainda mais atraente. No entanto, observe o tamanho de seu rastreamento 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 cópiasTamanho do rastreamento
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 defini-lo 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:


O campoTamanho (bytes)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL:56.

Aqui __weakref__ é um link para uma lista das chamadas referências fracas para esse objeto, o campo __dict__ é um link para o dicionário de instância da classe que contém os valores dos atributos da instância (observe que os links em uma plataforma de 64 bits ocupam 8 bytes). A partir do Python 3.3, um espaço de chave do dicionário compartilhado é usado para todas as instâncias da classe. Isso reduz o tamanho do rastreamento da instância na memória:


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

Como resultado, um grande número de instâncias de classe deixa uma pegada menor na memória que um dicionário comum ( dict ):


Número de cópiasTamanho do rastreamento
1.000.000168 Mb
10.000.0001,68 GB
100.000.00016,8 GB

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


Instância da classe com __slots__


Uma redução significativa no rastreamento de uma instância na memória é obtida com a eliminação de __dict__ e __weakref__ . Isso é possível com o "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 rastreamento na memória se tornou muito mais compacto:


O campoTamanho (bytes)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL:64

O uso de __slots__ na definição de classe leva ao fato de que o rastreamento de um grande número de instâncias na memória é reduzido significativamente:


Número de cópiasTamanho do rastreamento
1.000.00064 Mb
10.000.000640 Mb
100.000.0006,4 GB

Atualmente, esse é o principal método para reduzir significativamente o rastreamento de uma instância de classe na memória do programa.


Essa redução é alcançada pelo fato de que, após o cabeçalho do objeto, as referências aos objetos são armazenadas na memória 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>}) 

Existe uma biblioteca de lista __slots__ para automatizar o processo de criação de uma classe com __slots__ . A função namedlist.namedlist cria uma estrutura de classe idêntica à classe com __slots__ :


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

Outro pacote attrs permite automatizar o processo de criação de classes com e sem __slots__ .


Tuple


O Python também possui um tipo de tuple para representar conjuntos de dados. Tupla é uma estrutura ou registro fixo, mas sem nomes de campos. Para acessar o campo, o índice do campo é usado. Os campos da tupla são associados de uma vez por todas aos objetos de valor no momento em que a tupla é instanciada:


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

Instâncias de tupla são bastante compactas:


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

Eles ocupam 8 bytes mais na memória do que instâncias de classe com __slots__ , pois o rastreamento da tupla na memória também contém o número de campos:


O 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 para poder ainda ter acesso aos campos também pelo nome. A resposta a esta solicitação foi o módulo collections.namedtuple .


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


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

Ele cria uma subclasse de tupla, que define identificadores para acessar campos pelo nome. Para o nosso exemplo, será 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_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

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


Número de cópiasTamanho do rastreamento
1.000.00072 Mb
10.000.000720 Mb

Classe de gravação: mutuado nomeado duplo sem GC


Como as tuple e, consequentemente, namedtuple classes namedtuple geram objetos não mutáveis ​​no sentido de que um objeto de valor ob.x não ob.x mais ser associado a outro objeto de valor, surgiu uma solicitação para uma variante mutada de nomeado duplo. Como o Python não possui um tipo interno idêntico a uma tupla que suporta atribuições, muitas variações foram criadas. Vamos nos concentrar na classe de registro , que recebeu uma classificação de fluxo de pilha . Além disso, com sua ajuda, é possível reduzir o tamanho do rastreamento de um objeto na memória em comparação com o tamanho do rastreamento de objetos do tipo tuple .


No pacote recordclass , é introduzido o tipo recordclass.mutabletuple , que é quase idêntico à tupla, mas também suporta atribuições. Basicamente, são criadas subclasses quase 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 , automatiza a criação de tais classes:


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

Instâncias da classe têm a mesma estrutura que tuple , mas apenas sem PyGC_Head :


O campoTamanho (bytes)
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL:48.

Por padrão, a função recordclass gera uma classe que não está envolvida no mecanismo de coleta de lixo circular. Normalmente, namedtuple e recordclass são usados ​​para gerar classes que representam registros ou estruturas de dados simples (não recursivas). Seu uso correto no Python não gera referências circulares. Por esse motivo, o rastreamento de instâncias de classes geradas pela classe de PyGC_Head padrão PyGC_Head fragmento PyGC_Head , necessário para classes que suportam o mecanismo de coleta de lixo cíclico (mais precisamente: o sinalizador PyTypeObject não está definido no campo flags na estrutura PyTypeObject correspondente à classe que está sendo criada).


O tamanho do rastreamento de um grande número de instâncias é menor que o das instâncias de uma classe com __slots__ :


Número de cópiasTamanho do rastreamento
1.000.00048 Mb
10.000.000480 Mb
100.000.0004,8 GB

Dataobject


Outra solução proposta na biblioteca de recordclass baseada na idéia: usar a estrutura de armazenamento na memória, como nas instâncias de classes com __slots__ , mas não participar do mecanismo de coleta de lixo cíclico. A classe é recordclass.make_dataclass usando a função recordclass.make_dataclass :


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

A classe padrão criada dessa maneira cria instâncias mutadas.


Outra maneira é usar a declaração de classe herdando de recordclass.dataobject :


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

As classes criadas dessa maneira gerarão instâncias que não participam do mecanismo de coleta de lixo circular. A estrutura da instância na memória é a mesma de __slots__ , mas sem o cabeçalho PyGC_Head :


O campoTamanho (bytes)
PyObject_HEAD16
x8
y8
y8
TOTAL:40.

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

Para acessar os campos, também são utilizados descritores especiais para acessar o campo por seu deslocamento em relação ao início do objeto, que são colocados 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 do rastreamento de um grande número de instâncias é o menor possível para o CPython:


Número de cópiasTamanho do rastreamento
1.000.00040 Mb
10.000.000400 Mb
100.000.0004,0 GB

Cython


Existe uma abordagem baseada no uso do Cython . Sua vantagem é que os campos podem assumir valores dos tipos de linguagem C. Os 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 

Um rastreamento de instância na memória possui a seguinte estrutura:


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

O tamanho do rastreamento de um grande número de cópias é menor:


Número de cópiasTamanho do rastreamento
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, a conversão do objeto int para o Python e vice-versa será realizada a cada vez.


Numpy


O uso de matrizes multidimensionais ou de registro para grandes quantidades 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 e N elementos inicializados com zeros são criados usando a função:


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

O tamanho da matriz é o menor possível:


Número de cópiasTamanho do rastreamento
1.000.00012 Mb
10.000.000120 Mb
100.000.0001,20 GB

O acesso regular a elementos e cadeias de array exigirá uma conversão de objeto Python
no valor de C int e vice-versa. A extração de uma única linha resulta em uma matriz que contém um único elemento. Sua trilha não será tão compacta:


  >>> 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


Usando um exemplo claro e simples, foi possível verificar se a comunidade de desenvolvedores e usuários da linguagem de programação Python (CPython) tinha oportunidades reais de reduzir significativamente a quantidade de memória usada pelos objetos.

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


All Articles