Interfaces, Protocolos e Composição

Interfaces, Protocolos e Composição

Por que preferir composição à herança, e como declarar contratos em Python sem obrigar ninguém a herdar. Com as duas ressalvas do Protocol que os materiais omitem: declarado e não anotado ele não faz nada, e o isinstance confere só o nome do método, não a assinatura.
Python

• • 22 min de leitura

No artigo Herança e Polimorfismo vimos que herança é uma ferramenta poderosa — mas não é sempre a melhor escolha. Um princípio amplamente aceito no design de software diz: prefira composição à herança. Neste artigo vamos entender a diferença entre essas abordagens, explorar o sistema de protocolos do Python e aprender a construir sistemas mais flexíveis e fáceis de manter.

O Problema da Herança Excessiva

Herança cria um acoplamento forte entre classes. Considere:

class Ave:
    def respirar(self):
        return "Respirando..."

    def voar(self):
        return "Voando..."

    def cantar(self):
        return "Cantando..."


class Pinguim(Ave):
    def voar(self):
        raise NotImplementedError("Pinguins não voam!")

Vale ver a saída, porque diagnosticar sem corrigir deixa a lição pela metade. O erro de modelagem foi pôr voar na classe que representa o que toda ave é, quando voar é uma capacidade que algumas têm. A correção separa as duas coisas:

class Ave:
    def respirar(self): return "Respirando..."


class CapacidadeVoo:
    def voar(self): return "Voando..."


class Andorinha(Ave, CapacidadeVoo):
    pass

class Pinguim(Ave):
    def nadar(self): return "Nadando..."

Agora nenhuma classe herda um método que precise recusar, e o código que recebe uma ave qualquer não pode chamar voar() sem antes verificar — o que é honesto, porque nem toda ave voa. Repare que o defeito original não estava no Pinguim: estava na Ave, que prometia demais. É o padrão desta classe de problemas — quando uma subclasse precisa desabilitar algo, o erro quase sempre está um nível acima, e é lá que se conserta.

O Pinguim herda voar() de Ave mas não pode implementá-la — viola o Princípio de Substituição de Liskov: uma subclasse deve poder substituir sua classe pai sem quebrar o programa. Esse é o sinal de que herança não é a ferramenta certa aqui.

Composição

Composição significa construir classes a partir de outras classes, incluindo-as como atributos — relação "tem um" em vez de "é um":

class Motor:
    def __init__(self, potencia_cv, combustivel):
        self.potencia_cv = potencia_cv
        self.combustivel = combustivel
        self._ligado = False

    def ligar(self):
        self._ligado = True
        return f"Motor {self.potencia_cv}cv ligado."

    def desligar(self):
        self._ligado = False
        return "Motor desligado."

    @property
    def ligado(self):
        return self._ligado


class SistemaSom:
    def __init__(self, marca, potencia_watts):
        self.marca   = marca
        self.potencia = potencia_watts

    def tocar(self, musica):
        return f"[{self.marca}] Tocando: {musica}"


class Carro:
    """Carro TEM UM motor e TEM UM sistema de som — composição."""

    def __init__(self, modelo, motor, som):
        self.modelo = modelo
        self._motor = motor
        self._som   = som

    def ligar(self):
        return self._motor.ligar()

    def tocar_musica(self, musica):
        if not self._motor.ligado:
            return "Ligue o carro primeiro."
        return self._som.tocar(musica)

    def status(self):
        estado = "ligado" if self._motor.ligado else "desligado"
        return f"{self.modelo} — {estado}"


motor  = Motor(150, "Flex")
som    = SistemaSom("Pioneer", 200)
carro  = Carro("Honda Civic", motor, som)

print(carro.ligar())
print(carro.tocar_musica("Bohemian Rhapsody"))
print(carro.status())

A vantagem: podemos trocar o motor ou o sistema de som sem modificar a classe Carro. Cada componente é independente e reutilizável.

Interfaces Informais: Duck Typing

Python não tem a palavra-chave interface como Java ou C#. Em vez disso, usa duck typing — qualquer objeto que implemente os métodos esperados pode ser usado:

class RelatorioCSV:
    def gerar(self, dados):
        linhas = [",".join(str(v) for v in linha) for linha in dados]
        return "\n".join(linhas)

    def salvar(self, conteudo, caminho):
        print(f"Salvando CSV em {caminho}...")


class RelatorioJSON:
    def gerar(self, dados):
        import json
        return json.dumps(dados, ensure_ascii=False, indent=2)

    def salvar(self, conteudo, caminho):
        print(f"Salvando JSON em {caminho}...")


class RelatorioHTML:
    def gerar(self, dados):
        linhas = "".join(
            f"<tr>{''.join(f'<td>{v}</td>' for v in linha)}</tr>"
            for linha in dados
        )
        return f"<table>{linhas}</table>"

    def salvar(self, conteudo, caminho):
        print(f"Salvando HTML em {caminho}...")


def exportar(relatorio, dados, caminho):
    """Funciona com qualquer objeto que tenha gerar() e salvar()."""
    conteudo = relatorio.gerar(dados)
    relatorio.salvar(conteudo, caminho)
    return conteudo


dados = [["Ana", 9.5], ["Bruno", 8.0], ["Carla", 7.5]]

exportar(RelatorioCSV(),  dados, "notas.csv")
exportar(RelatorioJSON(), dados, "notas.json")
exportar(RelatorioHTML(), dados, "notas.html")

Nenhuma das classes herda de uma interface comum — mas todas funcionam com exportar() porque implementam gerar() e salvar().

Protocolos Formais: typing.Protocol

A partir do Python 3.8, o módulo typing oferece Protocol — uma forma de definir interfaces sem herança, usando structural subtyping (verificação estática pela estrutura):

from typing import Protocol

class Serializavel(Protocol):
    def serializar(self) -> str:
        ...

    def deserializar(self, dados: str) -> None:
        ...


class UsuarioJSON:
    def __init__(self, nome, email):
        self.nome  = nome
        self.email = email

    def serializar(self) -> str:
        import json
        return json.dumps({"nome": self.nome, "email": self.email})

    def deserializar(self, dados: str) -> None:
        import json
        obj = json.loads(dados)
        self.nome  = obj["nome"]
        self.email = obj["email"]


class ConfiguracaoINI:
    def __init__(self):
        self.dados = {}

    def serializar(self) -> str:
        return "\n".join(f"{k}={v}" for k, v in self.dados.items())

    def deserializar(self, dados: str) -> None:
        for linha in dados.strip().split("\n"):
            k, v = linha.split("=")
            self.dados[k.strip()] = v.strip()


def salvar(obj: Serializavel, caminho: str) -> None:
    """Aceita qualquer objeto que implemente o protocolo Serializavel."""
    conteudo = obj.serializar()
    print(f"Salvando em {caminho}: {conteudo}")


usuario = UsuarioJSON("Ana", "ana@email.com")
config  = ConfiguracaoINI()
config.dados = {"tema": "escuro", "idioma": "pt-BR"}

salvar(usuario, "usuario.json")
salvar(config,  "config.ini")

UsuarioJSON e ConfiguracaoINI não herdam de Serializavel — mas ferramentas como mypy verificam estaticamente se implementam os métodos exigidos pelo protocolo.

O Protocol em tempo de execução

Como a verificação é estática, tentar usar um protocolo com isinstance é recusado:

isinstance(UsuarioJSON(), Serializavel)
# TypeError: Instance and class checks can only be used with
# @runtime_checkable protocols

O decorador @runtime_checkable libera a checagem, mas com uma limitação que precisa ser conhecida antes de se confiar nela: a verificação olha apenas os nomes dos métodos, nunca as assinaturas nem os tipos.

from typing import Protocol, runtime_checkable

@runtime_checkable
class Serializavel(Protocol):
    def serializar(self) -> str: ...


class Impostor:
    def serializar(self, a, b, c):   # assinatura completamente diferente
        return 42

isinstance(Impostor(), Serializavel)   # True — passou
Impostor().serializar()
# TypeError: Impostor.serializar() missing 3 required positional
# arguments: 'a', 'b' and 'c'

Ou seja, o isinstance com protocolo responde tem um método com esse nome, e não cumpre este contrato. Quem garante o contrato é o verificador estático, e para isso ele precisa de uma coisa que é fácil esquecer: a anotação. Um Protocol declarado e nunca usado como tipo de parâmetro não faz absolutamente nada — nem em tempo de execução, onde anotações não são verificadas, nem na análise estática, que não tem onde comparar. O protocolo só ganha utilidade quando aparece na assinatura de quem recebe o objeto, como em def exportar(relatorio: Serializavel, ...).

Mixins

Mixins são classes pequenas e focadas que adicionam funcionalidades específicas via herança múltipla — sem representar uma entidade completa:

class LogMixin:
    """Adiciona capacidade de logging a qualquer classe."""

    def log(self, mensagem, nivel="INFO"):
        from datetime import datetime
        agora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{agora}] [{nivel}] [{type(self).__name__}] {mensagem}")


class ValidacaoMixin:
    """Adiciona validação de campos obrigatórios."""

    campos_obrigatorios = []

    def validar(self):
        erros = []
        for campo in self.campos_obrigatorios:
            valor = getattr(self, campo, None)
            if not valor:
                erros.append(f"Campo '{campo}' é obrigatório.")
        return erros


class SerializacaoMixin:
    """Adiciona serialização automática para dicionário."""

    def para_dict(self):
        return {
            k: v for k, v in self.__dict__.items()
            if not k.startswith("_")
        }


class Pedido(LogMixin, ValidacaoMixin, SerializacaoMixin):
    campos_obrigatorios = ["cliente", "produto", "quantidade"]

    def __init__(self, cliente, produto, quantidade):
        self.cliente    = cliente
        self.produto    = produto
        self.quantidade = quantidade

    def processar(self):
        erros = self.validar()
        if erros:
            for erro in erros:
                self.log(erro, "ERRO")
            return False

        self.log(f"Processando pedido: {self.produto} x{self.quantidade}")
        return True


pedido = Pedido("Ana", "Teclado Mecânico", 2)
pedido.processar()
print(pedido.para_dict())

pedido_invalido = Pedido("", "Mouse", 0)
pedido_invalido.validar()
pedido_invalido.processar()

Composição vs. Herança: quando usar cada uma

Situação Use
A relação é claramente "é um" (Cachorro é um Animal) Herança
A relação é "tem um" (Carro tem um Motor) Composição
Quer adicionar comportamento sem criar hierarquia Mixin
Quer definir um contrato sem forçar herança Protocol
A hierarquia tem mais de 2 níveis Reveja o design
Subclasse precisa desabilitar métodos do pai Composição

Exemplo Completo: Sistema de Notificações

from typing import Protocol
from datetime import datetime


class Canal(Protocol):
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        ...


class CanalEmail:
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        print(f"[EMAIL] Para: {destinatario} | {mensagem}")
        return True


class CanalSMS:
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        if len(mensagem) > 160:
            print(f"[SMS] Mensagem muito longa para {destinatario}")
            return False
        print(f"[SMS] Para: {destinatario} | {mensagem}")
        return True


class CanalPush:
    def enviar(self, destinatario: str, mensagem: str) -> bool:
        corte = mensagem[:50] + ("..." if len(mensagem) > 50 else "")
        print(f"[PUSH] Para: {destinatario} | {corte}")
        return True


class LogMixin:
    def _registrar(self, canal, destinatario, sucesso):
        status = "OK" if sucesso else "FALHOU"
        agora  = datetime.now().strftime("%H:%M:%S")
        print(f"  [{agora}] {canal} → {destinatario}: {status}")


class ServicoNotificacao(LogMixin):
    """Usa composição — recebe canais como dependências."""

    def __init__(self, canais: list[Canal]):
        self._canais = list(canais)   # cópia: veja a ressalva após o exemplo

    def notificar(self, destinatario: str, mensagem: str):
        resultados = {}
        for canal in self._canais:
            nome   = type(canal).__name__
            sucesso = canal.enviar(destinatario, mensagem)
            self._registrar(nome, destinatario, sucesso)
            resultados[nome] = sucesso
        return resultados

    def adicionar_canal(self, canal):
        self._canais.append(canal)


servico = ServicoNotificacao([
    CanalEmail(),
    CanalSMS(),
    CanalPush(),
])

print("=== Notificação de boas-vindas ===")
servico.notificar("ana@email.com", "Bem-vinda à plataforma!")

print("\n=== Alerta de segurança ===")
servico.notificar(
    "bruno@email.com",
    "Detectamos um acesso suspeito na sua conta. "
    "Se não foi você, altere sua senha imediatamente."
)

Duas decisões no construtor merecem explicação, porque as duas versões naturais de escrevê-lo têm defeito. A primeira é a anotação: o Canal foi declarado como Protocol no começo do exemplo, mas escrever canais: list o deixa sem uso nenhum — o protocolo não é verificado em tempo de execução, e sem aparecer na assinatura também não dá o que verificar ao mypy. Anotando list[Canal], a ferramenta passa a acusar, antes de rodar, quem tentar passar um objeto sem o método enviar.

A segunda é a cópia. Guardar self._canais = canais faz o objeto ficar com a mesma lista que o chamador tem na mão, e não com uma lista sua:

meus_canais = [CanalEmail(), CanalSMS()]
servico = ServicoNotificacao(meus_canais)

servico.adicionar_canal(CanalPush())

len(meus_canais)   # 3 — a lista de quem chamou cresceu sozinha

Pior: dois serviços construídos a partir da mesma lista passam a compartilhá-la, e acrescentar um canal a um altera o outro. Guardar list(canais) resolve, e o princípio vale muito além deste exemplo — quando um objeto recebe uma coleção mutável e vai guardá-la, ele precisa decidir explicitamente entre compartilhar e copiar. Copiar é quase sempre a resposta certa, e é a que não surpreende ninguém.

O conselho de preferir composição à herança se resume a uma pergunta que se responde antes de escrever a classe: a relação é é um ou tem um? Herança amarra a filha à forma interna da mãe, e o sinal mais claro de que ela foi a escolha errada é a subclasse precisar recusar um método herdado — como o pinguim que não voa. Quando isso acontece, o defeito quase nunca está na filha: está na classe de cima, que prometeu a todos uma capacidade que só alguns têm. A composição não tem esse problema porque as peças são independentes: dá para trocar o motor sem abrir o carro, e para testar cada componente isolado.

Do lado dos contratos, o Python oferece uma escala. O duck typing não exige nada e funciona; o Protocol descreve o contrato de forma verificável sem obrigar ninguém a herdar; o ABC obriga. Duas ressalvas sobre o Protocol valem mais que a definição: ele só serve de alguma coisa quando aparece como anotação de quem recebe o objeto — declarado e não usado, não faz nem efeito estático nem efeito nenhum em execução —, e o isinstance, liberado por @runtime_checkable, confere apenas os nomes dos métodos, não as assinaturas. Um objeto com o método certo e os parâmetros errados passa na checagem e quebra na chamada.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um serviço recebe uma lista de processadores no construtor e a guarda com self._processadores = processadores. Em produção, um serviço passou a executar processadores que ninguém configurou nele. Investigando, descobre-se que a aplicação cria três serviços a partir de uma mesma lista de padrões e cada um acrescenta os seus. Explique e corrija.

Ver resposta

✓ Resposta: Atribuir self._processadores = processadores não guarda uma lista do objeto: guarda uma referência à mesma lista que o chamador tem. Os três serviços criados a partir da lista de padrões ficaram todos apontando para o mesmo objeto, de modo que servico_a.adicionar(x) alterou a lista que servico_b e servico_c também enxergam — e alterou, de quebra, a lista de padrões da aplicação, que passou a crescer a cada serviço criado. É possível confirmar com servico_a._processadores is servico_b._processadores, que devolve True. O comportamento é o mesmo que já apareceu em cópia rasa de lista e em argumento padrão mutável: a lista é um objeto, atribuir um nome a ela não a duplica, e alterar por qualquer um dos nomes alcança o único objeto que existe. A correção é copiar na entrada, com self._processadores = list(processadores). Isso resolve os dois sintomas de uma vez, porque cada serviço passa a ter a sua, e a lista de padrões deixa de ser alcançável de dentro. Aceitar list(...) em vez de exigir uma lista tem ainda um ganho de projeto: o construtor passa a aceitar qualquer iterável — uma tupla, um gerador, as chaves de um dicionário —, o que costuma ser o que se quer de um parâmetro de entrada. O princípio geral vale além deste caso: quando um objeto recebe uma coleção mutável e vai guardá-la, ele precisa decidir explicitamente entre compartilhar e copiar, e não decidir é decidir por compartilhar. Copiar é quase sempre a resposta certa. Quando a lista é grande demais para copiar, a alternativa honesta é guardar uma versão imutável — uma tupla — ou documentar de forma bem visível que o objeto passa a ser dono daquela lista.

Exercício 2

Um time define class Exportavel(Protocol) com o método exportar, anota as funções que recebem exportadores e adiciona @runtime_checkable para poder validar em execução com isinstance. Um objeto passa no isinstance e quebra com TypeError na chamada seguinte. Explique o que o isinstance verifica de fato e o que não verifica.

Ver resposta

✓ Resposta: O isinstance com um protocolo @runtime_checkable verifica apenas se o objeto possui atributos com os nomes declarados no protocolo. Ele não olha assinatura, não olha quantidade de parâmetros, não olha tipos de argumento nem de retorno. Uma classe com def exportar(self, a, b, c) passa na checagem tão bem quanto a implementação correta, e só quebra quando alguém chama obj.exportar() e recebe TypeError: ... missing 3 required positional arguments. A razão é que essas informações vivem nas anotações, que existem para análise estática, e o isinstance é uma verificação de execução que se limita a inspecionar o objeto — perguntar existe um atributo chamado assim? é barato, comparar assinaturas em tempo de execução não seria. Sem o @runtime_checkable, aliás, a tentativa nem chega a rodar: levanta TypeError: Instance and class checks can only be used with @runtime_checkable protocols, o que é a linguagem avisando que aquele não é o mecanismo de verificação do Protocol. A conclusão prática é que o isinstance com protocolo responde tem um método com esse nome, e não cumpre este contrato. Quem garante o contrato é o verificador estático, rodando mypy ou pyright na integração contínua — e ele só consegue garantir se a anotação estiver na assinatura de quem recebe o objeto. Um Protocol declarado e nunca usado como tipo de parâmetro não faz nada em lugar nenhum. Vale registrar a escolha de ferramenta que isso sugere: quando a validação precisa mesmo acontecer em execução, porque o objeto vem de um plugin ou de configuração externa, o ABC com @abstractmethod é mais adequado, porque impede a instanciação de uma implementação incompleta, com mensagem que nomeia o método faltante.

Exercício 3

Uma classe Relatorio herda de list para poder usar append e iteração diretamente. Meses depois, alguém descobre que é possível chamar relatorio.sort(), relatorio.clear() e relatorio.pop() de qualquer lugar, desfazendo as invariantes que a classe tentava manter. Analise a decisão de herdar de list e proponha a alternativa.

Ver resposta

✓ Resposta: Herdar de list importa a interface inteira, e não apenas as duas ou três operações que interessavam. Junto com append e a iteração vieram clear, sort, pop, insert, __setitem__ e a fatia atribuível, cada uma capaz de alterar a coleção sem passar por nenhuma validação da classe. Não existe forma de remover um método herdado — sobrescrevê-lo para levantar exceção é justamente a violação do princípio de substituição que este artigo descreve, porque quem recebe a classe acreditando ser uma lista passa a encontrar erros onde a lista funcionaria. O sintoma descrito é, portanto, o comportamento esperado da decisão: a classe declarou eu sou uma lista e é tratada como tal. A alternativa é composição: guardar a lista como atributo interno, self._itens = [], e expor apenas o que a classe quer garantir — um adicionar que valida antes de inserir, uma __iter__ que delega a iteração, e um __len__. A superfície passa a ser escolhida em vez de herdada, e cada nova operação é uma decisão consciente. Havendo mesmo necessidade de a classe se comportar como uma sequência para o resto do sistema, o caminho correto não é herdar de list, e sim de collections.abc.Sequence, que exige apenas __getitem__ e __len__ e fornece o restante da interface de leitura — index, count, in, iteração, fatiamento — sem trazer nada que modifique. É exatamente o caso de uso da linha da tabela deste artigo que manda usar composição quando a subclasse precisaria desabilitar métodos do pai. Vale a observação final de que herdar de tipos nativos tem ainda uma armadilha extra: métodos internos como __init__ de list e extend não chamam o append sobrescrito, de modo que validações escritas em append são contornadas sem que ninguém perceba.

Exercício 4

Explique por que a classe Ave com o método voar() é um erro de modelagem, e por que corrigir o Pinguim — fazendo voar() retornar uma string vazia em vez de levantar exceção — não resolve o problema, apenas o esconde.

Ver resposta

✓ Resposta: O erro está em confundir o que toda ave é com o que algumas aves fazem. Pôr voar() na classe base é declarar que voar faz parte da definição de ave, e isso é falso para pinguins, avestruzes, emas e kiwis — não é um caso excepcional, é uma fatia inteira do domínio. A consequência aparece no princípio de substituição de Liskov: o código que recebe uma Ave qualquer tem o direito de chamar voar(), porque a classe prometeu esse método, e essa chamada precisa funcionar para toda subclasse. Se alguma recusa, a promessa era mentirosa, e quem escreveu o código que consome não tinha como saber. Sobre a correção proposta: fazer voar() devolver string vazia troca um erro barulhento por um erro silencioso, o que é uma piora. O programa deixa de estourar e passa a produzir resultado errado — um relatório de aves que voaram incluindo pinguins com a distância em branco, um contador que soma zero sem avisar que o dado não existe. O NotImplementedError ao menos aponta o lugar exato do problema no instante em que ele ocorre; o retorno vazio empurra a consequência para longe da causa. A correção de verdade é na modelagem, e é mover a capacidade para fora da base: uma classe Ave com o que toda ave tem, e a capacidade de voo numa classe própria que só as aves voadoras combinam — ou, em vez de herança, um atributo que descreva as capacidades do animal. Nas duas formas, o código que consome não pode mais chamar voar() sem antes verificar, e isso é uma vantagem, não um incômodo: a verificação existe porque a realidade a exige. A lição transferível é a de sempre nesta classe de defeitos — quando a subclasse precisa recusar algo, o conserto é um nível acima, na base que prometeu demais.

Exercício 5

Um sistema de notificações precisa suportar canais novos escritos por equipes diferentes, que não podem alterar o código do serviço central. Compare três desenhos possíveis — herança de uma classe base, ABC com método abstrato, e Protocol — dizendo o que cada um exige de quem escreve o canal novo e quando o erro de uma implementação incompleta aparece.

Ver resposta

✓ Resposta: Os três funcionam, e a escolha se decide por duas perguntas: quanto se exige de quem escreve o canal, e quando o engano aparece. A herança de uma classe base concreta é a que mais exige e a que menos garante. Obriga o autor do canal a importar e herdar da classe do serviço central, o que cria dependência de código entre as equipes e acopla o canal a detalhes internos da base; e não garante nada, porque a base tem implementações que a subclasse pode simplesmente não sobrescrever — o canal novo é aceito, roda e não faz nada, ou faz o que a base fazia. O erro, quando aparece, aparece em produção e sem mensagem. O ABC com @abstractmethod mantém a exigência de herdar, mas troca a garantia: não implementar enviar torna a classe impossível de instanciar, com TypeError nomeando o método faltante. É a opção certa quando o serviço carrega canais dinamicamente, vindos de plugins ou de configuração, porque a verificação acontece em execução, no momento da construção, que é onde o controle está. O preço continua sendo o acoplamento: a equipe do canal precisa importar a base. O Protocol é o que menos exige e o que melhor separa: o autor do canal não importa nada do serviço central e não herda de nada — basta escrever uma classe com enviar(self, destinatario: str, mensagem: str) -> bool. A verificação é estática, feita por mypy ou pyright na integração contínua de quem escreve o canal, e a mensagem de erro é precisa, apontando a incompatibilidade de assinatura, coisa que nenhuma das outras duas detecta. A contrapartida é que, sem essa ferramenta rodando, não há verificação alguma — e o isinstance com @runtime_checkable não cobre a lacuna, porque só confere nomes de métodos. Para o cenário descrito, em que as equipes são independentes e não devem depender do código central, o Protocol é a melhor escolha, com verificação estática obrigatória no processo de integração. Se os canais forem carregados por configuração em tempo de execução, o desenho mais seguro combina os dois: Protocol para o contrato e uma validação explícita na hora de registrar o canal, conferindo o que de fato importa antes de aceitá-lo.

Comentários

Mais em Python

Estruturas de Controle: if, elif e else
Estruturas de Controle: if, elif e else

Decidir é o que separa um script de um programa. O if, o elif e o else, a…

A História do Python e os Primeiros Passos
A História do Python e os Primeiros Passos

De um projeto de férias de Natal em 1989 à linguagem que hoje sai em versão…

Laços de Repetição: for e while
Laços de Repetição: for e while

O for do Python não conta: percorre. A diferença parece cosmética e muda o…