Pular para conteúdo

Python

Python é uma linguagem de propósito geral, interpretada (compilada para bytecode), orientada a objetos, de tipagem dinâmica e forte, famosa pela legibilidade. É a linguagem dominante em ciência de dados e machine learning (Machine Learning), muito usada em automação, scripts, backend (Django, FastAPI, Flask) e infraestrutura.

Sobre a versão

A apostila-fonte usa o Python 3.6. Esta página usa o Python 3 atual (3.10+): onde algo mudou (f-strings, dataclass, dicionários ordenados), há uma nota "Hoje". Os trechos marcados como complemento não estão na apostila. A teoria geral de orientação a objetos (encapsulamento, herança, polimorfismo, SOLID) está em Orientação a Objetos; aqui fica o que é específico do Python.

História, interpretador e versões

Python foi criada em 1990 por Guido van Rossum no CWI (Holanda), como sucessora da linguagem ABC, com a ideia de ser simples, legível e expressiva (por isso a indentação é obrigatória: ela define os blocos). Desde 2001 é mantida pela Python Software Foundation (PSF) e todas as versões são de código aberto.

Interpretada ou compilada?

Os dois. O CPython compila o código-fonte para bytecode (arquivos .pyc na pasta __pycache__) e a Python Virtual Machine (PVM) o executa. Não é preciso compilar manualmente — acontece sozinho; o .pyc é reutilizado se o .py não mudou. É um processo parecido com o do Java (Java), só que sem um passo separado de compilação.

Implementação O que é
CPython A implementação de referência (a que se instala de python.org)
PyPy Escrita em Python, com compilador JIT; costuma ser mais rápida
Jython / IronPython Geram bytecode para a JVM / para o .NET

Definição: PEP

PEP (Python Enhancement Proposal) é uma proposta de melhoria da linguagem, avaliada pela comunidade. A PEP 0 lista todas; a PEP 8 é o guia de estilo e a PEP 20 ("The Zen of Python", import this) resume a filosofia.

Python 2 x Python 3

Python 2 está descontinuado (fim de vida em 2020); use sempre Python 3. Diferenças que ainda aparecem em código legado:

Tema Python 2 Python 3
print comando (print "oi") função (print("oi"))
Entrada raw_input() input()
Divisão 5 / 2 → 2 5 / 2 → 2.5 (use // para divisão inteira)
Classes class A(object): class A: (herda de object implicitamente)
Strings bytes por padrão str é Unicode

Ferramentas do dia a dia

python3 --version
python3                 # modo interativo (REPL): testa trechos, prompt >>>
python3 programa.py     # modo script: executa um arquivo .py
python3 -m venv .venv   # complemento: cria um ambiente virtual isolado
pip install requests    # complemento: instala pacotes

O modo interativo serve para testar ideias rapidamente; o modo script é o de desenvolvimento. help(objeto) e dir(objeto) mostram a documentação e os membros de qualquer coisa. Complemento: use ambiente virtual (venv) por projeto para isolar dependências.

Ao ler um erro, comece pelo traceback (rastro de pilha): a última linha diz o tipo e a mensagem do erro (SyntaxError, NameError...) e as anteriores mostram a sequência de chamadas, da mais antiga para a mais recente.

Sintaxe básica

Variáveis e tipos embutidos

Uma variável é um nome que referencia um valor (uma "etiqueta"), e o tipo pertence ao valor, não à variável (tipagem dinâmica). O tipo é descoberto com type().

mensagem = 'oi, python'     # str
numero = 5                  # int
pi = 3.14                   # float
z = 2 + 3j                  # complex (j é a unidade imaginária)
ativo = True                # bool
nada = None                 # NoneType: ausência de valor

print(type(numero))         # <class 'int'>
  • Nomes: letras, números e _, sem começar por número e sem usar palavra reservada (class, if, for, None...; veja keyword.kwlist). A convenção é snake_case (numero_de_cadastro).
  • Strings aceitam aspas simples ou duplas. '2' e '3.2' são texto, não números.
  • Não confunda = (atribuição) com == (comparação).

Operadores

Operador Significado Exemplo
+ - * soma, subtração, multiplicação (+ e * também funcionam em strings e listas) 'ab' * 3 → 'ababab'
/ divisão (sempre float) 7 / 2 → 3.5
// divisão inteira 7 // 2 → 3
% resto (útil para testar divisibilidade) 7 % 3 → 1
** potência 2 ** 10 → 1024
== != < > <= >= comparação 2 > 1 → True
and or not lógicos a and not b
in, not in pertencimento 'a' in 'banana' → True
is, is not identidade (mesmo objeto) x is None

== x is

== compara o conteúdo; is compara a identidade (se são o mesmo objeto na memória). Duas listas iguais são ==, mas não is:

x = [1, 2, 3]
y = [1, 2, 3]
x == y     # True
x is y     # False

Use is para comparar com None (if valor is None).

Strings, input e formatação

texto = 'python'
texto.upper()            # 'PYTHON'
texto.capitalize()       # 'Python'
'  banana\n'.strip()      # 'banana'  (remove espaços e quebras de linha nas pontas)

nome = input('Digite seu nome: ')    # sempre devolve str!
idade = int(input('Idade: '))        # converta para número quando precisar

int('caelum') lança ValueError. Para montar mensagens:

print('Seu nome é {} e sua idade é {}'.format(nome, idade))   # .format (apostila)
print(f'Seu nome é {nome} e sua idade é {idade}')              # Hoje: f-string (Python 3.6+)
print(f'{245.2346:.2f}')                                       # 245.23 (duas casas decimais)

Valores "verdadeiros" (truthy) e None

bool(valor) converte qualquer valor: são falsos 0, 0.0, '' (string vazia), [], {}, set(), (), None e False; o resto é verdadeiro. Por isso se escreve if lista: em vez de if len(lista) > 0:. None representa "sem valor" e é um objeto de verdade (não um ponteiro nulo). É o padrão para parâmetros opcionais.

Condicionais e laços

if chute == numero_secreto:
    print('Você acertou!')
elif chute > numero_secreto:
    print('Seu chute foi maior')
else:
    print('Seu chute foi menor')

# Dica de legibilidade: dar nome às condições
acertou = chute == numero_secreto
maior = chute > numero_secreto
  • Indentação obrigatória (4 espaços por convenção): ela é a sintaxe do bloco.
  • while repete enquanto a condição for verdadeira — cuidado com o laço infinito se a condição nunca mudar. break sai do laço; continue pula para a próxima volta; pass é um bloco vazio.
  • for percorre qualquer sequência/iterável. range(inicio, fim, passo) gera inteiros e o fim não é incluído:
for rodada in range(1, 4):          # 1, 2, 3
    print(rodada)

for n in range(1, 10, 2):           # 1, 3, 5, 7, 9
    print(n)

for indice, letra in enumerate('abc'):   # complemento: índice + valor
    print(indice, letra)

# compreensão de lista (list comprehension): forma concisa de criar listas
letras_acertadas = ['_' for _ in 'banana']      # ['_','_','_','_','_','_']
pares = [n for n in range(10) if n % 2 == 0]

Estruturas de dados

Tipo Sintaxe Mutável? Ordenado? Notas
list (lista) [1, 2, 3] sim sim (por posição) Pode misturar tipos
tuple (tupla) (1, 2, 3) ou 1, 2, 3 não sim É a vírgula que cria a tupla; pode ser chave de dicionário
range range(1, 10) não sim Guarda só início, fim e passo (não a lista inteira)
str 'texto' não sim Sequência de caracteres
set (conjunto) {1, 2, 3} / set() sim não Sem repetidos; {} vazio é um dict!
dict (dicionário) {'nome': 'João'} sim por inserção* Pares chave-valor

*Hoje: a apostila trata o dict como sem ordem; desde o Python 3.7 ele preserva a ordem de inserção (conjuntos continuam sem ordem garantida).

Sequências: índices, fatias e métodos de lista

As sequências (list, tuple, range, str) compartilham operações: x in s, s + t, s * n, s[i], s[i:j], s[i:j:k], len, min, max, s.count(x).

lista = [2, 3, 5, 7, 11]
lista[0]          # 2          (índices começam em 0)
lista[-1]         # 11         (índice negativo conta do fim)
lista[:2]         # [2, 3]     (fatia: o fim não é incluído)
lista[2:]         # [5, 7, 11]
lista[-3:]        # [5, 7, 11]
lista[10]         # IndexError

lista.append(13)          # acrescenta um item no fim
lista.extend([17, 19])    # acrescenta vários (o mesmo que lista += [17, 19])
lista.pop()               # remove e devolve o último (ou o da posição informada)
  • Listas são mutáveis; tuplas e strings não (dias[0] = 'dom' → TypeError). Prefira tupla para dados que não devem mudar. Uma tupla que contém uma lista ainda pode ter essa lista alterada por dentro.
  • list('python') e tuple(lista) convertem iteráveis.

Conjuntos

a = set('abacate')
b = set('abacaxi')
a - b      # diferença            a | b   # união
a & b      # interseção           a ^ b   # diferença simétrica

Servem para remover duplicados e testar pertencimento rapidamente.

Dicionários

pessoa = {'nome': 'João', 'idade': 25, 'cidade': 'São Paulo'}
pessoa['nome']                  # 'João'
pessoa['pais'] = 'Brasil'       # inserir ou atualizar
pessoa.keys()                   # chaves
pessoa.values()                 # valores
pessoa.get('profissao', '—')    # complemento: valor padrão se a chave não existir

pessoa[0]                       # KeyError: não se acessa por posição, só por chave
  • Acessa-se pela chave, e a chave precisa ser imutável/hashable (str, int, tupla...). {[1, 2]: 'x'} dá TypeError: unhashable type: 'list'.
  • dict(um=1, dois=2) também cria dicionários.

Funções

def velocidade(espaco, tempo):
    return espaco / tempo          # sem return, a função devolve None

def dados(nome, idade=None):       # parâmetro opcional com valor padrão
    if idade is not None:
        return f'nome: {nome}, idade: {idade}'
    return f'nome: {nome}, idade: não informada'

dados('joão')                      # só o nome
dados(idade=20, nome='ana')        # argumentos nomeados (qualquer ordem)
  • Parâmetros obrigatórios vêm antes dos opcionais; chamar sem um obrigatório → TypeError.
  • Retornar vários valores devolve uma tupla (e dá para desempacotar):
def calculadora(x, y):
    return x + y, x - y

soma, diferenca = calculadora(1, 2)     # (3, -1)

Cuidado: valor padrão mutável

Complemento: nunca use lista/dicionário como valor padrão (def f(itens=[])): o objeto é criado uma única vez e compartilhado entre as chamadas. Use None e crie dentro da função.

*args e **kwargs

Permitem receber um número variável de argumentos. Os nomes são convenção — o que vale são os asteriscos.

def teste(arg, *args):             # *args: argumentos posicionais extras, empacotados em tupla
    for outro in args:
        print(outro)

def minha_funcao(**kwargs):        # **kwargs: argumentos nomeados extras, empacotados em dict
    for chave, valor in kwargs.items():
        print(f'{chave} = {valor}')

lista = ['é', 'muito', 'legal']
teste('python', *lista)            # * desempacota uma sequência na chamada
minha_funcao(**{'nome': 'joao', 'idade': 25})   # ** desempacota um dicionário

Módulos e import

Um módulo é simplesmente um arquivo .py; um pacote é uma pasta de módulos.

import random
from conta import Conta            # importa só uma classe do módulo conta.py
random.randrange(0, 4)             # inteiro de 0 a 3 (o fim não é incluído)

if __name__ == '__main__':         # só executa quando o arquivo é rodado diretamente
    jogar()                        # (e não quando é importado por outro módulo)

Variáveis criadas dentro de uma função têm escopo local: só existem ali. Para a função usar algo de fora, passe como parâmetro e devolva o resultado com return. Refatorar é dividir o código em funções pequenas e de responsabilidade única (Refatoração).

Arquivos

with open('palavras.txt', 'r', encoding='utf-8') as arquivo:    # r = leitura (padrão)
    palavras = [linha.strip() for linha in arquivo]             # um arquivo é uma sequência de linhas

with open('palavras.txt', 'a', encoding='utf-8') as arquivo:    # a = acrescentar ao final
    arquivo.write('morango\n')                                  # \n é a quebra de linha
Modo Efeito
'r' Leitura; FileNotFoundError se o arquivo não existir
'w' Escrita; apaga o conteúdo anterior (cria se não existir)
'a' Acrescenta no fim (append)
'b' (combinado: 'rb') Modo binário (imagens etc.)
  • read() lê tudo (e o "cursor" fica no fim: ler de novo devolve ''); iterar com for lê linha a linha; readline() lê uma. print e write não se comportam igual: print acrescenta \n, write não.
  • Sempre feche o arquivo. A forma segura é o with: ele fecha o arquivo mesmo se ocorrer um erro dentro do bloco (a apostila trata isso como "para saber mais"; na prática, é o padrão).
  • O módulo csv lê/escreve arquivos de valores separados por vírgula:
import csv

with open('funcionarios.txt', newline='', encoding='utf-8') as arquivo:
    for nome, cpf, salario in csv.reader(arquivo):
        print(nome, float(salario))

Orientação a objetos em Python

Classes, objetos e __init__

Uma classe é a "receita"; um objeto (instância) é o que se constrói a partir dela. O procedural com dicionários (conta['saldo']) deixa os dados soltos e sem proteção; a classe junta dados e comportamento.

class Conta:
    def __init__(self, numero, titular, saldo, limite=1000.0):
        self.numero = numero            # atributos de instância
        self.titular = titular
        self.saldo = saldo
        self.limite = limite

    def deposita(self, valor):
        self.saldo += valor

    def saca(self, valor):
        if valor > self.saldo + self.limite:
            return False
        self.saldo -= valor
        return True

    def transfere_para(self, destino, valor):
        if self.saca(valor):
            destino.deposita(valor)
            return True
        return False

conta = Conta('123-4', 'João', 120.0)
conta.deposita(20.0)                    # o Python passa a conta como "self" automaticamente
  • self é a referência à própria instância: todo método de instância o recebe como primeiro parâmetro (por convenção chamado self).
  • __init__ não cria o objeto — inicializa. Quem cria é o __new__, chamado antes; o __init__ recebe a instância pronta. Chama-se "construtor" por abuso de linguagem.
  • Sem __init__ compatível, Conta() dá TypeError: missing 4 required positional arguments.
  • Python é dinâmico: nada impede conta.nome = 'x' em tempo de execução (veja __slots__).
  • Tudo é objeto em Python: números, strings, funções, classes, módulos (type(2) → int).

Objetos são acessados por referência

Uma variável guarda uma referência, não o objeto. c2 = c1 não copia — as duas variáveis apontam para o mesmo objeto. A passagem de argumentos funciona como uma atribuição: passar uma Conta a uma função não a clona.

c1 = Conta('123-4', 'João', 120.0)
c2 = c1
c2.deposita(30.0)
c1.saldo            # 150.0 — é o mesmo objeto
id(c1) == id(c2)    # True

Por padrão, == entre objetos compara a identidade: duas contas com os mesmos dados são diferentes. Para comparar por conteúdo, implemente __eq__ (veja métodos mágicos).

Composição

Classes colaboram: um objeto guarda referências para outros.

class Cliente:
    def __init__(self, nome, sobrenome, cpf):
        self.nome, self.sobrenome, self.cpf = nome, sobrenome, cpf

class Historico:
    def __init__(self):
        self.data_abertura = datetime.datetime.today()
        self.transacoes = []

class Conta:
    def __init__(self, numero, cliente, saldo, limite=1000.0):
        self.cliente = cliente             # agregação: o Cliente existe sem a Conta
        self.historico = Historico()       # composição: o Histórico nasce e morre com a Conta
  • Agregação: o objeto contido existe independentemente (um Cliente).
  • Composição: o objeto contido depende do dono (o Historico da Conta).
  • Navega-se pelo ponto: conta.cliente.nome.

Encapsulamento: convenções, não cadeados

Python não tem private de verdade.

Convenção Efeito
atributo Público
_atributo "Protegido": por convenção (PEP 8), não deve ser acessado de fora; o interpretador não impede
__atributo Name mangling: o nome vira _Classe__atributo. Evita colisões em subclasses, mas não torna o atributo realmente privado (p._Pessoa__idade ainda funciona)

Atribuir pessoa.__idade = 25 de fora apenas cria um atributo novo — outra fonte de confusão. A maioria dos programadores Python prefere o _ simples.

Properties em vez de getters e setters

Em Java usa-se getSaldo()/setSaldo(); em Python o estilo "pitônico" é atributo público simples e, se um dia for preciso validar, transformá-lo em property sem mudar quem usa a classe:

class Conta:
    def __init__(self, saldo=0.0):
        self._saldo = saldo

    @property
    def saldo(self):                 # leitura: conta.saldo
        return self._saldo

    @saldo.setter
    def saldo(self, valor):          # escrita: conta.saldo = x  (com validação)
        if valor < 0:
            raise ValueError('saldo não pode ser negativo')
        self._saldo = valor

Definição: Decorator

Um decorator (@algo) é uma função que recebe outra função e devolve uma versão alterada dela. @property é um decorator: transforma o método em um atributo calculado. (Não confundir com o padrão de projeto Decorator, mas a ideia de "acrescentar comportamento sem editar o original" é a mesma.)

Não crie property/setter para todo atributo "por hábito": o saldo, por exemplo, só deveria mudar por saca() e deposita(); um setter público pode ser desnecessário.

Atributos de classe, @classmethod e @staticmethod

class Conta:
    _total_contas = 0                      # atributo de CLASSE: compartilhado por todas as instâncias

    def __init__(self, saldo):
        self._saldo = saldo
        Conta._total_contas += 1           # acesse pela classe (self.x += 1 criaria um atributo de instância!)

    @classmethod
    def total_contas(cls):                 # recebe a CLASSE (cls); respeita herança
        return cls._total_contas

    @staticmethod
    def codigo_banco():                    # não recebe nada: é só uma função dentro da classe
        return '001'
Tipo de método 1º parâmetro Quando usar
De instância self (a instância) Opera nos dados de um objeto
@classmethod cls (a classe) Opera na classe/atributos de classe; fábricas alternativas; pode ser sobrescrito nas filhas
@staticmethod nenhum Função utilitária sem relação com a instância ou a classe (muitas vezes cabe melhor em uma função de módulo)

__slots__

Por padrão, cada instância guarda seus atributos em um dicionário (__dict__), o que gasta memória. __slots__ fixa a lista de atributos e elimina o __dict__:

class Conta:
    __slots__ = ['_numero', '_titular', '_saldo', '_limite']

Efeito colateral: não dá mais para criar atributos novos (AttributeError). O objetivo principal é economizar memória quando há milhões de instâncias (a apostila cita 40–50%), e não "proteger" a classe.

dir, vars e __dict__

dir(obj) lista os membros (incluindo os métodos mágicos herdados de object); vars(obj) ou obj.__dict__ devolve um dicionário com os atributos da instância.

Complemento: dataclass

Para classes que só carregam dados, @dataclass gera __init__, __repr__ e __eq__:

from dataclasses import dataclass

@dataclass
class Cliente:
    nome: str
    sobrenome: str
    cpf: str

Cliente('João', 'Oliveira', '111') == Cliente('João', 'Oliveira', '111')   # True

Herança, polimorfismo e interfaces

Herança e super()

A subclasse recebe tudo da superclasse; para reaproveitar a inicialização, chame super():

class Funcionario:
    def __init__(self, nome, cpf, salario):
        self._nome, self._cpf, self._salario = nome, cpf, salario

    def get_bonificacao(self):
        return self._salario * 0.10

class Gerente(Funcionario):
    def __init__(self, nome, cpf, salario, senha, qtd_gerenciados):
        super().__init__(nome, cpf, salario)       # reutiliza o __init__ da mãe
        self._senha = senha
        self._qtd_gerenciados = qtd_gerenciados

    def get_bonificacao(self):                      # reescrita (override)
        return super().get_bonificacao() + 1000     # chama a versão da mãe e acrescenta
  • Toda classe herda de object (a mãe de todas), de onde vêm os métodos mágicos.
  • Reescrever um método substitui o da mãe; super().metodo() permite reaproveitá-lo (e evita duplicar regras que podem mudar).
  • Pergunta de modelagem: "o Diretor é um Gerente?" — só herde se a relação é um fizer sentido. Herança aumenta o acoplamento entre mãe e filha (Herança x composição).
  • Polimorfismo: código que usa Funcionario funciona com qualquer subclasse, e o método executado é sempre o do objeto real (ControleDeBonificacoes.registra(gerente) chama Gerente.get_bonificacao). Assim, criar Secretaria não exige mudar quem usa Funcionario.

Métodos mágicos (dunder methods)

Métodos com dois sublinhados dos dois lados (__nome__) são chamados pelo interpretador e permitem que seus objetos se comportem como tipos nativos:

Método Quando é chamado
__init__ / __new__ Inicialização / criação do objeto
__str__ print(obj) e str(obj): texto amigável para o usuário
__repr__ repr(obj): representação técnica, idealmente reconstruível (Ponto(1, 2)); usada no console e em logs
__eq__, __lt__... ==, <...
__add__, __mul__, __truediv__, __mod__, __pow__ +, *, /, %, **
__len__, __getitem__, __iter__, __contains__ len(), obj[i], for, in
class Ponto:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __str__(self):
        return f'({self.x}, {self.y})'

    def __repr__(self):
        return f'Ponto({self.x}, {self.y})'

    def __eq__(self, outro):
        return isinstance(outro, Ponto) and (self.x, self.y) == (outro.x, outro.y)

print(Ponto(1, 2))           # (1, 2)
repr(Ponto(1, 2))            # 'Ponto(1, 2)'

Correção da apostila

O exemplo de __repr__ da fonte soma + 1 às coordenadas, o que contradiz o propósito do método (reconstruir o mesmo objeto via eval(repr(obj))). O correto é o acima. Além disso, a fonte cita __div__, que é do Python 2: no Python 3 é __truediv__.

Duck typing

"Se anda como um pato e grasna como um pato, então é um pato."

Em Python, o que importa é se o objeto responde às mensagens (tem os métodos) que o código usa, não de qual classe ele é. Qualquer objeto com get_bonificacao() funciona em registra(), mesmo sem herdar de Funcionario.

class Pato:
    def grasna(self): return 'quack!'

class Ganso:
    def grasna(self): return 'quack!'

for ave in (Pato(), Ganso()):
    print(ave.grasna())
  • O estilo pitônico é EAFP (Easier to Ask Forgiveness than Permission): assuma que o atributo existe e trate a exceção (try/except AttributeError), em vez de testar com hasattr()/isinstance() antes (LBYL, Look Before You Leap).
  • hasattr(obj, 'metodo') verifica se o atributo existe (nas instâncias e na classe — a apostila simplifica ao dizer "no __dict__").
  • A tipagem é dinâmica, mas forte: '2' + 2 dá TypeError (não converte sozinho).

Classes abstratas (abc)

import abc

class Funcionario(abc.ABC):
    @abc.abstractmethod
    def get_bonificacao(self):
        """Cada tipo de funcionário define a sua bonificação."""

class Gerente(Funcionario):
    def get_bonificacao(self):
        return self._salario * 0.15

Funcionario()         # TypeError: Can't instantiate abstract class ... with abstract methods get_bonificacao

Uma classe abstrata não pode ser instanciada e serve de molde: toda subclasse concreta é obrigada a implementar os métodos @abstractmethod, senão também não é instanciável. Usa-se quando a classe-mãe só existe para dar polimorfismo (Pessoa → PessoaFisica/PessoaJuridica).

Herança múltipla, MRO e mix-ins

Python permite herdar de várias classes: class Gerente(Funcionario, Autenticavel). O risco é o problema do diamante: se B e C herdam de A e ambas sobrescrevem m2(), qual versão D(B, C) usa? O Python resolve com a MRO (Method Resolution Order): uma ordem linear, da esquerda para a direita, que se consulta com D.mro():

class A:
    def m1(self): print('A')

class B(A):
    def m2(self): print('B')

class C(A):
    def m2(self): print('C')

class D(B, C): pass

D.mro()     # [D, B, C, A, object]  -> D().m2() usa o m2 de B

super() segue a MRO, então chama cada superclasse uma única vez, mesmo em hierarquias complexas.

Definição: Mix-in

Classe não feita para ser usada sozinha, que só acrescenta uma funcionalidade opcional (ex.: AutenticavelMixIn, HoraExtraMixIn) a outras por herança múltipla. Por convenção, leva MixIn no nome e geralmente não tem __init__. É prático em frameworks pequenos, mas pode causar colisões de nomes e hierarquias confusas em sistemas grandes (o Tkinter é o exemplo clássico).

Interfaces: protocolos e subclasses virtuais

Python não tem a palavra interface: toda classe tem uma interface (seus membros públicos). Um protocolo é um conjunto de métodos que formam um "contrato" informal (como __iter__ + __next__ para iteráveis), definido por documentação e convenção.

Para formalizar o contrato sem herança múltipla, use uma ABC e a subclasse virtual:

class Autenticavel(abc.ABC):
    @abc.abstractmethod
    def autentica(self, senha): ...

Autenticavel.register(Gerente)          # Gerente "promete" cumprir o contrato, sem herdar
isinstance(Gerente(...), Autenticavel)   # True

O Python não verifica se Gerente realmente implementa autentica ao registrar; se não implementar, o erro (AttributeError) só aparece na chamada. Complemento: hoje o jeito preferido de declarar "estruturas que se comportam assim" é typing.Protocol (tipagem estrutural, verificada por ferramentas como o mypy) — a versão estática do duck typing.

Exceções e erros

Uma exceção representa algo anormal durante a execução. Se ninguém trata, ela sobe pela pilha de chamadas (de metodo2 para metodo1 e para o programa principal) e encerra o programa, imprimindo o traceback. Se alguém trata (except), a execução volta ao normal a partir dali.

try:
    resultado = 10 / divisor
except ZeroDivisionError:
    print('divisão por zero')
except (TypeError, ValueError) as erro:     # vários tipos em uma tupla; "as" dá acesso ao objeto
    print(f'entrada inválida: {erro}')
else:
    print(f'resultado: {resultado}')         # só roda se NENHUMA exceção ocorreu
finally:
    print('sempre executa')                  # limpeza: roda com ou sem erro
  • raise lança uma exceção: raise ValueError('Valor negativo'). raise sozinho, dentro de um except, relança a exceção atual.
  • Prefira exceções a códigos de retorno (return False, "números mágicos"): quem chama é obrigado a notar o problema, e o programa não segue em estado inconsistente.
  • Exceções próprias derivam de Exception (nunca de BaseException) e costumam terminar em Error; crie uma base por módulo e especializações:
class ContaError(Exception):
    """Base das exceções do módulo de contas."""

class SaldoInsuficienteError(ContaError):
    pass

def saca(self, valor):
    if valor < 0:
        raise ValueError('Valor de saque negativo.')
    if valor > self._saldo:
        raise SaldoInsuficienteError('Saldo insuficiente.')
    self._saldo -= valor
  • Evite except: "pelado" (pega tudo e esconde bugs) e capture o tipo mais específico.
  • Complemento: use raise NovoErro(...) from erro_original para encadear causas.

Tipos de erro

Tipo Quando ocorre
SyntaxError / IndentationError Estrutura inválida; detectado antes de executar (na compilação para bytecode)
NameError Variável que não existe
TypeError Operação com tipo inadequado (lista['a'], '2' + 2)
ValueError Tipo certo, valor inválido (int('abc'))
KeyError / IndexError Chave de dicionário / índice de sequência inexistente (ambos são LookupError)
AttributeError Atributo ou método inexistente no objeto
ZeroDivisionError Divisão por zero (ArithmeticError)
FileNotFoundError Arquivo inexistente (OSError)

Erro semântico é quando o programa roda, mas faz a coisa errada (erro de regra de negócio): não há exceção, então ajuda usar print, testes e o depurador pdb (import pdb; pdb.set_trace() ou, hoje, breakpoint()).

Hierarquia resumida: BaseException → SystemExit, KeyboardInterrupt e Exception; e Exception → ArithmeticError, LookupError, OSError, TypeError, ValueError, RuntimeError... É por isso que except LookupError pega tanto KeyError quanto IndexError. Compare com o modelo de exceções do Java.

O módulo collections

Tipo Para quê
defaultdict(list) Dicionário com valor padrão: d[chave].append(v) sem checar se a chave existe (sem KeyError)
Counter Conta ocorrências: Counter(['a','b','a']) → {'a': 2, 'b': 1}; .most_common(n)
deque Fila de duas pontas com append/appendleft/pop/popleft em O(1) (Estrutura de Dados)
namedtuple Tupla com campos nomeados e imutável: Conta = namedtuple('Conta', 'numero titular saldo')
UserDict, UserList, UserString Bases para criar variações de dict/list/str sem os problemas de herdar direto do tipo nativo
from collections import defaultdict, Counter, deque, namedtuple

cores = [('1', 'azul'), ('2', 'amarelo'), ('1', 'branco')]
agrupadas = defaultdict(list)
for chave, valor in cores:
    agrupadas[chave].append(valor)        # {'1': ['azul', 'branco'], '2': ['amarelo']}

fila = deque(['a', 'b'])
fila.appendleft('z')                      # deque(['z', 'a', 'b'])

Por que não herdar direto de dict/list

Em CPython, os métodos dos tipos nativos muitas vezes ignoram versões sobrescritas por subclasses (ex.: dict.update não chama o seu __setitem__). Para variar o comportamento, herde de UserDict/UserList (que guardam um dict/lista interno em .data) ou das ABCs.

Containers próprios com collections.abc

O módulo collections.abc oferece ABCs para as interfaces das coleções (Container, Sized, Iterable, Sequence, MutableSequence, Mapping...). Ao herdar de uma delas, você é obrigado a implementar os métodos abstratos e ganha vários de graça. Para uma lista que só aceita um tipo de objeto, herde de MutableSequence e implemente __len__, __getitem__, __setitem__, __delitem__ e insert (o append, a iteração e o in vêm prontos):

from collections.abc import MutableSequence

class Funcionarios(MutableSequence):
    def __init__(self):
        self._dados = []                  # atributo de INSTÂNCIA (um por objeto)

    def __len__(self): return len(self._dados)
    def __getitem__(self, pos): return self._dados[pos]
    def __delitem__(self, pos): del self._dados[pos]

    def __setitem__(self, pos, valor):
        self._validar(valor)
        self._dados[pos] = valor

    def insert(self, pos, valor):
        self._validar(valor)
        self._dados.insert(pos, valor)

    @staticmethod
    def _validar(valor):
        if not isinstance(valor, Funcionario):
            raise TypeError('Só são aceitos objetos Funcionario')

Correção da apostila

A fonte declara _dados = [] no corpo da classe, o que cria uma única lista compartilhada por todas as instâncias (e usa self._dados.__contains__(self, item), que está incorreto). O certo é criar a lista no __init__, como acima.

Na prática, o dia a dia raramente exige criar ABCs próprias: as ABCs são mais usadas em frameworks e bibliotecas, e para isinstance(x, Iterable)/Mapping.

Boas práticas e idiomas de Python

  • PEP 8: 4 espaços, snake_case para funções/variáveis, PascalCase para classes, MAIÚSCULAS para constantes. Ferramentas: black, ruff/flake8.
  • Prefira compreensões e iteradores a laços com contador manual; use enumerate e zip.
  • Use with para qualquer recurso que precise ser fechado (arquivos, conexões, locks).
  • Atributos públicos + @property quando precisar; evite getters/setters estilo Java.
  • Duck typing + EAFP, com ABCs/Protocol quando for preciso formalizar contratos.
  • Não use valores padrão mutáveis; use None.
  • Trate exceções específicas e lance as suas, derivadas de Exception.
  • Ambientes virtuais e arquivo de dependências (requirements.txt) em cada projeto.
  • Testes com pytest (ou unittest), na linha de Qualidade.

Perguntas comuns de entrevista (resumo)

Pergunta Resposta curta
Python é compilada ou interpretada? O CPython compila para bytecode (.pyc) e a PVM o interpreta; o passo de compilação é automático
Qual a diferença entre lista e tupla? Lista é mutável; tupla é imutável (e pode ser chave de dicionário)
== x is? == compara valores; is compara se é o mesmo objeto (identidade)
O que é self? A referência à instância; primeiro parâmetro de todo método de instância
__init__ é o construtor? Na verdade inicializa; quem cria o objeto é __new__
@classmethod x @staticmethod? O primeiro recebe a classe (cls) e respeita herança; o segundo não recebe nada
O que é *args/**kwargs? Empacotam argumentos posicionais (tupla) e nomeados (dict) extras
Existe private em Python? Não; só convenções (_x) e name mangling (__x)
O que é a MRO? A ordem em que o Python procura métodos em hierarquias com herança múltipla (Classe.mro())
O que é duck typing? Importa o que o objeto sabe fazer, não a que classe pertence
Para que serve o with? Garante a liberação do recurso (ex.: fechar o arquivo) mesmo com erros
Mutable default argument? Valor padrão mutável é criado uma vez e compartilhado; use None
O que é um decorator? Função que recebe outra função e devolve uma versão modificada (@property, @classmethod)

Veja também: Java e Go (para comparar tipagem, concorrência e modelos de objetos), Orientação a Objetos (teoria), Estrutura de Dados e Machine Learning (Python aplicado a dados com scikit-learn e Pandas).