Herança no Python: super(), sobrescrita, herança múltipla e MRO

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Resposta rápida

Herança é o recurso da POO que cria uma classe filha a partir de uma classe mãe, reaproveitando atributos e métodos. Em Python a sintaxe é class Filha(Mae):. A filha pode adicionar métodos, sobrescrever os herdados e chamar a versão da mãe com super().

1
2
3
4
5
6
7
8
9
10
11
12
class Animal:
    def __init__(self, nome):
        self.nome = nome

    def falar(self):
        return f"{self.nome} faz um som"

class Cachorro(Animal):  # Cachorro herda de Animal
    def falar(self):      # sobrescreve o método da mãe
        return f"{self.nome} late"

print(Cachorro("Rex").falar())  # Rex late

Resumo em 30 segundos:

  • class Filha(Mae): faz a filha herdar todos os atributos e métodos da mãe.
  • Sobrescrever é definir na filha um método com o mesmo nome do método da mãe.
  • super().__init__(...) executa o construtor da mãe; sem ele, os atributos da mãe não existem na filha.
  • Python aceita herança múltipla, e a ordem de busca dos métodos é o MRO (Classe.__mro__).
  • Relação “é um” pede herança; relação “tem um” pede composição.

Salve salve Pythonista!

Este post faz parte da nossa série sobre Programação Orientada a Objetos. Se você ainda não conhece o paradigma, comece pela introdução à Programação Orientada a Objetos no Python e pelo post de classes e objetos no Python, porque aqui vamos partir do princípio de que você já sabe criar uma classe com __init__ e self.

Vamos ver como criar classes filhas, sobrescrever métodos, usar o super(), lidar com herança múltipla e com o MRO, verificar tipos com isinstance() e issubclass() e, principalmente, quando não usar herança. No final tem erros comuns com traceback e exercícios resolvidos. Todos os exemplos foram executados no Python 3.10.

Então… Bora pro post! :rocket:

Vá Direto ao Assunto…

O que é herança no Python

O que é herança em Python? Herança é o mecanismo que permite definir uma classe nova (a classe filha) com base em uma classe que já existe (a classe mãe). A filha recebe automaticamente todos os atributos e métodos da mãe e pode acrescentar os seus ou substituir os herdados. Em Python, a mãe é indicada entre parênteses: class Cachorro(Animal):.

A herança existe para evitar repetição. Quando várias classes compartilham dados e comportamentos, você coloca a parte comum em uma classe mãe e deixa em cada filha só o que é específico dela. Se amanhã o comportamento comum mudar, você altera em um lugar só.

Você vai encontrar vários nomes para a mesma coisa:

  • Classe mãe = classe pai = classe base = superclasse.
  • Classe filha = classe derivada = subclasse.

A herança modela uma relação do tipo “é um”: um Cachorro é um Animal, um Gerente é um Funcionario. Guarde essa frase, ela vai ser o seu teste para decidir entre herança e composição mais adiante.

Veja o exemplo mais simples possível:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal:
    def __init__(self, nome):
        self.nome = nome

    def comer(self):
        print(f"{self.nome} está comendo.")


class Cachorro(Animal):
    def latir(self):
        print(f"{self.nome} diz: Au au!")


rex = Cachorro("Rex")
rex.comer()   # método herdado de Animal
rex.latir()   # método próprio de Cachorro
print(Cachorro.__bases__)
print(Animal.__bases__)
1
2
3
4
Rex está comendo.
Rex diz: Au au!
(<class '__main__.Animal'>,)
(<class 'object'>,)

Repare em três coisas:

  1. Cachorro não tem __init__ nem comer(), mas consegue usar os dois: eles vieram de Animal.
  2. O atributo __bases__ mostra de quem uma classe herda diretamente.
  3. Animal não declarou mãe nenhuma, mas herda de object. Toda classe em Python 3 herda de object, mesmo sem escrever isso. É de lá que vêm métodos como __str__ e __eq__ padrão.

A documentação oficial explica o mecanismo no tutorial de classes, seção Herança.

Como sobrescrever métodos na classe filha

Sobrescrever (em inglês, override) é definir na classe filha um método com o mesmo nome de um método da mãe. Quando você chama esse método em um objeto da filha, o Python encontra primeiro a versão da filha e para de procurar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal:
    def __init__(self, nome):
        self.nome = nome

    def comer(self):
        print(f"{self.nome} está comendo.")


class Cachorro(Animal):
    def comer(self):
        print(f"{self.nome} está comendo ração.")


class Gato(Animal):
    pass


Cachorro("Rex").comer()
Gato("Mimi").comer()
1
2
Rex está comendo ração.
Mimi está comendo.

Cachorro sobrescreveu comer(), então usa a própria versão. Gato não sobrescreveu nada (o pass deixa o corpo vazio), então usa a versão de Animal. É exatamente essa possibilidade de cada filha responder do seu jeito à mesma chamada que dá origem ao polimorfismo no Python, o próximo post da série.

Um detalhe que confunde quem vem de Java ou C#: Python não tem sobrecarga de métodos (vários métodos com o mesmo nome e parâmetros diferentes na mesma classe). Se você definir dois métodos com o mesmo nome, o segundo simplesmente substitui o primeiro:

1
2
3
4
5
6
7
8
class Calculadora:
    def somar(self, a, b):
        return a + b

    def somar(self, a, b, c):  # substitui o somar anterior
        return a + b + c

print(Calculadora().somar(1, 2, 3))  # 6

Agora Calculadora().somar(1, 2) quebraria, porque só existe a versão com três parâmetros. Para aceitar quantidades diferentes de argumentos, use valores padrão ou *args, como mostramos no post sobre funções em Python.

Como usar super() no Python

Sobrescrever substitui o método da mãe por completo. Na maior parte das vezes, porém, você quer estender o comportamento, não jogá-lo fora. É para isso que serve o super(): ele dá acesso aos métodos da classe mãe a partir da filha.

O caso mais comum é o construtor. Se a filha define o próprio __init__, o __init__ da mãe deixa de rodar sozinho. Você precisa chamá-lo com super().__init__(...):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def apresentar(self):
        return f"Sou {self.nome}, tenho {self.idade} anos"


class Funcionario(Pessoa):
    def __init__(self, nome, idade, cargo):
        super().__init__(nome, idade)  # a mãe cria nome e idade
        self.cargo = cargo             # a filha cria só o que é novo

    def apresentar(self):
        base = super().apresentar()     # reaproveita o texto da mãe
        return f"{base} e trabalho como {self.cargo}"


ana = Funcionario("Ana", 30, "desenvolvedora")
print(ana.apresentar())
print(vars(ana))
1
2
Sou Ana, tenho 30 anos e trabalho como desenvolvedora
{'nome': 'Ana', 'idade': 30, 'cargo': 'desenvolvedora'}

O que aconteceu:

  • super().__init__(nome, idade) executou o construtor de Pessoa, que criou nome e idade. Depois a filha criou cargo. O vars() confirma que o objeto tem os três.
  • Em apresentar(), super().apresentar() devolveu o texto da mãe e a filha só completou a frase. Se a regra de apresentação mudar em Pessoa, Funcionario acompanha sem você mexer nela.
  • Você não passa self para o super(): o Python já cuida disso. Passar self de novo é um dos erros mais comuns (está na seção de erros).

Em código antigo você vai ver super(Funcionario, self).__init__(nome, idade). É a sintaxe obrigatória do Python 2 e ainda funciona, mas no Python 3 a forma sem argumentos é a recomendada.

Se o atributo da mãe for privado, com dois underlines (self.__saldo), a filha não consegue acessá-lo pelo mesmo nome. Isso tem a ver com encapsulamento e é o erro 4 lá embaixo. Para controlar o acesso a atributos de forma elegante, veja o post sobre como utilizar @property no Python.

Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Herança múltipla e MRO no Python

Diferente de Java, Python permite que uma classe herde de várias classes ao mesmo tempo. Basta separar as mães por vírgula:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Voador:
    def mover(self):
        return "voando"

    def voar(self):
        return "bate as asas"


class Nadador:
    def mover(self):
        return "nadando"

    def nadar(self):
        return "bate os pés"


class Pato(Voador, Nadador):
    pass


pato = Pato()
print(pato.voar(), "e", pato.nadar())
print(pato.mover())
print(Pato.__mro__)
1
2
3
bate as asas e bate os pés
voando
(<class '__main__.Pato'>, <class '__main__.Voador'>, <class '__main__.Nadador'>, <class 'object'>)

Pato ganhou voar() de uma mãe e nadar() da outra. Mas as duas têm mover(). Qual vence? Quem responde é o MRO (Method Resolution Order, ordem de resolução de métodos): a lista de classes em que o Python procura um atributo, na ordem. Você a consulta com Classe.__mro__ (uma tupla) ou Classe.mro() (uma lista).

O MRO de Pato é Pato -> Voador -> Nadador -> object. Como Voador aparece antes de Nadador, a versão dela de mover() é a encontrada primeiro. Se você trocar para class Pato(Nadador, Voador):, o pato passa a “nadar” ao se mover. A ordem das mães na definição importa.

O problema do diamante e o super() cooperativo

O caso clássico de herança múltipla é o diamante: B e C herdam de A, e D herda de B e C. Em algumas linguagens isso faria o código de A rodar duas vezes. No Python, o super() segue o MRO, e não “a mãe direta”, então cada classe é visitada uma única vez:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A:
    def salvar(self):
        print("A.salvar")


class B(A):
    def salvar(self):
        print("B.salvar")
        super().salvar()


class C(A):
    def salvar(self):
        print("C.salvar")
        super().salvar()


class D(B, C):
    def salvar(self):
        print("D.salvar")
        super().salvar()


D().salvar()
print([classe.__name__ for classe in D.__mro__])
1
2
3
4
5
D.salvar
B.salvar
C.salvar
A.salvar
['D', 'B', 'C', 'A', 'object']

Olhe a linha 3 da saída: o super() dentro de B chamou C, e não A, mesmo B não sabendo que C existe. Isso acontece porque, para um objeto D, a “próxima classe” depois de B no MRO é C. O MRO é calculado pelo algoritmo C3, explicado no artigo oficial sobre a MRO do Python (em inglês). A regra prática: a classe vem antes das mães, e as mães mantêm a ordem em que foram declaradas.

Na prática, herança múltipla funciona bem com mixins: classes pequenas, sem __init__, que só adicionam um comportamento (por exemplo, um JsonMixin com um método to_json()). O Django usa muito esse padrão nas suas class-based views. Hierarquias profundas com várias mães cheias de estado, por outro lado, ficam difíceis de entender rápido.

isinstance e issubclass: verificando a herança

Duas funções nativas respondem perguntas sobre herança:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal:
    pass
class Cachorro(Animal):
    pass
class Gato(Animal):
    pass

rex = Cachorro()
print(isinstance(rex, Cachorro))        # True
print(isinstance(rex, Animal))          # True: todo Cachorro é um Animal
print(isinstance(rex, Gato))            # False
print(isinstance(rex, (Gato, Animal)))  # True: basta casar com um da tupla
print(issubclass(Cachorro, Animal))     # True
print(issubclass(Animal, Cachorro))     # False
print(issubclass(Cachorro, object))     # True: toda classe herda de object
print(type(rex) == Animal)              # False: type() ignora herança
print(issubclass(bool, int))            # True

Dois pontos que caem em prova e em entrevista:

  • type(obj) == Classe compara a classe exata e ignora a herança. isinstance() respeita a herança, e por isso é o jeito recomendado de checar tipos.
  • bool é filha de int na própria linguagem. Por isso True + True dá 2 e isinstance(True, int) é True.

Uma observação de estilo: se o seu código está cheio de if isinstance(...) para decidir o que fazer com cada tipo, provavelmente o certo é colocar esse comportamento em um método sobrescrito em cada classe. Esse é o assunto do post de polimorfismo.

Herdando de classes nativas: exceções personalizadas

O uso de herança que você mais vai escrever no dia a dia é, provavelmente, criar as próprias exceções. Toda exceção personalizada herda de Exception (ou de uma filha dela, como ValueError):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SaldoInsuficienteError(Exception):
    def __init__(self, saldo, valor):
        super().__init__(f"Saldo de R$ {saldo} não cobre saque de R$ {valor}")
        self.saldo = saldo
        self.valor = valor


def sacar(saldo, valor):
    if valor > saldo:
        raise SaldoInsuficienteError(saldo, valor)
    return saldo - valor


try:
    sacar(100, 250)
except SaldoInsuficienteError as erro:
    print(erro)
    print(isinstance(erro, Exception))
1
2
Saldo de R$ 100 não cobre saque de R$ 250
True

O super().__init__(mensagem) entrega a mensagem para Exception, que é quem sabe exibi-la. E como SaldoInsuficienteError é uma Exception, um except Exception genérico também a capturaria. Para entender try/except a fundo, veja o post sobre tratamento de erros e exceções no Python.

Herdar de list ou dict também é possível, mas tem pegadinhas: a filha herda todos os métodos da mãe, inclusive os que quebram as regras que você quer impor. É o próximo assunto.

Herança é só um dos pilares da POO, e usar mal essa ferramenta cria mais problema do que resolve. Pra fechar esse assunto com prática de verdade e suporte de professor, conheça a Jornada Python:

Composição vs herança: quando usar cada uma

Imagine uma pilha (o último que entra é o primeiro que sai) criada herdando de list:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Pilha(list):
    def empilhar(self, item):
        self.append(item)

    def desempilhar(self):
        return self.pop()


p = Pilha()
p.empilhar(1)
p.empilhar(2)
p.insert(0, 99)  # herdou TUDO de list, inclusive o que quebra a pilha
print(p)         # [99, 1, 2]

Funciona, mas a Pilha aceita insert(), sort(), fatiamento e tudo mais que uma lista faz, e nada disso faz sentido numa pilha. Uma pilha não é uma lista: ela usa uma lista por dentro. Esse é o caso de composição: a classe guarda outro objeto como atributo e delega o trabalho para ele (a versão com composição está no Exercício 7).

Outro exemplo clássico: um carro não é um motor, ele tem um motor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Motor:
    def __init__(self, potencia):
        self.potencia = potencia

    def ligar(self):
        return f"motor de {self.potencia} cv ligado"


class Carro:
    def __init__(self, modelo, motor):
        self.modelo = modelo
        self.motor = motor  # Carro TEM UM Motor

    def ligar(self):
        return f"{self.modelo}: {self.motor.ligar()}"


carro = Carro("Fusca", Motor(65))
print(carro.ligar())
carro.motor = Motor(90)  # troca a peça sem mexer na classe Carro
print(carro.ligar())
1
2
Fusca: motor de 65 cv ligado
Fusca: motor de 90 cv ligado

Com composição, você troca o motor em tempo de execução, testa Carro com um motor falso e muda Motor sem medo de quebrar uma hierarquia inteira. Por isso a recomendação clássica dos livros de design é “prefira composição a herança”. Não é uma proibição: é um lembrete de que herança acopla muito a filha à mãe.

Situação Use Exemplo
A filha é um tipo da mãe e pode substituí-la em qualquer lugar Herança class Gerente(Funcionario)
A classe tem um outro objeto e usa parte do que ele faz Composição Carro com atributo motor
Você quer reaproveitar só alguns métodos de uma classe nativa (list, dict) Composição Pilha com self._itens = []
Criar um tipo de erro do seu domínio Herança de Exception class SaldoInsuficienteError(Exception)
Adicionar um comportamento pequeno e reutilizável a várias classes Herança múltipla com mixin class Relatorio(JsonMixin, Base)
Obrigar as filhas a implementar certos métodos Classe abstrata class Pagamento(ABC) com @abstractmethod

A última linha é o tema do post sobre abstração e classes abstratas no Python, que fecha esta série.

Erros comuns

1. Esquecer de chamar super().__init__() na filha

1
2
3
4
5
6
7
8
9
10
11
12
class Pessoa:
    def __init__(self, nome):
        self.nome = nome


class Funcionario(Pessoa):
    def __init__(self, nome, cargo):
        self.cargo = cargo  # esqueceu o super().__init__(nome)


f = Funcionario("Ana", "dev")
print(f.nome)
1
AttributeError: 'Funcionario' object has no attribute 'nome'

Ao definir __init__ na filha, o construtor da mãe não roda mais automaticamente, e self.nome nunca foi criado. Correção: comece o __init__ da filha com super().__init__(nome).

2. Passar self para o super()

1
2
3
4
5
6
7
8
9
10
11
12
class Pessoa:
    def __init__(self, nome):
        self.nome = nome


class Funcionario(Pessoa):
    def __init__(self, nome, cargo):
        super().__init__(self, nome)  # self passado duas vezes
        self.cargo = cargo


Funcionario("Ana", "dev")
1
TypeError: Pessoa.__init__() takes 2 positional arguments but 3 were given

O super() já entrega o self sozinho. Passando de novo, Pessoa.__init__ recebe três argumentos (self, self e nome) quando espera dois. Correção: super().__init__(nome).

3. Ordem das mães que impede o MRO

1
2
3
4
5
6
7
8
class A:
    pass

class B(A):
    pass

class C(A, B):  # A antes da sua própria filha B
    pass
1
2
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

O MRO exige que uma classe apareça antes das suas mães. Aqui A foi listada antes de B, que é filha de A: não existe ordem que satisfaça as duas regras. (No Python 3.10 a mensagem sai quebrada em duas linhas; no 3.13, em uma.) Correção: class C(B, A):, ou simplesmente class C(B):, já que B já traz A.

4. Acessar na filha um atributo privado (__atributo) da mãe

1
2
3
4
5
6
7
8
9
10
11
class Conta:
    def __init__(self, saldo):
        self.__saldo = saldo  # dois underlines: name mangling


class ContaPoupanca(Conta):
    def render(self, taxa):
        self.__saldo *= 1 + taxa


ContaPoupanca(100).render(0.01)
1
AttributeError: 'ContaPoupanca' object has no attribute '_ContaPoupanca__saldo'

Nomes com dois underlines no início sofrem name mangling: dentro de Conta, self.__saldo vira self._Conta__saldo; dentro de ContaPoupanca, vira self._ContaPoupanca__saldo, que não existe. Correção: use um underline só (self._saldo), a convenção para “uso interno, mas visível para as filhas”, ou exponha o valor com @property.

5. Escrever super sem parênteses

1
2
3
4
5
6
7
8
9
10
11
12
class Animal:
    def __init__(self, nome):
        self.nome = nome


class Cachorro(Animal):
    def __init__(self, nome, raca):
        super.__init__(nome)  # faltaram os parênteses de super()
        self.raca = raca


Cachorro("Rex", "vira-lata")
1
TypeError: descriptor '__init__' requires a 'super' object but received a 'str'

Sem parênteses, super é a própria classe super, e você está chamando o __init__ dela com a string "Rex". A mensagem é confusa, mas a causa é simples. Correção: super().__init__(nome).

Exercícios resolvidos

Tente resolver cada exercício antes de abrir a solução. Todos os códigos foram executados e as saídas conferidas. Depois, continue praticando com a nossa lista de exercícios de Python.

Exercício 1. Crie uma classe Veiculo com marca, ano e um método descricao(). Depois crie Moto, filha de Veiculo, com o atributo extra cilindradas, e sobrescreva descricao() reaproveitando o texto da mãe.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Veiculo:
    def __init__(self, marca, ano):
        self.marca = marca
        self.ano = ano

    def descricao(self):
        return f"{self.marca} ({self.ano})"


class Moto(Veiculo):
    def __init__(self, marca, ano, cilindradas):
        super().__init__(marca, ano)
        self.cilindradas = cilindradas

    def descricao(self):
        return f"{super().descricao()} - {self.cilindradas} cc"


print(Moto("Honda", 2024, 160).descricao())

Saída: Honda (2024) - 160 cc

O super() aparece duas vezes: no construtor, para a mãe criar marca e ano, e em descricao(), para estender o texto em vez de reescrevê-lo.

Exercício 2. Crie Quadrado como filha de Retangulo (que recebe largura e altura e tem area()), de forma que Quadrado(5) funcione sem reescrever area().

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Retangulo:
    def __init__(self, largura, altura):
        self.largura = largura
        self.altura = altura

    def area(self):
        return self.largura * self.altura


class Quadrado(Retangulo):
    def __init__(self, lado):
        super().__init__(lado, lado)


q = Quadrado(5)
print(q.area(), isinstance(q, Retangulo))

Saída: 25 True

O Quadrado só adapta o construtor: passa o mesmo lado como largura e altura. O area() herdado já resolve o resto.

Exercício 3. (Questão de prova) Qual é a saída do código abaixo?

1
2
3
4
5
6
7
8
9
10
11
12
class A:
    def ola(self):
        return "A"

class B(A):
    def ola(self):
        return "B" + super().ola()

class C(B):
    pass

print(C().ola())

a) A
b) B
c) BA
d) Um AttributeError, porque C não define ola().

Ver solução

Saída: BA

Resposta: c. C não define ola(), então o Python segue o MRO (C -> B -> A) e usa a versão de B, que devolve "B" concatenado com o resultado de super().ola(), ou seja, "A". A alternativa d está errada porque métodos herdados podem ser chamados normalmente.

Exercício 4. (Questão de prova) Dadas as classes abaixo, qual é a ordem de D.__mro__?

1
2
3
4
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

a) D, B, A, C, object
b) D, B, C, A, object
c) D, C, B, A, object
d) D, A, B, C, object

Ver solução
1
print([k.__name__ for k in D.__mro__])

Saída: ['D', 'B', 'C', 'A', 'object']

Resposta: b. O MRO coloca cada classe antes das suas mães e respeita a ordem de declaração (B antes de C). Por isso A, mãe das duas, só entra depois de B e de C. A alternativa a seria a busca “em profundidade” ingênua, que visitaria A cedo demais.

Exercício 5. A função abaixo deveria devolver "funcionário" para um Funcionario, mas devolve "pessoa". Encontre o erro e corrija.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Pessoa:
    pass

class Funcionario(Pessoa):
    pass

def tipo(obj):
    if isinstance(obj, Pessoa):
        return "pessoa"
    elif isinstance(obj, Funcionario):
        return "funcionário"
    return "outro"

print(tipo(Funcionario()))  # pessoa
Ver solução
1
2
3
4
5
6
7
8
def tipo(obj):
    if isinstance(obj, Funcionario):  # da mais específica para a mais geral
        return "funcionário"
    elif isinstance(obj, Pessoa):
        return "pessoa"
    return "outro"

print(tipo(Funcionario()), tipo(Pessoa()))

Saída: funcionário pessoa

Todo Funcionario é uma Pessoa, então isinstance(obj, Pessoa) já é True e o elif nunca roda. Teste sempre da classe mais específica para a mais geral, como nos blocos except.

Exercício 6. Crie a exceção IdadeInvalidaError, filha de ValueError, e lance-a em uma função cadastrar(idade) quando a idade for negativa. Capture-a com except ValueError.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class IdadeInvalidaError(ValueError):
    pass


def cadastrar(idade):
    if idade < 0:
        raise IdadeInvalidaError(f"idade negativa: {idade}")
    return "ok"


try:
    cadastrar(-3)
except ValueError as erro:  # pega a filha porque ela É um ValueError
    print(type(erro).__name__, "-", erro)

Saída: IdadeInvalidaError - idade negativa: -3

Como a exceção herda de ValueError, quem já tratava ValueError continua funcionando, e quem quiser tratar só esse caso pode usar except IdadeInvalidaError.

Exercício 7. Reescreva a Pilha que herdava de list usando composição, expondo só empilhar(), desempilhar() e len().

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Pilha:
    def __init__(self):
        self._itens = []  # a Pilha TEM UMA lista

    def empilhar(self, item):
        self._itens.append(item)

    def desempilhar(self):
        return self._itens.pop()

    def __len__(self):
        return len(self._itens)


p = Pilha()
p.empilhar("a")
p.empilhar("b")
print(p.desempilhar(), len(p), hasattr(p, "insert"))

Saída: b 1 False

A lista continua fazendo o trabalho pesado, mas fica escondida. A Pilha não tem mais insert(), e o método especial __len__ faz len(p) funcionar.

Exercício 8. Crie Funcionario com pagamento() que devolve o salário, e Gerente, filho de Funcionario, que recebe também um bonus e cujo pagamento é salário + bônus. Calcule a folha de uma equipe com um de cada.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Funcionario:
    def __init__(self, nome, salario):
        self.nome = nome
        self.salario = salario

    def pagamento(self):
        return self.salario


class Gerente(Funcionario):
    def __init__(self, nome, salario, bonus):
        super().__init__(nome, salario)
        self.bonus = bonus

    def pagamento(self):
        return super().pagamento() + self.bonus


equipe = [Funcionario("Ana", 5000), Gerente("Bia", 8000, 2000)]
for pessoa in equipe:
    print(pessoa.nome, pessoa.pagamento())
print(sum(p.pagamento() for p in equipe))

Saída:

1
2
3
Ana 5000
Bia 10000
15000

O loop chama pagamento() sem saber se o objeto é Funcionario ou Gerente, e cada um responde do seu jeito. Isso já é polimorfismo, o próximo passo da série.

Conclusão

Neste post você viu que herança cria uma classe filha a partir de uma mãe com class Filha(Mae):, que sobrescrever é redefinir um método com o mesmo nome e que super() permite estender o comportamento da mãe em vez de copiá-lo. Viu também que Python aceita herança múltipla, que o MRO (Classe.__mro__) decide a ordem de busca e que isinstance() respeita a herança, enquanto type() não.

Mais importante do que a sintaxe é o critério: use herança para relações “é um” e composição para relações “tem um”.

A série de POO continua com polimorfismo no Python e abstração e classes abstratas no Python. Se quiser aprender POO com projetos guiados, do básico ao avançado, conheça o nosso curso de Python completo.

Nos vemos no próximo post!

Perguntas frequentes

O que é herança em Python?

Herança é o mecanismo da programação orientada a objetos que permite criar uma classe nova (filha) a partir de uma classe existente (mãe), reaproveitando seus atributos e métodos. Em Python basta colocar a classe mãe entre parênteses: class Cachorro(Animal):. A filha pode adicionar métodos novos e sobrescrever os herdados.

Como fazer uma classe herdar de outra em Python?

Coloque o nome da classe mãe entre parênteses na definição da filha: class Funcionario(Pessoa):. Se a filha tiver __init__ próprio, chame super().__init__(...) dentro dele para que a mãe inicialize os atributos dela. Sem parênteses, a classe herda apenas de object, a base de todas as classes.

Para que serve o super() no Python?

super() devolve um objeto que encaminha chamadas para a próxima classe na ordem de herança (MRO). O uso mais comum é super().__init__(nome) no construtor da filha, para reaproveitar a inicialização da mãe, e super().metodo() dentro de um método sobrescrito, para estender o comportamento original em vez de copiá-lo.

Python aceita herança múltipla?

Sim. Uma classe pode herdar de várias mães ao mesmo tempo: class Pato(Voador, Nadador):. Quando duas mães têm um método com o mesmo nome, o Python usa a ordem de resolução de métodos (MRO), que você consulta com Pato.__mro__. Nesse exemplo, vale a versão de Voador, por vir primeiro.

O que é MRO em Python?

MRO (Method Resolution Order) é a ordem em que o Python procura um atributo ou método nas classes de uma hierarquia. Ela começa na própria classe, segue as mães da esquerda para a direita e termina em object, calculada pelo algoritmo C3. Você pode vê-la com Classe.__mro__ ou Classe.mro(), e é essa ordem que o super() segue.

Qual a diferença entre isinstance e issubclass?

isinstance(objeto, Classe) recebe um objeto e diz se ele é instância da classe ou de alguma filha dela. issubclass(ClasseA, ClasseB) recebe duas classes e diz se a primeira herda da segunda. Exemplo: com class Cachorro(Animal), isinstance(Cachorro(), Animal) e issubclass(Cachorro, Animal) são ambos True.

Quando usar composição em vez de herança?

Use herança quando a relação for do tipo ‘é um’ (um Gerente é um Funcionario) e a filha puder substituir a mãe em qualquer lugar. Use composição quando a relação for ‘tem um’ (um Carro tem um Motor): a classe guarda outro objeto como atributo e delega para ele. Na dúvida, prefira composição, que acopla menos.

Em Python, qual a forma correta de declarar que a classe B herda da classe A?

A forma correta é class B(A):. As alternativas que costumam aparecer em provas estão erradas: class B extends A é sintaxe de Java, class B : A é de C# e C++, e class B inherits A não existe. Em Python a herança é sempre indicada pela classe mãe entre parênteses.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.