O que este curso é e como usá-lo
Este é o quarto curso de Ciência da Computação Universitária: programação orientada a objetos — como expandir um script pequeno em um sistema que outras pessoas possam alterar com segurança: objetos que detêm seu estado, interfaces que ocultam sua lógica, e testes que mantêm a integridade enquanto você refatora. você refatora.
Limites de escopo, declarados uma vez: curso de estudo, não grau; Python 3 é o veículo, e cada fragmento de programa é original, autocontido e foi executado para gerar as saídas mostradas.
- Unidades 4.1–4.2 constroem o objeto (classes, referências, barreiras); 4.3–4.4 conectam-nas; 4.5–4.6 mantêm o sistema ativo.
O hábito de projeto: cada classe é um contrato com seus chamadores — cada unidade nomeia o contrato, e cada exemplo é um programa que você pode executar.
Objetos e classes
Por que importa. Programas falham quando o estado está espalhado: uma classe agrupa estado e comportamento, garantindo exatamente um local para consulta. A partir de agora, cada sistema que você construir será classes conversando.
A ideia, com uma imagem. Uma classe é um projeto; uma instância
(um objeto) é uma unidade construída a partir dele. __init__ executa na
construção e inicializa os atributos da instância; os métodos
recebem self, a própria instância. Atributos de classe residem na
classe e são compartilhados por todas as instâncias. A atribuição nunca copia: uma
variável é uma referência a um objeto, portanto dois nomes podem ser
** aliases** de um único objeto.
class Book:
next_id = 1 # class attribute, shared
def __init__(self, title, copies):
self.id = Book.next_id; Book.next_id += 1
self.title = title
self.copies = copies # instance attribute
def take(self):
self.copies -= 1; return self.copies

Ordem de busca (uma derivação). obj.x resolve percorrendo para fora:
primeiro o dicionário da instância (obj.__dict__), depois o dicionário da classe, então as
classes base. Essa única regra explica a armadilha clássica: atribuir
obj.next_id = 9 não toca a classe — ele a sombra apenas para aquela uma
instância. id acima vai para o dicionário da instância; next_id permanece no
dicionário da classe, assim os livros recebem ids 1, 2, 3, … e Book.next_id sempre
armazena o próximo número disponível.
Exemplo resolvido 1 — uma classe, rastreada. Com Book acima:
a = Book('Signals', 3)
b = Book('Signals', 3)
c = a
a.take(); a.take()
Estado após: a.copies é $1$, b.copies é $3$ (intocado), e
como c é a, c.copies também é $1$ — um objeto, dois nomes.
a == b é False (igualdade padrão é identidade) enquanto a is c é
True: igualdade compara valores (uma vez definidos como), identidade
(is) compara objetos.
Exemplo resolvido 2 — o contador compartilhado. Após a e b acima,
Book.next_id, a.next_id e b.next_id leem todos $3$ — o contador
pertence à classe, então todo chamador concorda. Escreva a.next_id = 9
e apenas a lê $9$: a classe ainda diz $3$, e o próximo livro
ainda recebe id $3$.
Erros comuns. Esquecer self no cabeçalho de um método. Mutar um
atributo de classe através de uma instância e se perguntar por que outros discordam.
Esperar que d = c faça uma cópia — ele cria um alias.
Resumo. Classe = projeto, instância = objeto; dicionário de instância sobrepuja
dicionário de classe; atribuição cria alias, nunca copia; is é identidade, ==
é igualdade (definida).
Encapsulamento
Por que importa. Toda classe possui um invariante — uma afirmação verdadeira entre chamadas (uma cópia está na estante ou emprestada a um leitor, nada mais. Encapsulamento é a disciplina de manter essa afirmação verdadeira: o estado é privado, e as alterações passam por métodos que o validam.
A ideia, com uma ilustração. Por convenção, um sublinhado inicial marca
o estado interno (_copies); os chamadores usam os métodos públicos — a
fronteira da interface. Valide na entrada: o construtor e cada
método mutável restabelece o invariante ou recusa.
class Copy:
def __init__(self, title):
if title == '': raise ValueError('empty title')
self._title = title; self._out = False
def issue(self):
if self._out: raise ValueError('already out')
self._out = True
def give_back(self):
if not self._out: raise ValueError('not out')
self._out = False
def is_out(self): # public reader, no wall breach
return self._out

A dedução (por que uma única entrada basta). Se todo caminho que toca
_out passa por issue/give_back, o invariante (_out é exatamente
True ou False) pode ser provado a partir de dois pequenos
métodos. Adicione um único gravador sem verificação e a prova se perde — o número de
locais a verificar cresce com cada chamador, assim como a taxa de erros.
Exemplo resolvido 1 — a barreira se mantém. c.issue() tem sucesso;
c.issue() novamente eleva ValueError — o chamador não consegue atingir um
estado impossível, e após a chamada falha o objeto permanece inalterado
(verifique primeiro, logo não há necessidade de reversão). c.give_back(); c._out é
False, e c.issue() funciona novamente: exatamente os estados True /
False, nada mais, para sempre.
Exemplo resolvido 2 — o custo de um vazamento. Um colega apressado escreve
c._out = 'y'. Nada os impede — sublinhado é convenção — e agora
is_out() responde 'y': truthy, mas não True. Um chamado
(if c.is_out():) armazena a cópia como ausente; outro
(if c.is_out() is True:) armazena como disponível — dois leitores
discordam sobre um objeto, estado que nenhum método poderia gerar.
Custo de reparo escala com cada leitor de _out; o custo de validação foi
dois statements de if. Essa assimetria é todo o argumento.
Erros comuns. Retornar a lista interna (return self._loans)
— chamadores mutam seu estado privado; retorne uma cópia. Validando no
construtor, mas não no setter. Escrevendo pares get/set para tudo:
expor comportamento (available()), não armazenamento.
Resumo. Sublinhado marca intenção; métodos validam na porta; um caminho verificado por mudança mantém invariantes prováveis; exponha comportamento, não campos.
Composição e herança
Por que importa. Objetos se conectam de duas formas: herança
(é-um: um Member é um Person) e composição (tem-um: um
Library tem um Catalog). Escolher errado acopla classes tão fortemente
que uma correção em classe base quebra seus filhos — o problema da classe base frágil
problema.
A ideia, com uma imagem. Cadeias de herança e inicialização:
super().__init__(...) percorre a cadeia, começando pela base. Composição
delega: o objeto inteiro possui partes e encaminha chamadas.
class Person:
def __init__(self, name):
self._name = name
class Member(Person):
def __init__(self, name, card_no):
super().__init__(name) # base first
self._card = card_no; self._loans = 0
def borrow(self):
self._loans += 1; return self._loans

A derivação (ordem de inicialização). Member('Ana', 7) executa
Member.__init__, cuja primeira ação é super().__init__('Ana') — logo
Person.__init__ conclui antes que qualquer linha de Membro seja executada. A ordem é
sempre base-primeiro descendo pela cadeia; pule a chamada super() e self._name
não existirá quando Member precisar dela, falhando com um erro claro AttributeError
em vez de corromper silenciosamente.
Exemplo resolvido 1 — delegação por composição. Um Library possui um
Catalog (uma lista de Copy):
class Library:
def __init__(self):
self._shelves = [] # composition
def add(self, copy): self._shelves.append(copy)
def available(self):
return sum(1 for c in self._shelves if not c.is_out())
Após adicionar 5 cópias e emitir 2, available() é $3$ — a biblioteca
não possui lógica de empréstimo; ela consulta sua parte. Substitua a lista por um banco de
dados similar a lista e Library permanece inalterado: as partes mudam, o todo mantém seu
interface.
Exemplo resolvido 2 — a base frágil. A base Receipt.total() soma os
itens; uma subclasse sobreescreve copiando o código de soma e subtraindo
$10$. A base então ganha uma linha de fidelidade — cada recibo de desconto
perde silenciosamente: a cópia é um fork. Correção: return super().total() - 10 — substitua delegando. Herança para is-a,
composição para tudo o mais.
Erros comuns. Herdar para reutilizar dois métodos (composição ou uma
função de módulo em vez disso). Esquecer super().__init__. Duplicar
lógica base em uma substituição — o fork que fica desatualizado.
Resumo. is-a herda (e chama super()); has-a compõe (e
delega); nunca copie lógica base em uma substituição.
Interfaces e polimorfismo
Por que importa. Sistemas grandes sobrevivem à mudança quando chamadores dependem de o que uma coisa faz, não de sua classe. Uma interface nomeia o comportamento; polimorfismo é um ponto de chamada executando muitas implementações.
A ideia, com uma imagem. Python é tipado dinamicamente: qualquer objeto
com os métodos corretos serve — tipagem pato. Para tornar o
contrato explícito, escreva um Protocol (ou abc.ABC) nomeando os métodos.
class Notifier(Protocol):
def send(self, text: str) -> str: ...
class EmailNotify:
def send(self, text): return f'email: {text}'
class SmsNotify:
def send(self, text): return f'sms: {text}'

A derivação (dispatch dinâmico). Quando n.send('due') executa, a
busca de atributo de 4.1 resolve no tipo de tempo de execução —
o próprio objeto, não a anotação da variável. Assim, um corpo de laço
dispara para uma send diferente por elemento. Isso é dinâmico
dispatch: o local de chamada é escrito uma vez, a seleção
ocorre por objeto no momento da chamada.
Exemplo resolvido 1 — um laço, três emissores. Com um terceiro emissor
QueueNotify da mesma forma e team = [EmailNotify(), SmsNotify(), QueueNotify()]:
for n in team:
print(n.send('loan due'))
Três linhas para fora — email: loan due, sms: loan due, queue: loan vencido — e adicionar um quarto emissor não toca nenhuma linha existente: o chamador
nunca foi informado das classes concretas. Este é o aberto/fechado
formato: aberto a novos tipos, fechado para edição.
Exemplo resolvido 2 — substituição e sua armadilha. Um substituto que
honra os slots do contrato invisivelmente. Um que restringe falha
silenciosamente: uma SilentNotify.send que levanta erro em string vazia quebra um
chamador cujo contrato dizia enviar nunca levanta erro. A regra de
substituição (Liskov): pré-condições só podem alargar,
garantias apenas crescer.
Erros comuns. Cadeias de isinstance — substitua por um método que cada
classe implementa. Uma sobrecarga que adiciona uma pré-condição ou remove uma
garantia: o chamador não pode saber, até quebrar.
Resumo. Nomeie o comportamento com um Protocolo; dependa do nome; o dispatch segue o tipo em tempo de execução; os substitutos devem respeitar o todo contrato.
Exceções e tratamento de erros
Por que importa. Os erros viajam. Uma função três níveis abaixo não pode corrigir uma falha, mas não deve ignorá-la. Exceções transportam um erro para a frame que pode decidir, executando a limpeza de cada frame no caminho.
A ideia, com uma imagem. raise lança; try/except captura por
tipo; finally executa em todo caminho. Uma hierarquia personalizada permite
que os chamadores capturem precisamente: base ampla, filhos específicos.
class LoanError(Exception): ...
class RenewalError(LoanError): ...
class LimitError(RenewalError): ...
class Loan:
def __init__(self, days):
self.days = days
def renew(loan, extra):
if loan.days + extra > 30:
raise LimitError(f'{loan.days}+{extra} exceeds 30')
loan.days += extra
return loan.days

A derivação (valores de status vs exceções). Um retorno de $-1$ é um
valor normal: o compilador e o runtime não anexam nada a ele, então um chamador
que esquece de verificar continua calculando com nonsensens — a falha
se espalha silenciosamente. Uma exceção, uma vez lançada, não pode ser ignorada: ela
desfaz frame por frame (executando cada finally) até que algum except
corresponda; se nenhum corresponder, o programa para com ruído. Erros ignoráveis são
ignorados; erros não ignoráveis são tratados. Escolha valores de status para
resultados esperados, próximos ao chamador (uma falha em busca), exceções
para contratos quebrados e falhas, o chamador imediato não pode corrigir.
Exemplo resolvido 1 — captura precisa. renew(Loan(20), 15) levanta
LimitError com mensagem 20+15 exceeds 30; o chamador captura
RenewalError (um pai) e o empréstimo ainda lê $20$: verifique antes de executar
significa que chamadas falhadas deixam o estado intacto. A hierarquia é um filtro — o
chamador escolhe a granularidade.
Exemplo resolvido 2 — limpeza em ambos os caminhos. Um guardiã de lock:
locked = True
loan = Loan(28)
try:
total = renew(loan, 10) # raises
except RenewalError:
kept = 'handled'
finally:
locked = False
Rastreio: renew levanta; except executa (kept = 'handled'); finally
executa; depois locked está False e o empréstimo ainda lê $28$ —
liberado no caminho de falha. No caminho de sucesso finally executa apenas
o mesmo. Limpeza que deve ocorrer vai em finally (ou um
bloco with), nunca após a última instrução.
Erros comuns. except: nu — captura KeyboardInterrupt e
oculta bugs reais. except ... pass — a falha desaparece e o estado fica
metade atualizado. Capturar Exception quando se pretendia uma criança específica.
Resumo. Levante sobre contratos quebrados, capture no quadro que possa
decidir, finally para limpeza; uma pequena hierarquia compra tratamento preciso;
chamadas falhadas devem deixar o estado inalterado.
Testes e qualidade de design
Por que importa. Refatoração — alterar estrutura sem alterar comportamento — é seguro apenas quando o comportamento é fixado. Testes unitários são essa fixação, e os bons testam comportamentos, não métodos.
A ideia, com uma imagem. Um teste = um comportamento: arranjar
os objetos, agir uma chamada, assertar o resultado observável
do comportamento. Comportamentos, não métodos: renew past the limit raises é um
comportamento; test_days é um método.
def test_renew_ok():
loan = Loan(20) # arrange
out = renew(loan, 7) # act
assert out == 27 # assert
def test_renew_boundary():
assert renew(Loan(20), 10) == 30 # exactly 30 is allowed
def test_renew_limit():
loan = Loan(20)
try:
renew(loan, 11); assert False
except LimitError:
assert loan.days == 20 # unchanged

A derivação (por que primeiro comportamento). Um teste nomeado por método falha em toda renomeação e sobrevive a toda mudança de comportamento — ruído sem segurança; um teste nomeado por comportamento faz o oposto. A cobertura segue a lista de comportamentos: cada ramo do contrato é uma linha, e a grade mostra o próximo espaço vazio.
Exemplo resolvido 1 — três comportamentos, três testes. Para renew:
dentro do limite ($20 + 7 = 27$), exatamente no limite ($20 + 10 = 30$
é permitido, porque a verificação é $> 30$), e após ele ($20 + 11$
lança exceção, estado inalterado). O teste de fronteira captura o erro clássico
de escrever >= em vez de >. Executá-los imprime 3 passed.
Exemplo resolvido 2 — refatorar sob verde. A multa do empréstimo
cálculo move-se para fora de Loan para uma classe FineCalculator (único
princípio de responsabilidade: uma das razões para alterar). O
três testes permanecem inalterados, ainda 3 passed — comportamento preservado enquanto
a estrutura se move: acoplamento diminuiu, coesão aumentou.
para cima.
Erros comuns. Assertar implementações privadas — refactoring quebra tais testes. Estado mutável compartilhado nos testes (o teste B passa apenas após o teste A ser executado). Busca por cobertura de linhas em getters enquanto a ramificação limite não tem testar.
Resumo. Arrange-act-assert por comportamento; os testes fixam o comportamento para que refactoring seja seguro; SRP reduz o custo de alteração; baixo acoplamento e alta coesão são a direção de toda mudança.