Variáveis: o que são e como o Python as gerencia na memória

1. O que é uma variável em Python?

Em Python, uma variável é essencialmente um nome que referencia um objeto na memória. Diferente de linguagens como C ou C++, onde a variável é uma "caixa" que armazena um valor diretamente, em Python a variável funciona como um rótulo ou etiqueta que aponta para um objeto.

Quando escrevemos:

x = 42

O Python realiza duas operações:
1. Cria um objeto int com valor 42 no heap (memória dinâmica)
2. Associa o nome x a esse objeto

Isso significa que a variável não contém o valor 42 — ela apenas "aponta" para o objeto que contém esse valor. Essa distinção é fundamental para entender como Python gerencia memória.

2. Atribuição e reatribuição de variáveis

O mecanismo de atribuição em Python é chamado de binding (vinculação). Quando fazemos:

nome = "Maria"
idade = 30

Criamos dois objetos (str e int) e vinculamos os nomes a eles. A grande flexibilidade de Python é que uma variável pode referenciar objetos de tipos diferentes ao longo do programa:

x = 10          # x aponta para um int
print(type(x))  # <class 'int'>

x = "texto"     # x agora aponta para uma string
print(type(x))  # <class 'str'>

Isso ocorre porque variáveis não possuem tipo fixo — o tipo pertence ao objeto, não ao nome que o referencia.

3. Gerenciamento de memória: alocação e referências

Python gerencia a memória automaticamente. Objetos são alocados dinamicamente no heap, e cada objeto mantém um contador de referências que rastreia quantos nomes (ou outras estruturas) apontam para ele.

a = [1, 2, 3]    # Cria uma lista; contagem de referências = 1
b = a            # b aponta para o mesmo objeto; contagem = 2
c = a            # c também aponta; contagem = 3

Podemos verificar a identidade do objeto com id() e a contagem de referências com sys.getrefcount():

import sys

x = [10, 20]
print(sys.getrefcount(x))  # Contagem inclui a referência temporária da função

4. Coleta de lixo (Garbage Collection)

Quando a contagem de referências de um objeto chega a zero, Python o desaloca automaticamente. Esse é o mecanismo principal de gerenciamento de memória.

def exemplo():
    temp = [1, 2, 3]  # Objeto criado
    print(temp)
    # Ao sair da função, 'temp' é destruído
    # Contagem de referências chega a zero → objeto desalocado

exemplo()

No entanto, existe um problema: referências circulares. Duas listas que se referenciam mutuamente nunca terão contagem zero, mesmo que não sejam mais acessíveis:

a = []
b = []
a.append(b)  # a → b
b.append(a)  # b → a (referência circular)

Python resolve isso com um coletor de lixo cíclico, implementado no módulo gc. Podemos forçar a coleta manualmente:

import gc
gc.collect()  # Força a coleta de objetos inacessíveis

5. Mutabilidade e imutabilidade: impacto na memória

Objetos em Python podem ser mutáveis ou imutáveis, e isso afeta diretamente o uso de memória.

Objetos imutáveis (int, str, tuple, frozenset): qualquer operação que pareça modificar o objeto na verdade cria um novo objeto:

x = "Python"
print(id(x))  # Endereço de memória

x = x + " 3"
print(id(x))  # Novo endereço — um novo objeto foi criado

Objetos mutáveis (list, dict, set, bytearray): podem ser alterados sem mudar de identidade:

lista = [1, 2, 3]
print(id(lista))  # Endereço original

lista.append(4)
print(id(lista))  # Mesmo endereço — objeto foi modificado in-place

Essa diferença é crucial para performance: modificar uma lista grande é mais eficiente que "modificar" uma tupla grande.

6. Compartilhamento de objetos e o operador is

Variáveis diferentes podem apontar para o mesmo objeto — isso é chamado de aliasing:

a = [1, 2, 3]
b = a          # b é um alias de a
b.append(4)
print(a)       # [1, 2, 3, 4] — a também foi modificado!

Para diferenciar entre igualdade de valor e identidade de objeto, usamos:

  • == : compara se os valores são iguais
  • is : compara se são o mesmo objeto na memória
x = [1, 2]
y = [1, 2]
print(x == y)  # True (mesmo valor)
print(x is y)  # False (objetos diferentes)

Python otimiza memória com interning para inteiros pequenos (-5 a 256) e strings curtas:

a = 256
b = 256
print(a is b)  # True (interning)

c = 257
d = 257
print(c is d)  # False (cada um é um objeto separado)

7. Escopo de variáveis e a tabela de símbolos

Python segue a regra LEGB para resolução de nomes:

  • Local: escopo da função atual
  • Enclosing: escopo de funções externas (closures)
  • Global: escopo do módulo
  • Built-in: nomes embutidos como print, len

Cada escopo mantém uma tabela de símbolos (um dicionário) que mapeia nomes a objetos:

x = 10  # Variável global

def funcao():
    y = 20  # Variável local
    print(locals())  # {'y': 20}

funcao()
print(globals()['x'])  # 10 — acessando pela tabela global

Variáveis locais são criadas na entrada da função e descartadas na saída (a menos que sejam capturadas por closures):

def contador():
    count = 0
    def incrementar():
        nonlocal count
        count += 1
        return count
    return incrementar

c = contador()
print(c())  # 1
print(c())  # 2
# 'count' é preservado pelo closure

Referências