Imutabilidade significa que um objeto não pode ser alterado após criado. Qualquer modificação gera um novo objeto na memória. Primeiramente, isso evita efeitos colaterais indesejados em funções. Por exemplo, tuplas são imutáveis, enquanto listas são mutáveis. Além disso, frozenset e namedtuple também seguem esse princípio. A voz passiva é usada aqui: “os dados originais são preservados para sempre”. Quando utilizar imutabilidade? Em chaves de dicionários e conjuntos. Também em programação funcional e dados compartilhados entre threads. Python oferece estruturas imutáveis nativas para esses casos. Vamos explorar tuplas, frozenset e namedtuple em detalhes. Três subtítulos guiarão você pelo mundo da imutabilidade. Ao final, você escolherá a estrutura certa para cada situação.
Tuplas: listas imutáveis
Tuplas são sequências imutáveis de elementos em Python.
Você cria uma tupla com parênteses: (1, 2, 3).
Quando usar tuplas? Em dados que não devem mudar.
Por exemplo, coordenadas geográficas ou cores RGB.
A voz passiva é aplicada: “os elementos são acessados por índice”.
Exemplo de tuplas e suas características:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# Criando tuplas vazia = () um_elemento = (42,) # Vírgula é obrigatória! coordenadas = (10, 20) cores = ("vermelho", "verde", "azul") misturada = (1, "texto", 3.14, True) # Acesso e operações print("=== Tuplas ===\n") print(f"coordenadas[0]: {coordenadas[0]}") print(f"coordenadas[1]: {coordenadas[1]}") # Fatiamento (slicing) print(f"cores[1:3]: {cores[1:3]}") print(f"cores[-1]: {cores[-1]}") # Imutabilidade (não pode alterar) try: coordenadas[0] = 99 except TypeError as e: print(f"Erro ao modificar: {e}") # Tupla como chave de dicionário (listas não podem!) pontos = {} pontos[(10, 20)] = "Ponto A" pontos[(30, 40)] = "Ponto B" print(f"Dicionário com chaves tuplas: {pontos}") # Desempacotamento x, y = coordenadas print(f"Desempacotado: x={x}, y={y}") # Retornando múltiplos valores (comum em funções) def dividir_com_resto(a, b): return (a // b, a % b) # Retorna tupla quociente, resto = dividir_com_resto(17, 5) print(f"17/5: quociente={quociente}, resto={resto}") # Comparação com listas (performance) import timeit lista_teste = [1, 2, 3] tupla_teste = (1, 2, 3) tempo_lista = timeit.timeit(lambda: lista_teste[1], number=10_000_000) tempo_tupla = timeit.timeit(lambda: tupla_teste[1], number=10_000_000) print(f"\nAcesso: lista={tempo_lista:.4f}s, tupla={tempo_tupla:.4f}s") |
Tuplas são mais rápidas e consomem menos memória que listas. Use tuplas sempre que os dados forem fixos e imutáveis.
Frozenset: conjuntos imutáveis
Frozenset é a versão imutável de um set (conjunto). Ele suporta operações de conjunto sem alterar os dados. Quando usar frozenset? Como chave de dicionário. Também para armazenar dados únicos que não devem mudar. A voz passiva é aplicada: “os elementos são únicos e imutáveis”. Exemplo de frozenset:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# Criando frozensets set_normal = {1, 2, 3, 3, 4} # Set mutável frozen = frozenset([1, 2, 3, 3, 4, 5]) print("=== Frozenset ===\n") print(f"set_normal: {set_normal}") print(f"frozenset: {frozen}") # Operações de conjunto (retornam novos frozensets) a = frozenset([1, 2, 3, 4]) b = frozenset([3, 4, 5, 6]) print(f"a: {a}") print(f"b: {b}") print(f"União: {a | b}") print(f"Interseção: {a & b}") print(f"Diferença: {a - b}") print(f"Diferença simétrica: {a ^ b}") # Imutabilidade (não pode adicionar) try: frozen.add(10) except AttributeError as e: print(f"Erro ao adicionar: {e}") # Frozenset como chave de dicionário categorias = {} categorias[frozenset(["ativo", "premium"])] = "VIP" categorias[frozenset(["inativo"])] = "Regular" print(f"\nDicionário com chaves frozenset: {categorias}") # Verificação de subconjunto print(f"a é subconjunto de a|b? {a.issubset(a | b)}") print(f"a é subconjunto de b? {a.issubset(b)}") # Frozenset como chave para cache cache_resultados = {} def calcular_chave(*args): return frozenset(args) def fibonacci_com_cache(n): chave = calcular_chave(n) if chave in cache_resultados: return cache_resultados[chave] if n < 2: resultado = n else: resultado = fibonacci_com_cache(n-1) + fibonacci_com_cache(n-2) cache_resultados[chave] = resultado return resultado print(f"\nFibonacci(10) com cache: {fibonacci_com_cache(10)}") print(f"Chaves no cache: {list(cache_resultados.keys())}") |
Frozenset é ideal para conjuntos constantes e chaves de dicionário. Ele mantém todas as operações úteis dos sets sem a mutabilidade.
NamedTuple: tuplas com nomes de campos
NamedTuple combina imutabilidade de tupla com nomes de campos. Ela funciona como uma classe leve e imutável. Quando usar NamedTuple? Em estruturas de dados simples. Por exemplo, pontos, pessoas ou produtos imutáveis. A voz passiva é aplicada: “os campos são acessados por nome ou índice”. Exemplo de NamedTuple:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
from collections import namedtuple from typing import NamedTuple # Python 3.6+ # Forma clássica (collections.namedtuple) Ponto = namedtuple('Ponto', ['x', 'y']) Pessoa = namedtuple('Pessoa', 'nome idade cidade') # String com espaços print("=== NamedTuple (clássico) ===\n") p = Ponto(10, 20) pessoa = Pessoa("Ana", 30, "São Paulo") print(f"Ponto: {p}") print(f"Ponto.x: {p.x}, Ponto.y: {p.y}") print(f"Ponto[0]: {p[0]}, Ponto[1]: {p[1]}") print(f"\nPessoa: {pessoa}") print(f"Nome: {pessoa.nome}, Idade: {pessoa.idade}, Cidade: {pessoa.cidade}") # Imutabilidade try: p.x = 99 except AttributeError as e: print(f"Erro ao modificar: {e}") # Métodos úteis print(f"\n_replace (cria novo): {p._replace(x=99)}") print(f"Original permanece: {p}") print(f"_asdict: {pessoa._asdict()}") print(f"Campos: {pessoa._fields}") # NamedTuple com valores padrão (Python 3.6+) class Config(NamedTuple): host: str = "localhost" porta: int = 8080 debug: bool = False config1 = Config() config2 = Config(porta=9090, debug=True) print(f"\n=== NamedTuple (moderno) ===") print(f"Config padrão: {config1}") print(f"Config custom: {config2}") # NamedTuple com docstring e validação (Python 3.6+) class Produto(NamedTuple): nome: str preco: float estoque: int def __repr__(self): return f"Produto('{self.nome}', R${self.preco:.2f}, {self.estoque})" produto = Produto("Notebook", 2500.00, 10) print(f"\nProduto: {produto}") print(f"Preço: R${produto.preco:.2f}") # Exemplo prático: dados de sensores Sensor = namedtuple('Sensor', ['id', 'temperatura', 'umidade', 'timestamp']) def ler_sensor(id_sensor): """Simula leitura de sensor (retorna NamedTuple imutável).""" import time return Sensor( id=id_sensor, temperatura=22.5, umidade=65, timestamp=time.time() ) sensor_data = ler_sensor(1) print(f"\n=== Dados do Sensor ===") print(f"ID: {sensor_data.id}") print(f"Temperatura: {sensor_data.temperatura}°C") print(f"Umidade: {sensor_data.umidade}%") print(f"Timestamp: {sensor_data.timestamp:.2f}") # Comparação com dataclasses (mutáveis por padrão) from dataclasses import dataclass @dataclass class ProdutoMutavel: nome: str preco: float mutavel = ProdutoMutavel("Mouse", 50) imutavel = Produto("Teclado", 120, 5) mutavel.preco = 45 # Permite modificação print(f"\n=== Mutável vs Imutável ===") print(f"Produto mutável (preço alterado): {mutavel.preco}") print(f"Produto imutável: {imutavel}") |
NamedTuple oferece a melhor experiência para dados imutáveis. A fórmula da escolha entre estruturas imutáveis: \(E = \begin{cases} \text{tupla} & \text{simples e sem nomes} \\ \text{frozenset} & \text{operações de conjunto} \\ \text{namedtuple} & \text{acesso por nome} \end{cases}\) Imutabilidade não é apenas uma restrição, é uma garantia. Use-a para tornar seu código mais seguro e previsível. Tuplas para sequências simples e rápidas. Frozenset para conjuntos únicos que servem como chaves. NamedTuple para objetos leves com campos nomeados. Seu código será mais robusto e fácil de entender.