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:
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:
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
):
É 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 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:
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:
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:
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
:
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__
:
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
:
>>> 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:
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 tamanho do rastreamento de um grande número de cópias é menor:
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:
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.