Antes de POO, certifique-se de que você lê e escreve Python básico sem travar. Tudo o que vem depois assume essa base.
def cria uma função. Parâmetros entram entre parênteses. return devolve o resultado. Sem return, a função devolve None.
def saudar(nome):
return f"Olá, {nome}!"
print(saudar("Ana")) # Olá, Ana! Se você lê fundamentos.py do slide 4 e sabe dizer linha por linha o que faz, está pronto para o resto.
É o construtor: roda automaticamente quando você chama a classe como função. Serve para receber os dados iniciais e guardar no objeto.
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
p = Pessoa("Ana", 25)
print(p.nome) # Ana
print(p.idade) # 25 p1.nome e p2.nome não se interferem. self.x = ... cria atributo no objeto atual, não na classe.
Dica de prova: se a questão pergunta o que classe e o que objeto, lembre — classe é o molde; objeto é o que sai do molde.
| Tipo | Decorador | Primeiro parâmetro | Uso típico |
|---|---|---|---|
| Instância | (nenhum) | self | Operar sobre o objeto atual. |
| Classe | @classmethod | cls | Factories e atributos de classe. |
| Estático | @staticmethod | (nenhum) | Função utilitária agrupada por contexto. |
Se só um for definido, defina __repr__ — porque, sem __str__, o Python cai no __repr__. Use __str__ para humanos; __repr__ para você (debug).
class Aluno:
def __init__(self, nome):
self.nome = nome
def __str__(self):
return f"Aluno: {self.nome}"
def __repr__(self):
return f"Aluno(nome={self.nome!r})"
a = Aluno("Ana")
print(a) # Aluno: Ana
print(repr(a)) # Aluno(nome='Ana') | Notação | Significado | O Python faz algo? |
|---|---|---|
| x | Atributo público. | Não. |
| _x | Convenção: "trate como interno". | Não impede acesso. |
| __x | Name mangling: vira _Classe__x. | Dificulta acesso de fora. |
@property faz um método ser lido como atributo. @x.setter faz a atribuição passar por um método (onde você valida). Quem usa não percebe a diferença — chama saldo, não saldo().
class Conta:
def __init__(self):
self._saldo = 0
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("Saldo negativo")
self._saldo = valor
c = Conta()
c.saldo = 100 # passa pelo setter, valida
print(c.saldo) # 100
c.saldo = -1 # ValueError Regra prática: comece sem @property. Quando precisar validar ou calcular na hora da leitura/escrita, refatore para property.
| Relação | Quem cria a parte? | Parte vive sem o todo? | Exemplo |
|---|---|---|---|
| Composição | O todo, geralmente no __init__. | Não. | Pedido tem Itens; Casa tem Cômodos. |
| Agregação | Vem de fora, por parâmetro. | Sim. | Turma tem Professor; Carro tem Motorista. |
Em Python, a sintaxe das duas é igual — a diferença é semântica, no comportamento esperado.
# Composição (forte)
class Pedido:
def __init__(self, cliente):
self.cliente = cliente
self.itens = [] # nasce com o pedido
# Agregação (fraca)
class Turma:
def __init__(self, nome, professor):
self.nome = nome
self.professor = professor # vem de fora Use quando a relação É UM faz sentido com naturalidade. Carro é Veículo. Gerente é Funcionário. Se a frase soa estranha (Pedido é Cliente?), provavelmente é composição, não herança.
A subclasse define um método com o mesmo nome que o do pai. Se não chamar super(), substitui totalmente. Se chamar super(), estende.
class Funcionario:
def calcular_bonus(self):
return self.salario * 0.10
class Gerente(Funcionario):
def calcular_bonus(self):
base = super().calcular_bonus() # estende
return base + 500 class C(A, B): C herda de A e B. Quando há ambiguidade, o Python segue a ordem do MRO (Method Resolution Order), que você inspeciona com C.mro(). Em geral: filha primeiro, depois as bases na ordem declarada, terminando em object.
Pegadinha clássica: subclasse com __init__ próprio que não chama super().__init__() perde a inicialização do pai.
Polimorfismo é o código cliente chamar obj.metodo() sem saber a classe concreta de obj. Cada objeto responde do seu jeito. Isso elimina if/elif por tipo.
| Caminho | Como funciona | Quando usar |
|---|---|---|
| Herança | Subclasses sobrescrevem método da base. | Quando o domínio tem família clara. |
| Duck typing | Basta o método existir com o nome certo. Sem parentesco. | Quando objetos vêm de origens diferentes. |
# Os dois funcionam aqui:
def fazer_falar(animais):
for a in animais:
print(a.falar())
fazer_falar([Cachorro(), Gato(), Pato()])
# Funciona se Cachorro, Gato e Pato tiverem .falar() — herdando ou não. Use isinstance() para validação na fronteira ("isso é um Pagamento?"). Não use para escolher comportamento: aí volta o if/elif que o polimorfismo eliminou.
Cheiro ruim: if isinstance(x, Cachorro): x.latir() elif isinstance(x, Gato): x.miar(). Refatore para x.falar() polimórfico.
Sem ABC, dá para criar subclasse que esquece de implementar um método importante. O erro só aparece quando alguém chama o método — tarde demais. Com ABC, o erro aparece na hora de instanciar.
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self):
...
def descrever(self): # método concreto, herdado direto
return f"Área = {self.area()}"
class Quadrado(Forma):
def __init__(self, lado):
self.lado = lado
def area(self): # obrigatório
return self.lado ** 2 Frase para gravar: ABC transforma intenção em contrato. O Python passa a cobrar o que antes era só convenção.
Em Java/C++, dá para ter duas funções com mesmo nome e assinaturas diferentes. Em Python, a segunda definição apaga a primeira. Por isso a gente simula com defaults, *args e **kwargs — uma assinatura flexível.
| Ferramenta | Para que serve | Exemplo |
|---|---|---|
| Default | Argumento opcional. | def f(a, b=0): |
| *args | Quantidade variável de posicionais. | def f(*nums): |
| **kwargs | Argumentos nomeados variáveis. | def f(**opcoes): |
É diferente: você ensina o Python a usar +, ==, <, etc. com a sua classe, implementando dunders.
class Vetor:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, outro):
return Vetor(self.x + outro.x, self.y + outro.y)
def __eq__(self, outro):
return self.x == outro.x and self.y == outro.y
def __repr__(self):
return f"Vetor({self.x}, {self.y})" Quando uma função detecta um problema e devolve None (ou um valor mágico como -1 ou False), quem chamou precisa lembrar de checar. Esquecer é fácil — e o erro estoura longe da causa, geralmente como AttributeError três telas depois.
Devolver None é como entregar um troco errado e torcer pra ninguém conferir.
| Palavra | Quando roda | Para quê |
|---|---|---|
| try | Sempre tenta primeiro | Marca o bloco arriscado. |
| except | Se houver exceção compatível | Plano B; pode mirar tipo específico. |
| else | Se o try terminou sem exceção | Caminho feliz separado. |
| finally | Sempre, com ou sem erro | Limpeza (fechar arquivo, conexão). |
Exceções são classes. BaseException é o topo; Exception é a mãe de quase todas as que você vai tratar. Capturar a classe-mãe pega também as filhas — capturar ArithmeticError pega ZeroDivisionError, por exemplo. Coloque os except mais específicos antes dos genéricos.
Quando o erro é específico do seu domínio, dê um nome próprio. O nome vira documentação viva, e a exceção pode carregar dados do erro para quem captura.
class SaldoInsuficienteError(Exception):
def __init__(self, saldo, valor):
self.saldo = saldo
self.valor = valor
self.falta = valor - saldo
super().__init__(
f"Faltam R$ {self.falta:.2f} para sacar R$ {valor:.2f}"
)
try:
raise SaldoInsuficienteError(saldo=100, valor=150)
except SaldoInsuficienteError as erro:
print(erro) # Faltam R$ 50.00 para sacar R$ 150.00
print(erro.falta) # 50 — dá pra ler os dados do erro Tratar exceção não é fazer o erro sumir — é decidir, conscientemente, o que acontece quando o esperado não acontece.
Hoje a gente fecha o ciclo: dos fundamentos de Python até tratamento de exceções. Foco no que cai na prova teórica.
O caminho de hoje
Doze aulas em doze blocos. Cada bloco com o essencial: a ideia, o código mínimo e a armadilha clássica.
Variáveis, tipos, funções, condicionais. A base que sustenta tudo.
Molde vs instância, __init__ e self.
Instância, classe, __str__ e __repr__.
_, __, @property e @setter.
Defaults, validação, objetos dentro de objetos.
Tudo aplicado em um sistema só.
É UM, super() e especialização.
Sobrescrita, mixins e MRO.
Mesmo nome, comportamentos diferentes. Duck typing.
ABC, @abstractmethod e contratos.
Simulação com *args/**kwargs e operadores via dunders.
raise, try/except/finally e exceções customizadas.
Bloco 01 · Fundamentos
Tipos básicos. type(x) revela.
Coleções nativas. Diferem em mutabilidade e ordem.
Função é bloco reutilizável que recebe entrada e devolve saída.
Decisão. Avalia em ordem, para no primeiro verdadeiro.
Bloco 01 · Fundamentos
Variável, função, condicional, laço. Se você lê isso sem travar, a base está firme.
nome = "Ana"
idade = 25
altura = 1.68
ativo = True
def classificar(idade):
if idade < 18:
return "menor"
elif idade < 60:
return "adulto"
else:
return "idoso"
print(classificar(idade)) # adulto
for i in range(3):
print(f"Olá {nome}, tentativa {i + 1}")
# Olá Ana, tentativa 1
# Olá Ana, tentativa 2
# Olá Ana, tentativa 3 Bloco 02 · Classes e Objetos
Construtor. Roda automaticamente ao criar o objeto.
Guarda o estado da instância. Cada objeto tem o seu.
Chamar a classe como função cria um objeto novo.
Bloco 02 · Classes e Objetos
Repare: o molde é único. Os objetos são independentes — cada um carrega seu próprio estado.
class Aluno:
def __init__(self, nome, matricula):
self.nome = nome
self.matricula = matricula
self.notas = []
def adicionar_nota(self, nota):
self.notas.append(nota)
def media(self):
if not self.notas:
return 0
return sum(self.notas) / len(self.notas)
a1 = Aluno("Ana", "2026001")
a2 = Aluno("Bruno", "2026002")
a1.adicionar_nota(8)
a1.adicionar_nota(7)
print(a1.media()) # 7.5
print(a2.media()) # 0 Bloco 02 · Classes e Objetos
Esses três são os que mais caem na prova. Aprenda a reconhecer cada padrão.
Bloco 03 · Métodos e Dunders
self = a instância. cls = a classe.
Decorador que transforma o método em método de classe.
Como o objeto vira string para humano e para debug.
Bloco 03 · Métodos e Dunders
__str__ é a versão humana (print). __repr__ é a versão técnica (debug, REPL, listas).
class Produto:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
def __str__(self):
return f"{self.nome} — R$ {self.preco:.2f}"
def __repr__(self):
return f"Produto(nome={self.nome!r}, preco={self.preco!r})"
p = Produto("Caneta", 3.5)
print(p) # Caneta — R$ 3.50
print(str(p)) # Caneta — R$ 3.50
print(repr(p)) # Produto(nome='Caneta', preco=3.5)
print([p, p]) # [Produto(nome='Caneta', preco=3.5), Produto(nome='Caneta', preco=3.5)] Bloco 03 · Métodos e Dunders
| item | detalhe |
|---|---|
| Método de instância | Primeiro parâmetro é self. Acessa o estado do objeto. Uso mais comum. |
| Método de classe (@classmethod) | Primeiro parâmetro é cls. Acessa atributos da classe. Útil para factories. |
| Método estático (@staticmethod) | Não recebe self nem cls. Função utilitária agrupada na classe. |
| Dunder (__nome__) | Chamado pelo Python em situações especiais (print, +, ==, len, etc.). |
Bloco 04 · Encapsulamento
Convenção: "é interno, não mexa." Python não impede.
Name mangling: vira _Classe__atributo. Dificulta acesso externo.
Lê como atributo, mas roda método por trás.
Escreve como atributo, mas roda método (com validação).
Bloco 04 · Encapsulamento
O atributo real é _saldo. O acesso público vai por saldo, que valida.
class ContaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self._saldo = 0
self.saldo = saldo_inicial # passa pelo setter
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("Saldo não pode ser negativo")
self._saldo = valor
def depositar(self, valor):
self.saldo = self._saldo + valor # reusa o setter
c = ContaBancaria("Ana", 100)
print(c.saldo) # 100
c.depositar(50)
print(c.saldo) # 150
c.saldo = -10 # ValueError: Saldo não pode ser negativo Bloco 05 · Construtor e Composição
Pedido tem ItensPedido. Sem pedido, item não existe.
Turma tem Professor. Professor existe sem a turma.
Use None quando o default mutável seria armadilha.
Bloco 05 · Construtor e Composição
Pedido é dono dos itens. A lista nasce no __init__ — cada pedido com a sua.
class Item:
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
class Pedido:
def __init__(self, cliente, itens=None):
self.cliente = cliente
self.itens = itens if itens is not None else []
def adicionar(self, item):
self.itens.append(item)
def total(self):
return sum(i.preco for i in self.itens)
p = Pedido("Ana")
p.adicionar(Item("Pizza", 45))
p.adicionar(Item("Refri", 8))
print(p.total()) # 53 Bloco 05 · Construtor e Composição
Mesma sintaxe em Python. A diferença é semântica: quem é dono de quem.
Bloco 07 · Herança — parte 1
Declaração que cria o vínculo. Filha herda tudo de Pai.
Acesso à versão original do pai, sem citar o nome dele.
Filha redefine método do pai. Mesmo nome, comportamento próprio.
Bloco 07 · Herança — parte 1
Carro é Veículo. Reaproveita marca/modelo do pai e adiciona o seu (portas).
class Veiculo:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
def descrever(self):
return f"{self.marca} {self.modelo}"
class Carro(Veiculo):
def __init__(self, marca, modelo, portas):
super().__init__(marca, modelo)
self.portas = portas
def descrever(self):
base = super().descrever()
return f"{base} ({self.portas} portas)"
c = Carro("Fiat", "Uno", 4)
print(c.descrever()) # Fiat Uno (4 portas) Bloco 08 · Herança — parte 2
Substituir totalmente. Filha define seu próprio comportamento.
Reaproveita o pai e adiciona algo a mais.
class C(A, B): junta dois pais. MRO resolve o que vier primeiro.
Bloco 08 · Herança — parte 2
Gerente reaproveita o salário do Funcionário e soma uma gratificação. MRO mostra a ordem de busca.
class Funcionario:
def __init__(self, nome, salario):
self.nome = nome
self.salario = salario
def calcular_bonus(self):
return self.salario * 0.10
class Gerente(Funcionario):
def __init__(self, nome, salario, equipe):
super().__init__(nome, salario)
self.equipe = equipe
def calcular_bonus(self):
base = super().calcular_bonus()
return base + len(self.equipe) * 100
g = Gerente("Ana", 5000, ["Bruno", "Carla"])
print(g.calcular_bonus()) # 700.0
print(Gerente.mro()) # [Gerente, Funcionario, object] Bloco 09 · Polimorfismo
Subclasses sobrescrevem método da base. Contrato via hierarquia.
Sem parentesco. Basta ter o método com o nome certo.
Usar para validação, não para escolher comportamento (volta o if/elif).
Bloco 09 · Polimorfismo
Repare na função fazer_falar: ela não sabe (e não precisa saber) qual classe é cada animal.
class Animal:
def falar(self):
return "..."
class Cachorro(Animal):
def falar(self):
return "Au au!"
class Gato(Animal):
def falar(self):
return "Miau!"
class Pato: # sem herdar de Animal
def falar(self):
return "Quack!"
def fazer_falar(animais):
for a in animais:
print(a.falar())
fazer_falar([Cachorro(), Gato(), Pato()])
# Au au!
# Miau!
# Quack! Bloco 09 · Polimorfismo
Os dois entregam polimorfismo. A escolha depende da garantia que você quer dar.
Bloco 10 · Classes Abstratas
Importa o ABC, base para classes abstratas.
Marca a classe como abstrata.
Decorador que marca o método como obrigatório nas subclasses.
Bloco 10 · Classes Abstratas
A subclasse que esquecer de implementar processar() não chega nem a ser instanciada.
from abc import ABC, abstractmethod
class Pagamento(ABC):
def __init__(self, valor):
self.valor = valor
@abstractmethod
def processar(self):
...
def recibo(self):
return f"Recibo de R$ {self.valor:.2f}"
class PagamentoPix(Pagamento):
def processar(self):
return f"Pix de R$ {self.valor:.2f} confirmado"
class PagamentoCartao(Pagamento):
pass # esqueceu de implementar processar()
p = PagamentoPix(50)
print(p.processar()) # Pix de R$ 50.00 confirmado
print(p.recibo()) # Recibo de R$ 50.00
c = PagamentoCartao(80)
# TypeError: PagamentoCartao é abstrata e não implementou processar() Bloco 10 · Classes Abstratas
| item | detalhe |
|---|---|
| Classe abstrata | Classe que herda de ABC e existe para ser herdada — nunca instanciada. |
| Método abstrato | Método decorado com @abstractmethod, sem implementação obrigatória na base. |
| Método concreto | Método com corpo na classe abstrata. Pode ser herdado direto. |
| Subclasse concreta | Herda da abstrata e implementa todos os métodos abstratos. |
| Contrato | Conjunto de métodos abstratos que toda subclasse precisa implementar. |
Bloco 11 · Sobrecarga
def f(a, b=0): atende 1 ou 2 argumentos.
Coleta posicional em tupla. Quantidade variável.
Coleta nomeada em dict. Argumentos por nome.
__add__, __eq__, __lt__, __str__, __len__...
Bloco 11 · Sobrecarga
Uma função, várias chamadas válidas. Sem precisar de várias versões.
def somar(*numeros):
total = 0
for n in numeros:
total += n
return total
print(somar(2, 3)) # 5
print(somar(1, 2, 3, 4)) # 10
print(somar()) # 0
print(somar(*[10, 20, 30])) # 60 Bloco 11 · Sobrecarga
Com __add__, __eq__ e __repr__, o Vetor passa a se comportar como um tipo nativo.
class Vetor:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, outro):
return Vetor(self.x + outro.x, self.y + outro.y)
def __eq__(self, outro):
return self.x == outro.x and self.y == outro.y
def __repr__(self):
return f"Vetor({self.x}, {self.y})"
v1 = Vetor(1, 2)
v2 = Vetor(3, 4)
print(v1 + v2) # Vetor(4, 6)
print(v1 == Vetor(1, 2)) # True
print(v1 == v2) # False Bloco 11 · Sobrecarga
| item | detalhe |
|---|---|
| a + b | __add__(self, outro) — devolve um objeto novo, não modifica o atual. |
| a - b | __sub__(self, outro) |
| a * b | __mul__(self, outro) |
| a == b | __eq__(self, outro) — se redefinir, redefina também __hash__ para usar em set/dict. |
| a < b | __lt__(self, outro) — habilita ordenação com sorted(). |
| str(a) / print(a) | __str__(self) — representação amigável. |
| repr(a) | __repr__(self) — representação técnica, vista em listas e debug. |
| len(a) | __len__(self) — habilita len() na sua classe. |
Bloco 12 · Exceções
None ou imprimir um aviso esconde o erro: ele estoura longe da causa, num AttributeError três telas depois. raise para sinalizar; quem chamou decide a reação com try/except. raise desarma na origem; o try/except é você no quadro de luz decidindo o que fazer. Dispara uma exceção de propósito e interrompe a função.
Marca o bloco arriscado que pode levantar uma exceção.
Captura a exceção levantada no try e executa o plano B.
Bloco 12 · Exceções
else roda só se deu certo. finally roda sempre — perfeito para limpeza.
def sacar(saldo, valor):
if valor > saldo:
raise ValueError("saldo insuficiente")
return saldo - valor
try:
novo = sacar(100, 150)
except ValueError as erro:
print(f"Operação negada: {erro}") # Operação negada: saldo insuficiente
else:
print(f"Novo saldo: {novo}") # só roda se NÃO deu erro
finally:
print("Encerrando operação.") # roda SEMPRE Bloco 12 · Exceções
BaseException → Exception → ValueError / TypeError / .... Capturar a classe-mãe pega as filhas. Exception. O nome (SaldoInsuficienteError) já documenta o erro do seu domínio. __init__ próprio, a exceção carrega dados do erro (saldo, valor, falta). Quem captura lê esses dados para reagir com precisão. Tipo certo, valor errado. Ex.: int("abc").
Tipo errado para a operação. Ex.: "texto" + 5.
Divisão (ou módulo) por zero.
Chave ou índice que não existe.
Bloco 12 · Exceções
As duas estratégias reagem ao mesmo problema. Só uma avisa de verdade.
Bloco 12 · Exceções
| item | detalhe |
|---|---|
| Exceção | Objeto que representa um erro. Instância de uma classe (ValueError, TypeError, sua customizada...). |
| raise | Levanta uma exceção de propósito. Interrompe a função e sobe o erro para quem chamou. |
| try / except | Marca o bloco arriscado e captura um tipo específico de exceção. |
| else (em try) | Roda apenas se o try terminou sem exceção. Mantém o try enxuto. |
| finally | Roda sempre, com erro ou sem erro. Usado para liberar recursos (limpeza). |
| Hierarquia de exceções | Exceções são classes com herança. Capturar a classe-mãe pega também as filhas. |
| Exceção customizada | Classe que herda de Exception para representar um erro específico do seu domínio. |
| Engolir erro | Anti-padrão: except: pass. O bug some da tela, mas continua vivo no programa. |
| EAFP | Easier to Ask Forgiveness than Permission. Tente direto e trate o erro se acontecer — estilo pythônico. |
Os 4 pilares — fechamento
Esconder o que é interno e expor o que faz sentido. _, __, @property.
Reuso com semântica: subclasse É UM tipo da superclasse. super().
Mesma mensagem, respostas específicas. Herança ou duck typing.
Trabalhar com o contrato (o quê), escondendo a implementação (o como).
Armadilhas
Se você reconhece o padrão, você corrige. Treine identificar antes de ler a resposta.
Acessar atributo do objeto sem self dentro do método. NameError clássico.
def f(x=[]): a lista é compartilhada entre chamadas. Use None e inicialize dentro.
_ é só convenção. __ ativa name mangling de verdade.
Subclasse com __init__ próprio que não chama o pai perde a inicialização da base.
Tentar Pagamento(50) direto levanta TypeError. Só subclasses concretas instanciam.
Volta o if/elif por tipo. Confie no polimorfismo: cada classe sabe o que faz.
__add__ deve retornar um objeto novo, não alterar self.
Vocabulário
Saber o nome certo pesa: a prova cobra termo técnico, não só código.
Molde que define atributos e comportamento de objetos.
Cada exemplar criado a partir da classe, com estado próprio.
Dado guardado dentro do objeto (ou da classe).
Função definida dentro da classe, agindo sobre o objeto.
Referência ao próprio objeto, primeiro parâmetro do método de instância.
Construtor: roda ao criar o objeto, define o estado inicial.
Decorador que faz um método ser acessado como atributo.
Relação É UM. Subclasse herda atributos e métodos da superclasse.
Acessa o comportamento da superclasse sem nomeá-la.
Sobrescrita: subclasse redefine método herdado.
Mesmo nome de método, comportamentos diferentes por tipo.
Compatibilidade por comportamento, não por hierarquia.
Herda de ABC, define contrato via @abstractmethod.
Método com nome em __dupla__: o Python chama em situações especiais.
Ordem de resolução de métodos em herança múltipla.
Objeto que representa um erro. Levantado com raise, capturado com try/except.
Dispara uma exceção de propósito e interrompe a função.
Classe que herda de Exception e representa erro do seu domínio.
Como ler código em prova
Treine este roteiro nas questões de "o que esse código imprime?"
Quais classes existem? Quem herda de quem? Anote a hierarquia na margem.
Que atributos cada objeto guarda? Anote os campos.
Mesmo nome aparecendo em pai e filha? Veja qual versão será chamada.
Crie os objetos no papel. Anote o estado depois de cada chamada.
Para cada print, anote o que sai. Para cada erro, anote o tipo (TypeError, ValueError, AttributeError).
Autoavaliação
Dicas finais
Resumo em uma linha cada