Python gerencia a memória automaticamente, mas entender como isso funciona é essencial para escrever programas eficientes e evitar vazamentos. Nesta aula, vamos mergulhar no mecanismo de contagem de referências, que é a base do gerenciamento de memória em CPython, e depois explorar o módulo gc que lida com ciclos de referências. Você também aprenderá sobre os vazamentos de memória mais comuns e como evitá-los.

Vamos começar com o conceito fundamental: cada objeto em Python tem uma contagem de referências que é incrementada quando uma nova referência é criada e decrementada quando uma referência é removida. Quando essa contagem chega a zero, o objeto é imediatamente destruído e a memória é liberada. Esse modelo é simples e eficiente, mas tem uma limitação: ciclos de referências podem impedir a liberação de memória. Para resolver isso, Python inclui um coletor de lixo cíclico, implementado no módulo gc.

Contagem de referências

Em CPython, cada objeto possui um campo ob_refcnt que armazena o número de referências ao objeto. Quando você faz uma atribuição, passa um objeto como argumento ou o adiciona a uma lista, a contagem é incrementada. Quando uma variável sai do escopo ou é sobrescrita, a contagem é decrementada. Quando chega a zero, o objeto é liberado imediatamente.

Vamos ver como isso funciona na prática. O módulo sys oferece a função getrefcount() que retorna a contagem atual de referências. Note que a própria chamada à função temporariamente incrementa a contagem, então o valor real é o retornado menos 1.

import sys

# Criando uma referência
x = []
print(sys.getrefcount(x))  # Saída: 2 (1 de x + 1 da chamada)

y = x
print(sys.getrefcount(x))  # Saída: 3 (x, y e a chamada)

del y
print(sys.getrefcount(x))  # Saída: 2 (x e a chamada)

del x
# Agora o objeto está sem referências e é destruído

Quando o objeto é destruído, o método __del__ (se definido) é chamado. Isso pode ser útil para liberar recursos externos, mas cuidado: se você criar um ciclo com objetos que possuem __del__, o coletor de lixo pode não conseguir coletá-los, causando vazamento.

Ciclos

Um ciclo de referências ocorre quando dois ou mais objetos se referenciam mutuamente, formando um ciclo. Por exemplo, uma lista que contém a si mesma ou dois objetos que se apontam. Nesses casos, a contagem de referências nunca chega a zero, mesmo que não haja mais nenhuma referência externa ao ciclo. Isso significa que a memória não é liberada.

# Exemplo de ciclo simples
class Node:
    def __init__(self):
        self.other = None

# Criando dois nós que se referenciam mutuamente
a = Node()
b = Node()
a.other = b
b.other = a

# Removendo as referências externas
del a
del b
# O ciclo permanece, e a memória não é liberada

Para resolver isso, Python usa um coletor de lixo cíclico que detecta e coleta esses ciclos. Ele funciona periodicamente ou pode ser invocado manualmente via gc.collect(). O coletor rastreia objetos que podem participar de ciclos (como containers) e usa um algoritmo de marcação e varredura para identificar ciclos inacessíveis.

gc module

O módulo gc fornece funções para controlar o coletor de lixo. Você pode usá-lo para desabilitar a coleta automática, forçar uma coleta, inspecionar objetos rastreados e ajustar o limiar de geração.

import gc

# Desabilitar a coleta automática (não recomendado em produção)
gc.disable()

# Forçar uma coleta manual
gc.collect()

# Ver o número de objetos rastreados por geração
print(gc.get_count())

# Obter estatísticas detalhadas
print(gc.get_stats())

O coletor de lixo do Python é geracional: objetos são divididos em três gerações (0, 1 e 2). Novos objetos vão para a geração 0. Se sobrevivem a uma coleta, são promovidos para a próxima geração. O coletor coleta a geração 0 com mais frequência que a 1, e a 1 mais que a 2. Isso otimiza o desempenho, pois a maioria dos objetos é temporária.

Você pode ajustar os limiares usando gc.set_threshold(). Por exemplo, gc.set_threshold(700, 10, 5) define que a geração 0 é coletada quando há 700 objetos, a geração 1 quando há 10 coletas da geração 0, e a geração 2 quando há 5 coletas da geração 1.

Vazamentos comuns

Vazamentos de memória em Python geralmente não são vazamentos no sentido tradicional (memória não é liberada), mas sim referências acidentais que mantêm objetos vivos. Alguns padrões comuns incluem:

  • Variáveis globais e caches persistentes: manter referências em estruturas globais ou caches sem controle.
  • Callbacks e closures: funções que capturam variáveis de escopo externo e são mantidas em listas de eventos, impedindo a liberação desses objetos.
  • Objetos com __del__ em ciclos: ciclos de referências com objetos que possuem __del__ não podem ser coletados automaticamente, pois o coletor não sabe a ordem de destruição.

Vamos ver um exemplo de vazamento por cache:

# Cache que nunca é limpo
cache = {}

def process_data(key, data):
    # Processa dados e armazena no cache
    cache[key] = data

# Uso repetido com chaves únicas
for i in range(100000):
    process_data(i, {"data": i})

# cache agora tem 100000 entradas, consumindo muita memória

Para evitar vazamentos, você pode usar weakref para criar referências fracas, que não incrementam a contagem de referências. Isso é útil para caches que não devem impedir a coleta de objetos. O módulo weakref permite criar referências que não mantêm o objeto vivo.

import weakref

class Data:
    pass

obj = Data()
ref = weakref.ref(obj)
print(ref() is obj)  # True

del obj
print(ref() is None)  # True (objeto foi coletado)

Boas práticas e observações finais

  • Evite manter referências globais a objetos grandes ou temporários.
  • Use weakref para caches e estruturas que não devem impedir a coleta.
  • Evite definir __del__ em classes que podem participar de ciclos; prefira usar weakref.finalize para limpeza.
  • Monitore o uso de memória da sua aplicação e identifique picos anormais.
  • Em aplicações de longa duração, considere desabilitar a coleta automática e executar gc.collect() em momentos controlados.

Referências

Exercícios

  1. Explique como a contagem de referências funciona em Python e por que ela não é suficiente para gerenciar ciclos de referências.
  2. ✓ Resposta: A contagem de referências incrementa um contador cada vez que uma referência a um objeto é criada e decrementa quando uma referência é removida. Quando o contador chega a zero, o objeto é imediatamente destruído. No entanto, se dois objetos se referenciam mutuamente, suas contagens nunca chegam a zero, mesmo que não haja referências externas, pois cada um mantém o outro vivo. Isso impede a liberação da memória, exigindo um coletor de lixo cíclico.
  3. Escreva um programa que crie um ciclo de referências e use o módulo gc para detectar e coletar o ciclo. Mostre que a memória é liberada após a coleta.
  4. ✓ Resposta:
    import gc
    
    class Node:
        pass
    
    # Criar ciclo
    a = Node()
    b = Node()
    a.ref = b
    b.ref = a
    
    # Remover referências externas
    del a, b
    
    # Verificar que o ciclo existe
    print("Objetos antes da coleta:", gc.collect())  # Força coleta e retorna número de objetos coletados
    
  5. Qual é a diferença entre referências fortes e fracas? Dê um exemplo de uso de weakref para evitar vazamento de memória em um cache.
  6. ✓ Resposta: Referências fortes mantêm o objeto vivo; referências fracas não impedem a coleta. No cache, podemos usar weakref.WeakValueDictionary para que os valores sejam coletados quando não houver mais referências fortes. Exemplo:
    import weakref
    
    class Data: pass
    
    cache = weakref.WeakValueDictionary()
    
    def get_data(key):
        if key in cache:
            return cache[key]
        obj = Data()
        cache[key] = obj
        return obj
    
  7. Explique o que são gerações no coletor de lixo e como podemos ajustar os limiares para otimizar a coleta.
  8. ✓ Resposta: O coletor divide os objetos em três gerações (0, 1, 2). Objetos novos vão para a geração 0; se sobrevivem a uma coleta, são promovidos. A geração 0 é coletada com mais frequência. Os limiares podem ser ajustados com gc.set_threshold(threshold0, threshold1, threshold2), controlando quando cada geração é coletada. Por exemplo, aumentar o threshold0 reduz a frequência de coleta, o que pode melhorar o desempenho se houver muitos objetos temporários.
  9. Descreva duas situações comuns de vazamento de memória em Python e como evitá-las.
  10. ✓ Resposta: 1) Manter referências em caches globais sem limite: usar weakref ou implementar uma política de expiração. 2) Callbacks que capturam variáveis de escopo externo e são mantidos em listas de eventos: remover o callback quando não for mais necessário, ou usar referências fracas. Além disso, evitar __del__ em ciclos, pois impede a coleta.