Iteráveis

Um iterável é qualquer objeto que pode ser percorrido, item por item, em um loop for ou utilizado em outras construções que requerem iteração, como map(), filter(), etc.  A principal característica de um iterável é que ele implementa o método especial __iter__() ou o método __getitem__().

Nem todo iterável é uma coleção, como por exemplo geradores e arquivos. Quando uma coleção é Ordenável significa que mantém a ordem de inserção dos elementos, ou seja, ao iterar sobre, os itens aparecem na mesma ordem em que foram adicionados.

Coleção Ordenada Iterável Sequencial Mutável
Listas Sim Sim Sim Sim
Tuplas Sim Sim Sim Não
Strings Sim Sim Sim Não
Conjuntos Não Sim Não Sim
Dicionários Sim (a partir do Python 3.7) Sim Não Sim

Iterável → Um objeto que pode ser percorrido (for x in obj:)
Iterador → Um objeto/MECANISMO que armazena a posição atual e retorna elementos com next().

Em uma analogia um iterável é como um um livro que sozinho ele não sabe “te dar a próxima página”. E um Iterador um marcador de página. Ele sabe onde você está e consegue te entregar a próxima página quando você pede (next()).
Nenhuma dessas coleções acima é um iterador (Eles têm __iter__() → podem gerar um iterador. Mas não têm __next__() → não percorrem sozinhos), a não ser que você crie um pelo metodo iter(nome_da_coleção), se isso for feito em uma lista, por exemplo, o metodo  __iter__() dela, retorna um objeto iterador (no caso, um list_iterator). Mas quando percorre uma lista por um for, internamente é criado um iterador para saber o proximo item.
 
Principais Diferenças
Característica Iterável Iterador
Implementa __iter__() ou __getitem__() (mais antigo) __iter__()__next__()
Pode ser usado em for? ✅ Sim ✅ Sim, se tiver o __iter__()
Pode ser usado com next()? ❌ Não ✅ Sim
Retorna o próprio iterador? ❌ Não (ele cria um novo) ✅ Sim (__iter__() retorna self)
Armazena estado da iteração? ❌ Não ✅ Sim

 

iterador.py
class Contador:
    def __init__(self, tamanho):
        self.cont = 0
        self.tamanho = tamanho
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.cont < self.tamanho:
            self.cont += 1
            return self.cont
        else:
            raise StopIteration
 
cont = Contador(3)

# chamada automatica do next
for i in cont:
    print(i)

# chamada manual de next
print(cont.__next__()) # chamo diretamente
print(next(cont)) # o metodo built in chama internamente a mesmo metodo __next__ acima: def next(iterator): return iterator.__next__()

# Transformar uma lista em um iterador no Python é uma tarefa simples 
numeros = [1, 2, 3, 4, 5]
num_iterador = iter(numeros) # Cria um iterador para percorrer os números
print(type(num_iterador))  # <class 'list_iterator'>, retorna uma instância de uma classe interna do Python, otimizada e já pronta para iterar sobre listas.
print(next(num_iterador))  # consumo a lista de forma sequencial

Criação de um objeto iterador proprio.

iteravel_iterador.py
lst = [10, 20, 30]
it = iter(lst) # Aqui criei um iterador para a lista acima.
print(type(lst))  # <class 'list'>
print(type(it))   # <class 'list_iterator'>


# Internamente quando é feito um for em um iterável.
for x in [10, 20, 30]:
    print(x)

# Quando é feito um for como acima, o Python internamente faz isso
_iter = iter([10, 20, 30])   # cria o list_iterator
while True:
    try:
        x = next(_iter)      # pega o próximo item
    except StopIteration:
        break
    print(x)

Criando um iterador manualmente a partir de um iteravel.
it não é uma copia da lista, é apenas um objeto (list_iterator - iterador especializado em percorrer listas) apontando para a lista e guardando o progresso.

No segundo caso, quando o for termina (ou ocorre break), o iterador temporário é descartado (não é mais referenciado e o garbage collector pode limpar depois). A lista original nunca é alterada nesse processo.


Sequências

Em Python, sequências (subtipo de iteráveis) são tipos de dados que mantêm a ordem de seus elementos e permitem acessar elementos por índice. Elas são um subconjunto das coleções e são projetadas para armazenar dados de forma ordenada.

Tipos de sequências em Python incluem:

  1. Listas (list)
  2. Tuplas (tuple)
  3. Strings (str)
  4. range (embora não seja uma coleção tradicional, é considerado uma sequência)

Características das sequências:

  • Ordenação: A ordem dos elementos é importante e preservada.
  • Indexação: Você pode acessar elementos por índice (por exemplo, lista[0]).
  • Imutabilidade/Mutabilidade: Algumas sequências são mutáveis (listas), enquanto outras são imutáveis (tuplas, strings).

Geradores

Um gerador é um tipo específico de iterável, criado para gerar itens "sob demanda". Ele é mais eficiente para grandes conjuntos de dados, pois só mantém um item na memória de cada vez (produzem os valores de forma preguiçosa - lazy evaluation). Esse itens são consumiveis, o que significa que, uma vez que você iterar sobre um item gerado, ele não estará mais disponível.

A palavra-chave yield transforma uma função em geradora. Quando uma função com yield é chamada, Python cria um objeto de gerador que salva:

  • A posição atual no código (linha do último yield).
  • O estado das variáveis locais (como em um ponto de salvamento).

Yield: palavra chave que a cada vez que é encontrada, a função pausa e salva o estado atual. (incluindo variáveis locais e a posição na função) "lembra de onde parou". para que ela possa continuar na próxima vez em que for chamada.

Cada vez que um gerador é chamado, ele gera um item de cada vez, até que não haja mais itens a serem produzidos. Assim, quando next() é chamado novamente, a execução continua exatamente do ponto em que parou. quando chamamos next e todos os elementos foram consumidos gera o erro StopIteration.

Expressão geradora é uma maneira concisa de criar geradores, semelhante a uma compreensão em lista. gera valores um de cada vez economizando memória.

geradores.py
def contagem():
    print("Iniciando contagem")
    yield 1 # Retorna o valor, mas ao contrario de return ela não encerra a função.
    print("Contando 2")
    yield 2
    print("Contando 3")
    yield 3
    print("Contagem finalizada")

# Cria o gerador
contador = contagem()

# Usando o gerador com next()
print(next(contador))  # Saída: "Iniciando contagem" e "1"
print(next(contador))  # Saída: "Contando 2" e "2"
print(next(contador))  # Saída: "Contando 3" e "3"

# Se não tiver mais valor o valor padrão None é retornado para não gerar o erro StopIteration,
print(next(contador, None))  # Saída: "Contagem finalizada" e "None"
for_gerador.py
# O for internamente chama a função next() e já trata o erro StopIteration quando os iteráveis acabam
for numero in contagem():
    print(numero)
send_geradores.py
def contador():
    valor = 0
    while True:
        incremento = yield valor  # Pausa e espera um valor externo
        if incremento is None:
            incremento = 1  # Se nada for enviado, assume 1
        valor += incremento

# Criando o gerador
gen = contador()

# Iniciando o gerador (necessário antes de usar send)
print(next(gen))  # Saída: 0

# Enviando valores para o gerador
print(gen.send(3))  # Saída: 3 (0 + 3)
print(gen.send(2))  # Saída: 5 (3 + 2)
print(gen.send(5))  # Saída: 10 (5 + 5)
print(next(gen))    # Saída: 11 (10 + 1, pois None vira 1)

O método .send() é usado para enviar valores para um gerador que contém yield. Isso permite comunicação bidirecional entre o gerador e quem está chamando. Nesse caso o gerador começa em 0 (next(gen)).A cada .send(valor), o gerador recebe um número e soma ao valor atual. Se None for enviado, ele soma 1 por padrão. yield pausa e retorna o valor, esperando o próximo .send().

| Expressão Geradora vs Compreensão em Lista

expressao_geradora.py
# Cria um gerador, observe o uso de parênteses ao invés de colchetes 
quadrados = (x ** 2 for x in range(5))

# ela salva o objeto onde parou
print(next(quadrados))  # Saída: 0
print(next(quadrados))  # Saída: 1
print(next(quadrados))  # Saída: 4
compreensao_lista.py
# Compreensão em lista. Salva todos os valores na memoria
quadrados = [x ** 2 for x in range(5)]

Desempacotamento (Unpacking)

O desempacotamento (ou unpacking) é uma técnica poderosa que permite extrair os valores de um iterável (como listas, tuplas ou dicionários) e atribuí-los diretamente a variáveis de forma concisa.

starred variable (variável estrelada) é uma variável de desempacotamento com asterisco. O operador * é usado para agrupar múltiplos valores em uma lista. Esse recurso é útil quando você quer capturar uma parte específica da lista e agrupar o restante dos itens em uma única variável.

desempacotamento.py
# desempacotamento de tupla
tupla = (1, 2, 3)
a, b, c = tupla

# Itera sobre as variáveis descompactada anteriormente
for valor in [a, b, c]:
    print(valor)

dic = {
    'nome': 'Diogo',
    'idade': '28'
}

# Descompacotamento em dicionário. Duas Strings (chave e valor)
for chave, valor in dic.items():
    print(chave, valor)
desempacotamento_aninhado.py
nomes = ['Diogo', 'Roberto', 'Rodrigo']
idades = [20, 30, 19]

# desempacotamento aninhado (nesting)
for i, (nome, idade) in enumerate(zip(nomes, idades)):
    print(i, nome, idade) 
# Saída: 
# 0 Diogo 20 
# 1 Roberto 30 
# 2 Rodrigo 19

# Sem desempacotamento
for e in enumerate(zip(nomes,idades)):
    print(e) # Saída: (0, ('Diogo', 20)) (1, ('Roberto', 30)) (2, ('Rodrigo', 19))
starred_variable_unpacking.py
minha_lista = [1, 2, 3, 4, 5, 6]

# o asterisco retorna uma lista, abaixo, o meio é uma lista com valor 3 e 4
primeiro, segundo, *meio, penultimo, ultimo = minha_lista # saída: 1 2 [3, 4] 5 6

# Primeiro e ultimo da lista uso *_ por convenção
primeiro, *_, ultimo = minha_lista # Saída (se imprimir sem o _): 1 6. Saída (com _ ): 1 [2, 3, 4, 5] 6

# Se não sobrar valores para preencher, ele retorna uma lista vazia
minha_lista_b = [1, 2]
num_primeiro, *metade, num_ultimo = minha_lista_b # Saída ao imprimir: 1 [] 2

Exemplos com Starred Variable.


Range

O range é um tipo embutido em Python (uma classe) usado para gerar uma sequência de números inteiros. Não é um iterador mas pode ser iterado (é um iterável), e é muito eficiente pois não armazena todos os números na memória (lazy), ele gera cada número apenas quando necessário. Por isso é comumente usado em laços for. Também é random-access, ou seja, pode acessar qualquer posição sem precisar consumir do início, o que um gerador não faz.

Sintaxe:

    range(start, stop, step)

  • start (opcional): O valor inicial da sequência (padrão é 0).
  • stop (obrigatório): O valor final, não incluso na sequência.
  • step (opcional): O intervalo entre os números (padrão é 1).
range.py
r = range(1, 10, 2)
print(r[0])  # Saída: 1
print(r[2])  # Saída: 5
r = range(10**12)   # É lazy, não ocupa memória de 1 trilhão de números!

# Diferenças de um range para um gerador
print(r[999_999_999])  # acesso direto, funciona
print(len(r)) # também funciona

Listas

Coleções ordenadas (sequênciais) que podem ser alteradas (mútaveis).

É possível obter o último item da lista com números negativos (o último elemento tem índice -1, o penúltimo tem índice -2 e etc… )

lista.py
numeros = [10, 20, 30, 40]

# Imprime uma lista completamente
print(numeros)

# Acessa o primeiro elemento da lista
print(numeros[0])

# imprime o primeiro caracter da String ja que uma string é tratada como uma lista em python
print("Ola"[0])

cidades = ["Palmas", "Chicago", "LEM"]

# Retorna LEM, que é o ultimo item da lista
print(cidades[-1])

# Soma de Listas (+=)
l_a = [1, 2, 3]
l_b = [4, 5, 6]
l_a+=l_b
print(l_a) # Saída
metodos_lista.py
numeros = [10, 20, 30, 40]
numeros2 = [50,60,]

# Adiciona um novo elemento à lista
numeros.append(50)

# Adiciona uma lista a outra lista
numeros.extend(numeros2)
 
# Remove o último elemento
numeros.pop()

# Limpa a lista
numeros2.clear()

# reordena a lista sortida em ordem crescente
numeros.sort()

# inverte a ordem da lista
numeros.reverse()

lista = ["Banana","Laranja","Maça","Limão" ]
# Retorna o índice que este item está na lista (primeira ocorrência), também presente na tupla e string.
lista.index("Maça")

# Copia a lista, assim evita modificar a lista original (como ao atribuir com =, pois é uma referencia)
copia_lista = lista.copy()

Os principais métodos de uma lista


Tuplas

Coleção ordenada (sequência) imútaveis. Atribuir valor à tupla vai gerar um erro (tupla[0] = 10). 

Obs.: Sempre que criar uma tupla com um único elemento, adicione uma vírgula ao final para evitar que o python interprete como uma tupla e não como um valor isolado.

tupla.py
ponto = (4, 5)

# Acessa o primeiro elemento da tupla
print(ponto[0])

# Desempacotando uma tupla
a, b = (1, 2)

# Criar tupla de um único elemento
tupla_real = ("Diogo",)

Dicionários

Dicionários são coleções de pares chave-valor. São úteis para armazenar dados associativos.

dicionario.py
# Define um dicionário vazio, ou limpa/zera um dicionário
pessoa = {}

# Define um dicionário
pessoa = {
    "nome": "João", 
    "idade": 25,
}

# imprime o dicionário completo com chave e valor
print(pessoas)

# Acessa o valor associado à chave "nome"
print(pessoa["nome"])

# Adiciona um novo elemento/par chave-valor
pessoa["cidade"] = "São Paulo"
metodos_dicionario.py
pessoa = {
    "nome": "João", 
    "idade": 25,
}
# recupera o valor de uma chave, equivalente ao uso dos colchetes, mas se a chave não existir retorna None
pessoa.get("nome")

# mostra todas as chaves do dicionário, retorna um dict_keys
pessoa.keys()

# mostra todos os valores, retorna um dict_values
pessoa.values()

# retorna uma lista de tuplas, com chave e valor, um dict_items
pessoa.items()

# Atualiza um valor ou adiciona valores ao dicionário
pessoa.update({"nome":"pedro", "sobrenome": "Carmelo"}) # atualiza o nome e adiciona o sobrenome
pessoa.update(altura=1.75, profissao="dentista") # pode ser atualizado com argumentos nomeados também

# remove item do dicionário
pessoa.pop("altura", "not found") # retorna a chave removida, pode personalizar o retorno se não a chave não existir (2º argumento)
del pessoa["altura"] # Não retorna nada, e não tem personalização de retorno

# remove todos os itens
pessoa.clear()

# Exclui o dicionário inteiro
del pessoa

Os principais métodos de um dicionário. 


Conjuntos (Sets)

Conjuntos são mutáveisnão ordenados (pois é possivel adicionar e excluir elementos), mas seus elementos precisam ser Imutáveis, únicos (hashable) . Os conjuntos aceitam diversos tipos de dados simultaneamente!

Conjuntos não aceitam listas, dicionários e etc, como valor (pois são multaveis), mas podem aceitar tuplas (contanta que os elementos da tupla sejam imutáveis também).

O função built-in set(), recebe um iterável e retorna um conjunto com os elementos únicos do iterável. Bastante usado para tirar valores repetidos de uma lista.

Os conjuntos tem melhor performance de execução do que listas.

conjunto.py
# cria um conjunto
conjunto = {
    1, 2, 3, 4,
}

# Conjunto com vários tipos de dados
conjuntoEcletico = {
    1, 'Diogo', 2.5, True
}

print(conjunto)
metodos_conjunto.py
conjunto = {
    1, 2, 3, 4,
}

# Adiciona um elemento ao conjunto
conjunto.add(5)

# Remove um elemento do conjunto
conjunto.remove(3)

# Adiciona vários elementos de uma vez
conjunto.update([3, 4, 5]) # adiciona só o 5, pois já tem o 3 e o 4.

# Cria um conjunto vazio
conjunto2 = set()

lista = [3, 4, 5]

# converte uma lista em um conjunto
conjunto2 = set(lista)

# Adiciona valores (ordena automático) e não adiciona valores repetidos
conjunto2.add(6)

# União
conjunto.union(conjunto2)

# União com operador
uniao = conjunto1 | conjunto2

# Intersecção
conjunto.intersection(conjunto2)

# Intersecção com operador
interseccao = conjunto & conjunto2

# Diferença de conjuntos (mostra o que tem no conjunto a que não tem em conjunto2)
conjunto.difference(conjunto2)

# Diferença com operador
diferenca = conjunto - conjunto2

# diferença simétrica (mostra tudo o que não tem em comum entre os dois conjuntos)
conjunto.symmetric_difference(conjunto2)

# Diferença simétrica com operador
diferenca_simetrica = (conjunto-conjunto2) | (conjunto2-conjunto)
performance_conjunto.py
numeros = list(range(1_000))

%timeit 500 in numeros  # Saída: 11.8 μs (microsegundos)

# Converte em conjunto
conjunto_velocidade = set(numeros)

%timeit 500 in conjunto_velocidade # Saída: 101 ns

Teste de velocidade em conjunto comparado à lista. Nesse teste, o conjunto foi cerca de 116.83 vezes mais rápido que a lista, ou seja, cerca de 1/117 do tempo da busca na lista


Aninhamento (Nesting)

O aninhamento é uma técnica essencial em programação que permite organizar fluxos de controle, dados ou elementos visuais de forma hierárquica.

Aninhamento de Estruturas de Dados como listas, dicionários ou tuplas, podem ser aninhadas para representar informações complexas.

aninhamento.py
# Nesting em estrutura de dados
viagens_log = [
    {
        "pais": "Brasil",
        "visitas": 10,
        "cidades": ["Rio", "Sao Paulo", "Lem"]
    },
    {
        "pais": "França",
        "visitas": 3,
        "cidades": ["Paris", "Lile"]
    }]