Como escrever classes eficientes em termos de memória em Python
Aprenda sobre o gerenciamento de memória em Python com técnicas avançadas para codificar classes eficientes em termos de memória. Explore exercícios práticos para um desempenho ideal.
Se você escrever classes eficientes em termos de memória em Python, será necessário desenvolver aplicativos robustos com bom desempenho sob várias restrições do sistema. O uso eficiente da memória acelera o tempo de execução do aplicativo e aumenta a escalabilidade ao consumir menos recursos, o que é especialmente importante em ambientes com disponibilidade limitada de memória.
Neste artigo, apresentarei a você os fundamentos do gerenciamento de memória em Python e apresentarei diferentes técnicas práticas para escrever classes eficientes em termos de memória. Quer você esteja criando sistemas complexos ou scripts simples, esses insights o ajudarão a escrever um código Python mais limpo e eficiente.
Os fundamentos do gerenciamento de memória em Python
O sistema de gerenciamento de memória do Python é sofisticado e foi projetado para simplificar o desenvolvimento, lidando com a complexidade das operações de memória. Esse sistema é necessário para garantir que os aplicativos Python sejam executados com eficiência.
Como o Python gerencia a memória
O heap privado é o núcleo do gerenciamento de memória do Python. É onde todos os objetos e estruturas de dados do Python são armazenados. Os programadores não podem acessar esse heap privado diretamente; em vez disso, eles interagem com os objetos por meio do sistema de gerenciamento de memória do Python.
O sistema de gerenciamento de memória usa:
Alocadores de memória: O Python usa um alocador de memória interno que gerencia a alocação e a desalocação de blocos de memória. Esse alocador é otimizado para objetos pequenos usando "listas livres", que reciclam blocos de memória alocados anteriormente para acelerar futuras alocações. Para objetos mais complexos, como listas e dicionários, o Python emprega a alocação dinâmica de memória para gerenciar suas variações de tamanho.
Pools de memória: O Python organiza a memória em pools de acordo com o tamanho do objeto para minimizar a sobrecarga e a fragmentação. Isso ajuda a gerenciar a memória com mais eficiência, agrupando objetos de tamanho semelhante.
Desalocação de memória e coleta de lixo
O Python emprega uma abordagem dupla para a coleta de lixo:
Contagem de referência: Esse é o principal método em que o Python programa as referências de cada objeto. Quando a contagem de referências de um objeto cai para zero, indicando que não há referências a ele, o alocador de memória libera imediatamente sua memória.
Coletor de lixo cíclico: O Python inclui um coletor de lixo cíclico para gerenciar referências circulares, com as quais a contagem de referências por si só não consegue lidar. Esse coletor identifica e limpa periodicamente os grupos de objetos que fazem referência uns aos outros, mas que não estão mais em uso em outras partes do programa.
Criação de perfil e otimização do uso da memória
Compreender e otimizar o uso da memória é fundamental para manter o desempenho dos aplicativos.
O Python oferece várias ferramentas para criação de perfil de memória, que fornecem insights além da depuração padrão, ajudando os desenvolvedores a identificar e resolver ineficiências de memória:
pympler: Uma ferramenta abrangente que programa o uso da memória e analisa o espaço do objeto, tornando-a adequada para investigações detalhadas da memória.
memory_profiler: Essa ferramenta oferece uma análise de uso de memória linha por linha, permitindo que os desenvolvedores identifiquem as linhas exatas em que o consumo de memória é alto.
tracemalloc: Integrado à biblioteca padrão do Python, o tracemalloc ajuda a rastrear as alocações de memória e a detectar vazamentos, oferecendo insights sobre as tendências de alocação de memória.
Usaremos a biblioteca pympler neste tutorial, e você pode instalá-la usando o gerenciador de pacotes pip em seu terminal:
Técnicas para escrever classes eficientes em termos de memória em Python
Agora que você entendeu os conceitos básicos do gerenciamento de memória em Python, vamos examinar algumas técnicas avançadas para escrever classes eficientes em termos de memória.
1. Otimização da estrutura de classes com __slots__
No Python, cada instância de classe tem um dicionário chamado __dict__ para armazenar variáveis de instância. Embora isso permita a atribuição dinâmica de novas variáveis, pode levar a uma sobrecarga significativa de memória, especialmente ao criar muitas instâncias de classe. Ao definir __slots__, você diz ao Python para alocar espaço para um conjunto fixo de atributos, eliminando a necessidade do dicionário dinâmico.
Usar __slots__tem dois benefícios principais:
Uso reduzido da memória: Cada objeto usa menos memória sem o dicionário de instância. Isso pode ser relevante quando milhões de instâncias de classe são necessárias, pois a economia por instância se multiplica.
Aumento da velocidade de acesso aos atributos: O acesso aos atributos por meio do site __slots__ é mais rápido do que por meio de um dicionário, devido à pesquisa direta de atributos, que ignora as operações de tabela de hash envolvidas no acesso ao dicionário.
Exemplo prático: Pontos em um espaço bidimensional
Vamos considerar uma classe simples que representa um ponto em um espaço bidimensional:
classPointWithoutSlots:def__init__(self, x, y):
self.x = x
self.y = y
classPointWithSlots:
__slots__ =('x','y')def__init__(self, x, y):
self.x = x
self.y = yTecnologia
Na classe PointWithoutSlots cada instância tem um __dict__ para armazenar x e y enquanto a classe PointWithSlots não, o que resulta em menos memória usada por instância.
Para ilustrar a diferença no uso da memória, vamos criar mil pontos e medir a memória total usada usando o pacote pympler pacote:
from pympler import asizeof
points_without_slots =[PointWithoutSlots(i, i)for i inrange(1000)]
points_with_slots =[PointWithSlots(i, i)for i inrange(1000)]print("Total memory (without __slots__):", asizeof.asizeof(points_without_slots))print("Total memory (with __slots__):", asizeof.asizeof(points_with_slots))Tecnologia
A saída que vemos é:
Comparação de memória com e sem slots
O resultado normalmente mostra que a instância de PointWithSlots consome menos memória do que PointWithoutSlotsdemonstrando a eficácia de __slots__ na redução do overhead de memória por instância.
Limitações do uso __slots__
Embora __slots__ sejam benéficos para a otimização da memória, eles têm certas limitações:
Inflexibilidade: Depois que o site__slots__ for definido, você não poderá adicionar dinamicamente novos atributos às instâncias.
Complicações hereditárias: Se uma classe com __slots__ forherdadaa subclasse também deverá definir __slots__ para continuar se beneficiando da otimização da memória, o que pode complicar o design da classe .
Apesar dessas limitações, o uso do __slots__ é uma ferramenta poderosa para desenvolvedores que desejam otimizar o uso da memória em aplicativos Python, especialmente em ambientes com restrições rigorosas de memória ou onde o número de instâncias é muito alto.
2. Implementação de padrões de design com eficiência de memória
Os padrões de design com eficiência de memória são soluções arquitetônicas que ajudam a otimizar o uso e o gerenciamento da memória nos aplicativos Python.
Dois padrões particularmente eficazes para a eficiência de memória no design de classes são os padrões Flyweight e Singleton. Quando implementados corretamente em cenários que envolvem várias instâncias de classes ou dados compartilhados, esses padrões podem reduzir drasticamente o consumo de memória.
Padrão de design Flyweight
O padrão de design Flyweight funciona compartilhando partes comuns do estado entre vários objetos em vez de manter todos os dados em cada objeto, reduzindo significativamente o espaço total de memória.
Considere uma classe que representa círculos gráficos em um aplicativo de desenho em que cada círculo tem uma cor, um raio e coordenadas. Usando o padrão Flyweight, a cor e o raio, que provavelmente são compartilhados por muitos círculos, podem ser armazenados em objetos compartilhados:
classCircle:# Flyweight objectdef__init__(self, color):
self.color = color
defdraw(self, radius, x, y):print(f"Drawing a {self.color} circle with radius {radius} at ({x},{y})")classCircleFactory:# Factory to manage Flyweight objects
_circles ={}@classmethoddefget_circle(cls, color):ifnot cls._circles.get(color):
cls._circles[color]= Circle(color)return cls._circles[color]Tecnologia
Neste exemplo, várias instâncias de círculo compartilham o objeto Circle do objeto CircleFactory com base na cor, garantindo que a memória seja usada de forma mais eficiente ao armazenar os dados de cor uma vez.
Vamos usar essa classe para criar instâncias e entender como a memória é salva.
import sys
factory = CircleFactory()
circle1 = factory.get_circle("Red")
circle2 = factory.get_circle("Red")
circle3 = factory.get_circle("Blue")print("Memory address of circle1:",id(circle1))print("Memory address of circle2:",id(circle2))print("Memory address of circle3:",id(circle3))print("-----------------------------")print("Memory used by circle1:", sys.getsizeof(circle1))print("Memory used by circle2:", sys.getsizeof(circle2))print("Memory used by circle3:", sys.getsizeof(circle3))Tecnologia
A saída que vemos é:
Uso de memória para o padrão de design flyweight
Podemos ver que circle1 e circle2 têm o mesmo endereço de memória, o que significa que eles são de fato o mesmo objeto (compartilhando a mesma instância para a cor "Red"). circle3 tem um endereço de memória diferente porque representa um círculo com uma cor diferente ("Azul").
circle1 e circle2 sendo a mesma instância demonstra que o CircleFactory compartilhou com sucesso a instânciaCircle entre esses dois. É aqui que entra a eficiência da memória: se você tivesse mil círculos vermelhos em seu aplicativo usando o padrão Flyweight, todos eles compartilhariam uma única instância de Circle para a cor, em vez de cada um ter sua própria instância deCircle separada , economizando assim uma quantidade significativa de memória.
Padrão de design Singleton
O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Esse padrão é útil para gerenciar configurações ou recursos compartilhados em um aplicativo, reduzindo a sobrecarga de memória ao não replicar recursos compartilhados ou dados de configuração em vários locais.
Considere o seguinte Database classe:
classDatabase:
_instance =Nonedef__new__(cls):if cls._instance isNone:
cls._instance =super(Database, cls).__new__(cls)# Initialize any attributes of the instance herereturn cls._instance
defconnect(self):# method to simulate database connectionreturn"Connection established"Tecnologia
Nessa implementação, o Database garante que, não importa quantas vezes ele seja instanciado, apenas uma instância compartilhada será usada, conservando a memória ao evitar vários objetos idênticos.
Você pode verificar isso com o código a seguir:
db1 = Database()print(db1.connect())
db2 = Database()print(db1 is db2)Tecnologia
O resultado é o seguinte:
Criação de instância para o padrão de design singleton
A saída "True" indica que db1 e db2referem-se à mesma instância.
Esses padrões demonstram como um design cuidadoso pode melhorar significativamente a eficiência da memória. Ao escolher e implementar padrões de design apropriados, os desenvolvedores podem otimizar seus aplicativos Python para que sejam mais enxutos e tenham melhor desempenho.
3. Uso de tipos de dados incorporados em classes
Selecionando o tipo de dados tipo de dados interno apropriado em Python pode afetar significativamente a eficiência da memória. Os tipos incorporados são otimizados para desempenho e uso de memória devido às suas implementações internas no interpretador Python.
Por exemplo, devido às suas propriedades exclusivas, as tuplas e os conjuntos podem ser opções mais eficientes em termos de memória para casos de uso específicos do que as listas e os dicionários.
As tuplas geralmente usam menos memória do que as listas porque têm um tamanho fixo e não podem ser alteradas depois de criadas, permitindo que os mecanismos internos do Python otimizem seu armazenamento na memória. Para classes projetadas para lidar com dados que não precisam ser modificados após a criação, as tuplas são uma excelente opção.
Exemplo prático: Classes de polígonos
Considere duas classes simples de polígono que usam tuplas e listas para armazenar valores:
classPolygonWithTuple:def__init__(self, points):
self.points =tuple(points)# Storing points as a tupledefdisplay_points(self):return self.points
classPolygonWithList:def__init__(self, points):
self.points =list(points)# Storing points as a listdefdisplay_points(self):return self.pointsTecnologia
Podemos criar instâncias de ambas as classes para comparar o uso da memória:
from pympler import asizeof
points =[(1,2),(3,4),(5,6),(7,8)]# Creating instances of each class
polygon_tuple = PolygonWithTuple(points)
polygon_list = PolygonWithList(points)# Memory usage comparison using pymplerprint("Total memory used by Polygon with Tuple:", asizeof.asizeof(polygon_tuple))print("Total memory used by Polygon with List:", asizeof.asizeof(polygon_list))Tecnologia
O resultado que você obtém é:
Comparação de memória para tuplas e listas
Esse resultado demonstra que a tupla normalmente usa menos memória do que a lista, o que torna a classe PolygonWithTuple mais eficiente em termos de memória para armazenar dados fixos e imutáveis, como coordenadas geométricas.
4. Aproveitamento de geradores para avaliação preguiçosa
Em Python, geradores são uma ferramenta poderosa para gerenciar a memória de forma eficiente, especialmente quando você trabalha com grandes conjuntos de dados.
Os geradores permitem que os dados sejam processados um item de cada vez, carregando na memória apenas o que for necessário em um determinado momento. Esse método de avaliação preguiçosa é benéfico em aplicativos em que grandes volumes de dados são manipulados, mas apenas uma pequena quantidade de cada vez precisa ser processada, reduzindo assim o espaço total de memória.
Exemplo prático: Processamento de grandes conjuntos de dados
Considere um aplicativo que precise processar um grande conjunto de dados de registros de usuários, como o cálculo da idade média dos usuários. Em vez de carregar todos os registros em uma lista (o que poderia consumir uma quantidade significativa de memória), um gerador pode processar um registro por vez.
defprocess_records(file_name):"""Generator that yields user ages from a file one at a time."""withopen(file_name,'r')asfile:for line infile:
user_data = line.strip().split(',')yieldint(user_data[1])defaverage_age(file_name):"""Calculate the average age from user records."""
total_age =0
count =0for age in process_records(file_name):
total_age += age
count +=1return total_age / count if count else0print("Average Age:", average_age('user_data.csv'))Tecnologia
Neste exemplo, a função geradoraprocess_records() lê de um arquivo uma linha de cada vez, converte os dados relevantes em um número inteiro e os produz. Isso significa que apenas uma linha de dados é mantida na memória a qualquer momento, minimizando o uso da memória. A funçãoaverage_age() consome o gerador, acumulando a idade total e a contagem sem nunca precisar de todo o conjunto de dados na memória de uma só vez.
Assim, usando a abordagem de avaliação preguiçosa, podemos conservar a memória e aumentar a capacidade de resposta e o dimensionamento dos aplicativos.
5. Evitar referências circulares
As referências circulares ocorrem quando dois ou mais objetos fazem referência uns aos outros. Isso pode acontecer sutilmente com estruturas como listas, dicionários ou até mesmo definições de classes mais complexas, em que as instâncias mantêm referências umas às outras, criando um loop.
Como vimos nos fundamentos do gerenciamento de memória, o Python tem um coletor de lixo adicional para detectar e limpar referências circulares. No entanto, confiar nesse mecanismo pode levar ao aumento do uso de memória e a ineficiências:
Limpeza atrasada: O coletor de lixo cíclico é executado periodicamente, não continuamente. Isso significa que os objetos envolvidos em referências circulares podem não ser recuperados imediatamente após não serem mais necessários, consumindo memória desnecessariamente.
Sobrecarga de desempenho: O coletor de lixo cíclico requer recursos adicionais para verificar e limpar periodicamente as referências circulares, o que pode afetar o desempenho.
Exemplo prático: Uma estrutura de dados em árvore
Considere um caso de uso típico em uma estrutura de dados em forma de árvore em que cada nó pode manter uma referência a seus filhos e, opcionalmente, a seu pai. Essa vinculação pai-filho pode facilmente criar referências circulares.
Neste exemplo, o root faz referência aochild por meio de sua lista children, e o child faz referência ao root por meio de seu atributoparent. Essa configuração pode criar uma referência circular sem nenhuma intervenção adicional.
Usando referências fracas para evitar referências circulares
As referências fracas são uma maneira eficaz de interromper ou evitar referências circulares sem redesenhar toda a estrutura de dados.
O móduloweakrefdo Python permite que você faça uma referência a um objeto que não aumente sua contagem de referências. Essa referência fraca não impede que o objeto referenciado seja coletado pelo lixo, evitando vazamentos de memória associados a referências circulares.
Veja como podemos modificar a classe anterior para usar referências fracas:
Nessa classe, o atributo pai é agora uma referência fraca ao nó pai. Isso significa que o nó pai pode ser coletado do lixo mesmo que o nó filho ainda exista, desde que não haja outras referências fortes ao pai.
Essa abordagem evita com eficiência os problemas de vazamento de memória normalmente associados a referências circulares em tais estruturas.
Conclusão
Este artigo introduziu os fundamentos do gerenciamento de memória em Python e apresentou cinco técnicas avançadas para escrever classes eficientes em termos de memória, juntamente com exemplos práticos e comparações de memória. Esperamos que você adote essas técnicas e práticas recomendadas em seus projetos para criar aplicativos dimensionáveis e eficientes.
O que são __slots__ e como eles ajudam você a escrever classes Python eficientes em termos de memória?⌄
__slots__ são um mecanismo em Python que permite a declaração explícita de atributos de instância, evitando que você crie um dicionário de instância. Ao definir __slots__ em uma classe, o Python usa uma estrutura de dados muito mais eficiente em termos de memória do que o dicionário usual por instância. Isso reduz o uso da memória, especialmente em programas em que muitas instâncias de classe são criadas.
Como as referências circulares podem afetar o uso da memória e como elas podem ser evitadas?⌄
As referências circulares ocorrem quando dois ou mais objetos fazem referência uns aos outros, criando um loop que impede que o coletor de lixo do Python recupere a memória. Para evitar referências circulares, você pode usar referências fracas por meio do móduloweakref, refatorar seu projeto para remover referências desnecessárias ou interromper manualmente o ciclo definindo as referências como None quando elas não forem mais necessárias.
Que papel o coletor de lixo desempenha no gerenciamento da memória em Python?⌄
O coletor de lixo do Python complementa o sistema de contagem de referências, detectando e descartando referências circulares. Ele procura periodicamente grupos de objetos que só podem ser acessados entre si e que não são usados em nenhum outro lugar do aplicativo, liberando assim a memória que não pode ser recuperada apenas pela simples contagem de referências.
Por que o uso de tipos e estruturas de dados incorporados é necessário para a eficiência da memória?⌄
Os tipos e estruturas de dados incorporados do Python são altamente otimizados para desempenho e uso de memória. O uso desses elementos incorporados, como tuplas em vez de listas para dados imutáveis ou o uso de conjuntos para coleções exclusivas, pode reduzir significativamente a sobrecarga de memória em comparação com estruturas de dados personalizadas ou menos adequadas.
Como as ferramentas de criação de perfil de memória ajudam você a escrever classes Python eficientes em termos de memória?⌄
As ferramentas de criação de perfil de memória, como memory_profiler, pympler e tracemalloc, ajudam os desenvolvedores a entender como a memória é usada em seus aplicativos. Essas ferramentas podem identificar vazamentos de memória, estruturas de dados ineficientes ou linhas específicas de código que usam memória excessiva, permitindo que os desenvolvedores decidam onde as otimizações são necessárias.
Este artigo ensina a você sobre otimização numérica, destacando diferentes técnicas. Ele discute os pacotes Python, como SciPy, CVXPY e Pyomo, e fornece um notebook DataLab prático para você executar exemplos de código.
Aprenda sobre classes abstratas em Python, sua finalidade e como usar o módulo `abc` para garantir interfaces consistentes. Inclui exemplos práticos e melhores práticas para uma implementação eficaz.
Aprenda a implementar a pesquisa binária em Python usando abordagens iterativas e recursivas e explore o módulo bisect integrado para obter funções de pesquisa binária eficientes e pré-implementadas.
Se você escrever classes eficientes em termos de memória em Python, será necessário desenvolver aplicativos robustos com bom desempenho sob várias restrições do sistema. O uso eficiente da memória acelera o tempo de execução do aplicativo e aumenta a escalabilidade ao consumir menos recursos, o que é especialmente importante em ambientes com disponibilidade limitada de memória.
Neste artigo, apresentarei a você os fundamentos do gerenciamento de memória em Python e apresentarei diferentes técnicas práticas para escrever classes eficientes em termos de memória. Quer você esteja criando sistemas complexos ou scripts simples, esses insights o ajudarão a escrever um código Python mais limpo e eficiente.
Os fundamentos do gerenciamento de memória em Python
O sistema de gerenciamento de memória do Python é sofisticado e foi projetado para simplificar o desenvolvimento, lidando com a complexidade das operações de memória. Esse sistema é necessário para garantir que os aplicativos Python sejam executados com eficiência.
Como o Python gerencia a memória
O heap privado é o núcleo do gerenciamento de memória do Python. É onde todos os objetos e estruturas de dados do Python são armazenados. Os programadores não podem acessar esse heap privado diretamente; em vez disso, eles interagem com os objetos por meio do sistema de gerenciamento de memória do Python.
O sistema de gerenciamento de memória usa:
Desalocação de memória e coleta de lixo
O Python emprega uma abordagem dupla para a coleta de lixo:
Criação de perfil e otimização do uso da memória
Compreender e otimizar o uso da memória é fundamental para manter o desempenho dos aplicativos.
O Python oferece várias ferramentas para criação de perfil de memória, que fornecem insights além da depuração padrão, ajudando os desenvolvedores a identificar e resolver ineficiências de memória:
Usaremos a biblioteca
pymplerneste tutorial, e você pode instalá-la usando o gerenciador de pacotes pip em seu terminal:Você pode ler mais sobre a criação de perfil de memória em nosso tutorial - Introdução à criação de perfil de memória em Python.Introdução à criação de perfis de memória em Python.
Técnicas para escrever classes eficientes em termos de memória em Python
Agora que você entendeu os conceitos básicos do gerenciamento de memória em Python, vamos examinar algumas técnicas avançadas para escrever classes eficientes em termos de memória.
1. Otimização da estrutura de classes com
__slots__No Python, cada instância de classe tem um dicionário chamado
__dict__para armazenar variáveis de instância. Embora isso permita a atribuição dinâmica de novas variáveis, pode levar a uma sobrecarga significativa de memória, especialmente ao criar muitas instâncias de classe. Ao definir__slots__, você diz ao Python para alocar espaço para um conjunto fixo de atributos, eliminando a necessidade do dicionário dinâmico.Usar
__slots__tem dois benefícios principais:__slots__é mais rápido do que por meio de um dicionário, devido à pesquisa direta de atributos, que ignora as operações de tabela de hash envolvidas no acesso ao dicionário.Exemplo prático: Pontos em um espaço bidimensional
Vamos considerar uma classe simples que representa um ponto em um espaço bidimensional:
Na classe
PointWithoutSlotscada instância tem um__dict__para armazenarxeyenquanto a classePointWithSlotsnão, o que resulta em menos memória usada por instância.Para ilustrar a diferença no uso da memória, vamos criar mil pontos e medir a memória total usada usando o pacote
pymplerpacote:A saída que vemos é:
Comparação de memória com e sem slots
O resultado normalmente mostra que a instância de
PointWithSlotsconsome menos memória do quePointWithoutSlotsdemonstrando a eficácia de__slots__na redução do overhead de memória por instância.Limitações do uso
__slots__Embora
__slots__sejam benéficos para a otimização da memória, eles têm certas limitações:__slots__for definido, você não poderá adicionar dinamicamente novos atributos às instâncias.__slots__for herdadaa subclasse também deverá definir__slots__para continuar se beneficiando da otimização da memória, o que pode complicar o design da classe .Apesar dessas limitações, o uso do
__slots__é uma ferramenta poderosa para desenvolvedores que desejam otimizar o uso da memória em aplicativos Python, especialmente em ambientes com restrições rigorosas de memória ou onde o número de instâncias é muito alto.2. Implementação de padrões de design com eficiência de memória
Os padrões de design com eficiência de memória são soluções arquitetônicas que ajudam a otimizar o uso e o gerenciamento da memória nos aplicativos Python.
Dois padrões particularmente eficazes para a eficiência de memória no design de classes são os padrões Flyweight e Singleton. Quando implementados corretamente em cenários que envolvem várias instâncias de classes ou dados compartilhados, esses padrões podem reduzir drasticamente o consumo de memória.
Padrão de design Flyweight
O padrão de design Flyweight funciona compartilhando partes comuns do estado entre vários objetos em vez de manter todos os dados em cada objeto, reduzindo significativamente o espaço total de memória.
Considere uma classe que representa círculos gráficos em um aplicativo de desenho em que cada círculo tem uma cor, um raio e coordenadas. Usando o padrão Flyweight, a cor e o raio, que provavelmente são compartilhados por muitos círculos, podem ser armazenados em objetos compartilhados:
Neste exemplo, várias instâncias de círculo compartilham o objeto
Circledo objetoCircleFactorycom base na cor, garantindo que a memória seja usada de forma mais eficiente ao armazenar os dados de cor uma vez.Vamos usar essa classe para criar instâncias e entender como a memória é salva.
A saída que vemos é:
Uso de memória para o padrão de design flyweight
Podemos ver que
circle1ecircle2têm o mesmo endereço de memória, o que significa que eles são de fato o mesmo objeto (compartilhando a mesma instância para a cor "Red").circle3tem um endereço de memória diferente porque representa um círculo com uma cor diferente ("Azul").circle1ecircle2sendo a mesma instância demonstra que oCircleFactorycompartilhou com sucesso a instânciaCircleentre esses dois. É aqui que entra a eficiência da memória: se você tivesse mil círculos vermelhos em seu aplicativo usando o padrão Flyweight, todos eles compartilhariam uma única instância deCirclepara a cor, em vez de cada um ter sua própria instância deCircleseparada , economizando assim uma quantidade significativa de memória.Padrão de design Singleton
O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Esse padrão é útil para gerenciar configurações ou recursos compartilhados em um aplicativo, reduzindo a sobrecarga de memória ao não replicar recursos compartilhados ou dados de configuração em vários locais.
Considere o seguinte
Databaseclasse:Nessa implementação, o
Databasegarante que, não importa quantas vezes ele seja instanciado, apenas uma instância compartilhada será usada, conservando a memória ao evitar vários objetos idênticos.Você pode verificar isso com o código a seguir:
O resultado é o seguinte:
Criação de instância para o padrão de design singleton
A saída "True" indica que
db1edb2referem-se à mesma instância.Esses padrões demonstram como um design cuidadoso pode melhorar significativamente a eficiência da memória. Ao escolher e implementar padrões de design apropriados, os desenvolvedores podem otimizar seus aplicativos Python para que sejam mais enxutos e tenham melhor desempenho.
3. Uso de tipos de dados incorporados em classes
Selecionando o tipo de dados tipo de dados interno apropriado em Python pode afetar significativamente a eficiência da memória. Os tipos incorporados são otimizados para desempenho e uso de memória devido às suas implementações internas no interpretador Python.
Por exemplo, devido às suas propriedades exclusivas, as tuplas e os conjuntos podem ser opções mais eficientes em termos de memória para casos de uso específicos do que as listas e os dicionários.
As tuplas geralmente usam menos memória do que as listas porque têm um tamanho fixo e não podem ser alteradas depois de criadas, permitindo que os mecanismos internos do Python otimizem seu armazenamento na memória. Para classes projetadas para lidar com dados que não precisam ser modificados após a criação, as tuplas são uma excelente opção.
Exemplo prático: Classes de polígonos
Considere duas classes simples de polígono que usam tuplas e listas para armazenar valores:
Podemos criar instâncias de ambas as classes para comparar o uso da memória:
O resultado que você obtém é:
Comparação de memória para tuplas e listas
Esse resultado demonstra que a tupla normalmente usa menos memória do que a lista, o que torna a classe
PolygonWithTuplemais eficiente em termos de memória para armazenar dados fixos e imutáveis, como coordenadas geométricas.4. Aproveitamento de geradores para avaliação preguiçosa
Em Python, geradores são uma ferramenta poderosa para gerenciar a memória de forma eficiente, especialmente quando você trabalha com grandes conjuntos de dados.
Os geradores permitem que os dados sejam processados um item de cada vez, carregando na memória apenas o que for necessário em um determinado momento. Esse método de avaliação preguiçosa é benéfico em aplicativos em que grandes volumes de dados são manipulados, mas apenas uma pequena quantidade de cada vez precisa ser processada, reduzindo assim o espaço total de memória.
Exemplo prático: Processamento de grandes conjuntos de dados
Considere um aplicativo que precise processar um grande conjunto de dados de registros de usuários, como o cálculo da idade média dos usuários. Em vez de carregar todos os registros em uma lista (o que poderia consumir uma quantidade significativa de memória), um gerador pode processar um registro por vez.
Neste exemplo, a função geradora
process_records()lê de um arquivo uma linha de cada vez, converte os dados relevantes em um número inteiro e os produz. Isso significa que apenas uma linha de dados é mantida na memória a qualquer momento, minimizando o uso da memória. A funçãoaverage_age()consome o gerador, acumulando a idade total e a contagem sem nunca precisar de todo o conjunto de dados na memória de uma só vez.Assim, usando a abordagem de avaliação preguiçosa, podemos conservar a memória e aumentar a capacidade de resposta e o dimensionamento dos aplicativos.
5. Evitar referências circulares
As referências circulares ocorrem quando dois ou mais objetos fazem referência uns aos outros. Isso pode acontecer sutilmente com estruturas como listas, dicionários ou até mesmo definições de classes mais complexas, em que as instâncias mantêm referências umas às outras, criando um loop.
Como vimos nos fundamentos do gerenciamento de memória, o Python tem um coletor de lixo adicional para detectar e limpar referências circulares. No entanto, confiar nesse mecanismo pode levar ao aumento do uso de memória e a ineficiências:
Exemplo prático: Uma estrutura de dados em árvore
Considere um caso de uso típico em uma estrutura de dados em forma de árvore em que cada nó pode manter uma referência a seus filhos e, opcionalmente, a seu pai. Essa vinculação pai-filho pode facilmente criar referências circulares.
Neste exemplo, o
rootfaz referência aochildpor meio de sua listachildren, e ochildfaz referência aorootpor meio de seu atributoparent. Essa configuração pode criar uma referência circular sem nenhuma intervenção adicional.Usando referências fracas para evitar referências circulares
As referências fracas são uma maneira eficaz de interromper ou evitar referências circulares sem redesenhar toda a estrutura de dados.
O módulo
weakrefdo Python permite que você faça uma referência a um objeto que não aumente sua contagem de referências. Essa referência fraca não impede que o objeto referenciado seja coletado pelo lixo, evitando vazamentos de memória associados a referências circulares.Veja como podemos modificar a classe anterior para usar referências fracas:
Nessa classe, o atributo pai é agora uma referência fraca ao nó pai. Isso significa que o nó pai pode ser coletado do lixo mesmo que o nó filho ainda exista, desde que não haja outras referências fortes ao pai.
Essa abordagem evita com eficiência os problemas de vazamento de memória normalmente associados a referências circulares em tais estruturas.
Conclusão
Este artigo introduziu os fundamentos do gerenciamento de memória em Python e apresentou cinco técnicas avançadas para escrever classes eficientes em termos de memória, juntamente com exemplos práticos e comparações de memória. Esperamos que você adote essas técnicas e práticas recomendadas em seus projetos para criar aplicativos dimensionáveis e eficientes.
Recomendamos que você confira o nosso Programa Pythonque abrange princípios de engenharia de software e programação orientada a objetos, incluindo escrever código Python eficiente.