As tuplas são um dos tipos de sequência incorporados em Python, semelhantes às listas, mas com uma diferença fundamental: são imutáveis. Isso significa que, uma vez criadas, não é possível alterar, adicionar ou remover elementos. Essa característica confere às tuplas propriedades importantes, como a capacidade de serem usadas como chaves em dicionários e garantirem que os dados não sejam modificados acidentalmente. Além disso, as tuplas são frequentemente mais eficientes em termos de desempenho e memória do que as listas para coleções de valores que não precisam ser alteradas.

Nesta aula, vamos mergulhar nos conceitos de imutabilidade, empacotamento e desempacotamento, discutir quando é apropriado usar tuplas em vez de listas e apresentar as named tuples, que adicionam nomes aos campos, tornando o código mais legível e autodocumentado.

Imutabilidade

A imutabilidade das tuplas é a sua característica mais marcante. Diferente das listas, que podem ser modificadas in-place (adicionando, removendo ou alterando elementos), as tuplas não oferecem esses métodos. Uma vez criada, a tupla permanece a mesma. Isso tem implicações importantes:

  • As tuplas podem ser usadas como chaves em dicionários, pois são hasháveis (desde que todos os seus elementos também sejam imutáveis e hasháveis). Listas não podem ser chaves.
  • As tuplas garantem a integridade dos dados, evitando modificações acidentais. Isso é útil para constantes ou configurações que não devem mudar.
  • Internamente, o Python pode otimizar o uso de memória para tuplas, tornando-as mais leves que listas.

Exemplo de criação e tentativa de modificação:

# Criando uma tupla
coordenadas = (10, 20)
print(coordenadas[0])  # 10

# Tentando alterar um elemento (gera erro)
# coordenadas[0] = 30  # TypeError: 'tuple' object does not support item assignment

# Tuplas podem conter objetos mutáveis, como listas, mas a própria tupla permanece imutável
mix = (1, [2, 3], "a")
# mix[0] = 10  # Erro, pois a tupla é imutável
# Mas podemos modificar a lista interna:
mix[1].append(4)
print(mix)  # (1, [2, 3, 4], "a")

Empacotamento e desempacotamento

Empacotamento é o processo de agrupar múltiplos valores em uma única tupla. Isso acontece automaticamente quando você separa valores por vírgula e os atribui a uma variável. O desempacotamento é o processo inverso: extrair os valores de uma tupla e atribuí-los a variáveis individuais.

Exemplo de empacotamento:

# Empacotamento: criando uma tupla sem parênteses
dados = 10, 20, 30
print(type(dados))  # <class 'tuple'>
print(dados)        # (10, 20, 30)

Desempacotamento básico:

# Desempacotamento
x, y, z = dados
print(x, y, z)  # 10 20 30

O desempacotamento é muito útil em loops e funções que retornam múltiplos valores. Python também suporta o operador * para desempacotar partes de uma sequência (desempacotamento estendido):

# Desempacotamento estendido
primeiro, *resto = (1, 2, 3, 4)
print(primeiro)  # 1
print(resto)     # [2, 3, 4]

# Ignorando valores com _
a, _, b = (10, 20, 30)
print(a, b)  # 10 30

Quando usar

Escolher entre tupla e lista depende do propósito. Use tuplas quando:

  • Você precisa de uma coleção de itens que não deve ser alterada (imutabilidade).
  • Deseja usar a coleção como chave de dicionário.
  • Quer garantir que os dados sejam somente leitura, evitando bugs de modificação acidental.
  • Precisa de desempenho ligeiramente melhor (tuplas são mais rápidas que listas para iteração e criação).
  • Está lidando com dados heterogêneos que representam uma estrutura fixa (ex.: coordenadas (x, y), registros de banco de dados).

Use listas quando a coleção precisa ser dinâmica: adicionar, remover ou modificar elementos.

Exemplo prático: retorno de função que retorna múltiplos valores:

def dividir(a, b):
    quociente = a // b
    resto = a % b
    return quociente, resto  # retorna uma tupla

q, r = dividir(10, 3)
print(f"Quociente: {q}, Resto: {r}")  # Quociente: 3, Resto: 1

Named tuples (visão geral)

As named tuples são uma extensão das tuplas padrão, fornecidas pelo módulo collections. Elas permitem que você crie classes de tupla com campos nomeados, tornando o código mais legível e autodocumentado. Cada instância de uma named tuple se comporta como uma tupla normal, mas também permite acesso aos campos por nome, além do índice.

Exemplo de criação e uso:

from collections import namedtuple

# Definindo uma named tuple chamada 'Ponto' com campos 'x' e 'y'
Ponto = namedtuple('Ponto', ['x', 'y'])

# Criando instâncias
p1 = Ponto(10, 20)
p2 = Ponto(x=30, y=40)

# Acessando por índice
print(p1[0], p1[1])  # 10 20

# Acessando por nome
print(p2.x, p2.y)    # 30 40

# Desempacotamento funciona normalmente
x, y = p1
print(x, y)          # 10 20

# Named tuples são imutáveis
# p1.x = 100  # AttributeError

As named tuples são ideais para representar registros simples, como pontos em um gráfico, coordenadas, ou dados de configuração. Elas ocupam a mesma memória que tuplas normais e são mais eficientes que dicionários para acesso a atributos.

Boas práticas

  • Prefira tuplas a listas para dados que não devem ser alterados.
  • Use desempacotamento para tornar o código mais limpo e expressivo.
  • Evite criar tuplas com um único elemento sem a vírgula: (1,) é uma tupla, (1) é um inteiro.
  • Para estruturas de dados com campos nomeados, considere usar named tuples ou dataclasses (Python 3.7+).
  • Lembre-se de que tuplas são hasháveis apenas se todos os seus elementos forem hasháveis.

Referências

Exercícios

  1. Crie uma tupla chamada cores com as strings "vermelho", "verde" e "azul". Em seguida, tente alterar o primeiro elemento para "amarelo" e observe o erro. Explique por que o erro ocorre.

    ✓ Resposta:
    cores = ("vermelho", "verde", "azul")
    # cores[0] = "amarelo"  # TypeError: 'tuple' object does not support item assignment
    O erro ocorre porque tuplas são imutáveis em Python. Uma vez criadas, não é possível modificar seus elementos.
  2. Dada a tupla dados = (10, 20, 30, 40, 50), use desempacotamento para atribuir o primeiro elemento a a, o último a b e os elementos restantes a uma lista resto.

    ✓ Resposta:
    dados = (10, 20, 30, 40, 50)
    a, *resto, b = dados
    print(a)     # 10
    print(b)     # 50
    print(resto) # [20, 30, 40]
    O operador * captura os elementos intermediários em uma lista.
  3. Escreva uma função chamada min_max que recebe uma tupla de números e retorna uma tupla contendo o menor e o maior valor. Teste com nums = (3, 1, 7, 0, 5).

    ✓ Resposta:
    def min_max(nums):
        return min(nums), max(nums)
    
    nums = (3, 1, 7, 0, 5)
    menor, maior = min_max(nums)
    print(f"Menor: {menor}, Maior: {maior}")  # Menor: 0, Maior: 7
  4. Crie uma named tuple chamada Aluno com campos nome e nota. Crie uma instância para "Ana" com nota 9.5 e imprima o nome e a nota usando acesso por nome.

    ✓ Resposta:
    from collections import namedtuple
    
    Aluno = namedtuple('Aluno', ['nome', 'nota'])
    aluno = Aluno(nome="Ana", nota=9.5)
    print(f"Nome: {aluno.nome}, Nota: {aluno.nota}")  # Nome: Ana, Nota: 9.5
  5. Explique por que a tupla t = (1, [2, 3]) é imutável, mas podemos modificar a lista interna. O que acontece com a hashabilidade da tupla?

    ✓ Resposta: A tupla t é imutável porque não podemos adicionar, remover ou substituir seus elementos. No entanto, a lista interna [2, 3] é mutável, então podemos modificar seu conteúdo (ex.: t[1].append(4)). Isso não altera a tupla em si, mas modifica o objeto ao qual ela se refere. Consequentemente, a tupla não é hashável, pois contém um objeto mutável. Tentar usar hash(t) resultará em TypeError.