Python Herança

A Ana está montando o cadastro do petshop da vizinha. Todo animal tem nome e come; o cachorro, além disso, busca a bolinha; o gato mia em vez de latir. Escrever três classes do zero, repetindo o nome e o comer em todas, seria trabalho dobrado, e cada correção teria de ser feita três vezes. A herança resolve: você escreve uma classe geral, Animal, e cria classes filhas, Cachorro e Gato, que recebem tudo o que a mãe tem e só acrescentam ou mudam o que é diferente.

Você vai aprender
  • A criar uma classe filha com class Cachorro(Animal):, e o que ela herda.
  • A acrescentar métodos na filha e a sobrescrever (trocar) um método da mãe.
  • A usar super() para aproveitar o __init__ e os métodos da mãe.
  • A perguntar "é um?" com isinstance e "é filha de?" com issubclass.
  • Como o Python procura um método: primeiro na filha, depois na mãe (__mro__).
  • Quando usar herança ("é um") e quando guardar um objeto dentro do outro ("tem um").
  • Os erros reais: o super().__init__ esquecido, o self a mais, a mãe criada depois da filha e o método com nome errado.

A ideia: "é um"

Na ficha do petshop há uma parte que vale para todo animal (nome, idade, vacinas) e uma parte que muda: o cachorro tem raça e porte; o peixe, o tipo de aquário. A herança segue a mesma lógica da frase "é um": um cachorro é um animal, um gato é um animal. Tudo o que vale para animal vale também para cachorro.

NomeTambém chamada deNo exemplo
Classe mãeclasse base, superclasse, classe paiAnimal
Classe filhasubclasse, classe derivadaCachorro, Gato
Herdarestender, derivarCachorro herda de Animal

1. A filha recebe tudo

class Animal:
    def __init__(self, nome):
        self.nome = nome

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

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


class Cachorro(Animal):
    pass


rex = Cachorro("Rex")
rex.comer()
rex.falar()
print(type(rex))
class Animal:
A classe mãe, escrita como qualquer classe.
class Cachorro(Animal):
O nome da mãe entre parênteses depois do nome da classe: "o Cachorro herda de Animal". São os parênteses que a aula Classes e Objetos prometeu explicar.
pass
A filha não acrescenta nada (por enquanto). Mesmo assim, ela já tem tudo o que a mãe tem.
Cachorro("Rex")
O Cachorro não escreveu __init__: usa o da mãe, que pede o nome.
$ python3 animal.py
Rex está comendo.
Rex faz um som.
<class '__main__.Cachorro'>

Com duas linhas (class Cachorro(Animal): e pass), o cachorro já tem __init__, comer e falar. E o rex é um Cachorro, não um Animal "genérico": o type mostra.


2. Acrescentando: o que só a filha faz

class Animal:
    def __init__(self, nome):
        self.nome = nome

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


class Cachorro(Animal):
    def buscar(self):
        print(f"{self.nome} foi buscar a bolinha.")


rex = Cachorro("Rex")
rex.comer()
rex.buscar()

bicho = Animal("Bicho")
bicho.buscar()
$ python3 novo_metodo.py
Rex está comendo.
Rex foi buscar a bolinha.
Traceback (most recent call last):
  File "/home/ana/novo_metodo.py", line 19, in <module>
    bicho.buscar()
    ^^^^^^^^^^^^
AttributeError: 'Animal' object has no attribute 'buscar'

O rex sabe comer (herdou da mãe) e buscar (escrito na filha). Repare que o buscar usa o self.nome, criado pelo __init__ da mãe: tudo se mistura num objeto só. Mas a herança vai num sentido só: da mãe para a filha. Um Animal qualquer não ganhou o buscar, e daí o AttributeError. Nem todo animal busca bolinha.


3. Sobrescrevendo: a filha faz do seu jeito

Se a filha escreve um método com o mesmo nome de um método da mãe, o da filha vale para ela. Isso se chama sobrescrever (em inglês, override, "passar por cima").

class Animal:
    def __init__(self, nome):
        self.nome = nome

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


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


class Gato(Animal):
    def falar(self):
        print(f"{self.nome} diz: Miau!")


class Peixe(Animal):
    pass


for bicho in [Cachorro("Rex"), Gato("Mimi"), Peixe("Nemo")]:
    bicho.falar()
$ python3 sobrescrever.py
Rex diz: Au au!
Mimi diz: Miau!
Nemo faz um som.                   « o Peixe não sobrescreveu: usou o da mãe

Como o Python decide qual falar usar? Ele procura o método primeiro na classe do objeto; se não achar, sobe para a mãe. No Cachorro, achou logo. No Peixe, não achou e subiu para o Animal. É a mesma busca da aula Atributos e Métodos (primeiro o objeto, depois a classe), com mais um andar: depois da classe, a mãe.

Missão: o petshop agora atende passarinhos. Crie a classe Passaro, filha de Animal, que sobrescreve o falar para mostrar Piu diz: Piu piu! (use o self.nome).

class Animal:
    def __init__(self, nome):
        self.nome = nome

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


# crie aqui a classe Passaro


piu = Passaro("Piu")
piu.falar()
class Animal:
    def __init__(self, nome):
        self.nome = nome

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


class Passaro(Animal):
    def falar(self):
        print(f"{self.nome} diz: Piu piu!")


piu = Passaro("Piu")
piu.falar()

4. super(): aproveitando o __init__ da mãe

O cachorro precisa guardar a raça, além do nome. Então ele precisa do seu próprio __init__. Mas o __init__ da filha sobrescreve o da mãe, e aí ninguém guardaria o nome. A solução é a filha chamar o __init__ da mãe, para ela fazer a parte dela, e depois fazer a sua:

class Animal:
    def __init__(self, nome):
        self.nome = nome

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


class Cachorro(Animal):
    def __init__(self, nome, raca):
        super().__init__(nome)
        self.raca = raca

    def falar(self):
        print(f"{self.nome}, o {self.raca}, diz: Au au!")


rex = Cachorro("Rex", "vira-lata")
print(rex.nome, rex.raca)
rex.falar()
print(vars(rex))
def __init__(self, nome, raca):
O __init__ do cachorro recebe tudo o que um cachorro precisa: o nome (que todo animal tem) e a raça (só dele).
super()
"A classe de cima", a mãe. super() dá acesso aos métodos da mãe, mesmo os que a filha sobrescreveu.
super().__init__(nome)
Chama o __init__ do Animal, que guarda o self.nome. Sem self nos parênteses: o super() já sabe qual é o objeto.
self.raca = raca
Depois, a parte que só o cachorro tem.
$ python3 super_init.py
Rex vira-lata
Rex, o vira-lata, diz: Au au!
{'nome': 'Rex', 'raca': 'vira-lata'}

O vars(rex) mostra os dois atributos num objeto só: o nome, guardado pelo __init__ da mãe, e a raca, pelo da filha. O costume é chamar o super().__init__ na primeira linha do __init__ da filha: primeiro a parte geral, depois a específica.

super() em outros métodos

O super() serve para qualquer método, não só o __init__. Use quando a filha quer fazer o mesmo que a mãe e mais um pouco. Uma conta poupança mostra o saldo como qualquer conta, mais uma linha sobre o rendimento:

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def mostrar(self):
        print(f"{self.titular}: R$ {self.saldo:.2f}")


class ContaPoupanca(ContaBancaria):
    def __init__(self, titular, saldo=0, taxa=0.01):
        super().__init__(titular, saldo)
        self.taxa = taxa

    def render(self):
        self.saldo += self.saldo * self.taxa

    def mostrar(self):
        super().mostrar()
        print(f"  (poupança, rende {self.taxa:.0%} ao mês)")


corrente = ContaBancaria("Ana", 500)
poupanca = ContaPoupanca("Bia", 1000)
poupanca.render()

corrente.mostrar()
poupanca.mostrar()
$ python3 conta.py
Ana: R$ 500.00
Bia: R$ 1010.00
  (poupança, rende 1% ao mês)

O mostrar da poupança chama o super().mostrar() (a linha com o titular e o saldo, escrita uma vez só, na mãe) e acrescenta a sua linha. Se um dia o banco mudar o jeito de mostrar o saldo, a mudança na mãe vale para as duas contas. (O :.0% mostra o 0.01 como porcentagem sem casas: 1%. Veja a Formatação.)


5. "É um?": isinstance e issubclass

class Animal:
    pass


class Cachorro(Animal):
    pass


class Gato(Animal):
    pass


rex = Cachorro()
print(isinstance(rex, Cachorro))
print(isinstance(rex, Animal))
print(isinstance(rex, Gato))
print(type(rex) == Animal)
print(issubclass(Cachorro, Animal))
print(issubclass(Animal, Cachorro))
print(issubclass(Cachorro, object))
print(Cachorro.__mro__)
$ python3 isinst.py
True                               « o rex é um Cachorro
True                               « …e também é um Animal
False                              « mas não é um Gato
False                              « o type() só olha a classe exata
True                               « Cachorro é filha de Animal
False                              « Animal não é filha de Cachorro
True                               « toda classe é filha de object
(<class '__main__.Cachorro'>, <class '__main__.Animal'>, <class 'object'>)
isinstance(rex, Animal)
True: um cachorro é um animal. O isinstance aceita a classe do objeto e todas as mães dela. Era isso que a aula Classes e Objetos prometeu: por isso ele é preferido ao type() ==, que responde False aqui.
issubclass(Cachorro, Animal)
Pergunta sobre as classes, não sobre objetos: "Cachorro é filha (ou neta…) de Animal?".
Cachorro.__mro__
A ordem de busca dos métodos (MRO, de method resolution order, "ordem de resolução de métodos"): primeiro Cachorro, depois Animal, por fim object, a mãe de todas as classes.
Herança múltipla: uma palavra

Em Python, uma classe pode ter mais de uma mãe: class Pato(Voador, Nadador):. O pato recebe os métodos das duas.

class Voador:
    def voar(self):
        print("Voando!")


class Nadador:
    def nadar(self):
        print("Nadando!")


class Pato(Voador, Nadador):
    pass


donald = Pato()
donald.voar()
donald.nadar()
print(Pato.__mro__)
$ python3 multipla.py
Voando!
Nadando!
(<class '__main__.Pato'>, <class '__main__.Voador'>, <class '__main__.Nadador'>, <class 'object'>)

O __mro__ diz a ordem da busca: se as duas mães tiverem um método com o mesmo nome, vale o da que aparece primeiro. A herança múltipla tem regras próprias e é fácil de embaralhar; para quem está começando, uma mãe só basta para quase tudo.


Formas diferentes: herdar ou guardar dentro?

Herança não é o único jeito de aproveitar uma classe. Outro, muito comum, é um objeto guardar outro como atributo. Isso se chama composição, e segue a frase "tem um": a aluna tem um endereço (ela não é um endereço).

class Endereco:
    def __init__(self, rua, cidade):
        self.rua = rua
        self.cidade = cidade


class Aluno:
    def __init__(self, nome, endereco):
        self.nome = nome
        self.endereco = endereco


casa = Endereco("Rua das Flores, 10", "Teresina")
bia = Aluno("Bia", casa)
print(bia.nome, "mora em", bia.endereco.cidade)
$ python3 composicao.py
Bia mora em Teresina

O bia.endereco.cidade se lê da esquerda para a direita: o endereco da bia (um objeto Endereco), e a cidade dele.

FormaA fraseQuando usar
Herança: class Cachorro(Animal):"Um cachorro é um animal"Quando a filha é um tipo mais específico da mãe e serve em todo lugar onde a mãe serve
Composição: self.endereco = endereco"A aluna tem um endereço"Quando um objeto só usa o outro como parte de si. Na dúvida, prefira esta: é mais simples de mudar depois
Copiar e colar o código—Nunca: cada correção teria de ser feita em todas as cópias

Uma armadilha clássica: fazer class Aluno(Endereco): só para "ganhar" a rua e a cidade. Funciona, mas a frase fica errada ("um aluno é um endereço?"), e o aluno herda coisas de endereço que não fazem sentido para ele.


Quando dá errado

Esquecer o super().__init__
class Animal:
    def __init__(self, nome):
        self.nome = nome


class Cachorro(Animal):
    def __init__(self, nome, raca):
        self.raca = raca


rex = Cachorro("Rex", "vira-lata")
print(rex.raca)
print(rex.nome)
$ python3 sem_super.py
vira-lata
Traceback (most recent call last):
  File "/home/ana/sem_super.py", line 13, in <module>
    print(rex.nome)
          ^^^^^^^^
AttributeError: 'Cachorro' object has no attribute 'nome'

O cachorro nasceu sem nome. O __init__ da filha sobrescreveu o da mãe, e o Python não chama o da mãe sozinho: quem não chamou, ficou sem. O nome até chegou como parâmetro, mas ninguém o guardou. Como consertar: comece o __init__ da filha com super().__init__(nome).

Passar o self para o super(): takes 2 positional arguments but 3 were given
class Animal:
    def __init__(self, nome):
        self.nome = nome


class Cachorro(Animal):
    def __init__(self, nome, raca):
        super().__init__(self, nome)
        self.raca = raca


rex = Cachorro("Rex", "vira-lata")
$ python3 super_self.py
Traceback (most recent call last):
  File "/home/ana/super_self.py", line 12, in <module>
    rex = Cachorro("Rex", "vira-lata")
  File "/home/ana/super_self.py", line 8, in __init__
    super().__init__(self, nome)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^
TypeError: Animal.__init__() takes 2 positional arguments but 3 were given

Lendo de baixo para cima: o erro foi na linha 8, dentro do __init__ do cachorro, que tinha sido chamado pela linha 12. "O Animal.__init__() recebe 2 argumentos (self e nome), mas ganhou 3". O super() já passa o objeto como self, sozinho, como o rex.falar() passa o rex. O self escrito à mão foi o terceiro. Como consertar: super().__init__(nome).

Não passar o que a mãe pede: missing 1 required positional argument
class Animal:
    def __init__(self, nome):
        self.nome = nome


class Cachorro(Animal):
    def __init__(self, nome, raca):
        super().__init__()
        self.raca = raca


rex = Cachorro("Rex", "vira-lata")
$ python3 super_falta.py
Traceback (most recent call last):
  File "/home/ana/super_falta.py", line 12, in <module>
    rex = Cachorro("Rex", "vira-lata")
  File "/home/ana/super_falta.py", line 8, in __init__
    super().__init__()
    ~~~~~~~~~~~~~~~~^^
TypeError: Animal.__init__() missing 1 required positional argument: 'nome'

O super().__init__ é uma chamada como outra qualquer: precisa dos dados que o __init__ da mãe pede. Como consertar: repasse o nome: super().__init__(nome).

A filha antes da mãe: NameError
class Cachorro(Animal):
    pass


class Animal:
    pass
$ python3 ordem.py
Traceback (most recent call last):
  File "/home/ana/ordem.py", line 1, in <module>
    class Cachorro(Animal):
                   ^^^^^^
NameError: name 'Animal' is not defined

Na linha 1, o Python precisa da mãe para montar a filha, e o Animal só é criado na linha 5. Os ^^^^^^ apontam exatamente o nome que faltou. O mesmo erro aparece se o nome da mãe estiver digitado errado (Animall). Como consertar: escreva a classe mãe antes das filhas.

Sobrescrever com o nome errado: nenhum erro, e nada muda
class Animal:
    def __init__(self, nome):
        self.nome = nome

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


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


rex = Cachorro("Rex")
rex.falar()
$ python3 nome_errado.py
Rex faz um som.                    « era para latir!

Faltou o r: a filha escreveu fala, um método novo, e não sobrescreveu o falar. Para o Python está tudo certo; o rex.falar() achou o da mãe. Como consertar: o método da filha precisa ter exatamente o nome do método da mãe: def falar(self):.


Referência rápida

EscritaO que faz
class Cachorro(Animal):Cria a classe filha Cachorro, que herda tudo de Animal
Método novo na filhaSó os objetos da filha ganham
Método com o mesmo nome do da mãeSobrescreve: para a filha, vale o dela
super().__init__(nome)Chama o __init__ da mãe (sem self)
super().mostrar()Chama o mostrar da mãe, de dentro da filha
isinstance(rex, Animal)True se o objeto for da classe ou de uma filha dela
issubclass(Cachorro, Animal)True se a classe for filha (ou neta…) da outra
Cachorro.__mro__A ordem em que o Python procura os métodos
class Pato(Voador, Nadador):Herança múltipla: duas mães
SintomaCausa provável
AttributeError num atributo que a mãe deveria criarA filha tem __init__ e não chamou o super().__init__(…)
takes 2 positional arguments but 3 were given no super()self a mais: super().__init__(self, nome)
missing 1 required positional argument no super()Faltou repassar um dado para a mãe
NameError: name 'Animal' is not defined no classA mãe foi escrita depois da filha (ou o nome está errado)
A filha continua fazendo o que a mãe fazO método da filha tem um nome diferente do da mãe
AttributeError num objeto da mãeO método só existe na filha: a herança vai da mãe para a filha, não ao contrário

Exercícios

1. A classe filha

Complete para a classe Gato herdar de Animal:

class Gato:

2. Chamando a mãe

Complete a primeira linha do __init__ do Gato, que repassa o nome para o __init__ do Animal:

3. Peixe herda de Animal e não escreve falar. O que acontece em Peixe("Nemo").falar()?

4. Com class Cachorro(Animal) e rex = Cachorro("Rex"), quanto vale isinstance(rex, Animal)?

5. Qual destas relações pede composição (um objeto guardado dentro do outro), e não herança?

6. O __init__ da filha começa com super().__init__(self, nome) e dá TypeError. Por quê?

Missão final: a conta poupança

Missão: complete a ContaPoupanca, filha de ContaBancaria. (1) No __init__, chame o __init__ da mãe com super() (passando o titular e o saldo) e guarde a taxa. (2) Escreva o método render(self), que soma ao saldo o saldo vezes a taxa. A Bia tem 1000 reais a 1% ao mês: depois de render, o programa mostra Bia: R$ 1010.00.

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def mostrar(self):
        print(f"{self.titular}: R$ {self.saldo:.2f}")


class ContaPoupanca(ContaBancaria):
    def __init__(self, titular, saldo=0, taxa=0.01):
        self.taxa = taxa

    # escreva aqui o método render


poupanca = ContaPoupanca("Bia", 1000)
poupanca.render()
poupanca.mostrar()
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo

    def mostrar(self):
        print(f"{self.titular}: R$ {self.saldo:.2f}")


class ContaPoupanca(ContaBancaria):
    def __init__(self, titular, saldo=0, taxa=0.01):
        super().__init__(titular, saldo)
        self.taxa = taxa

    def render(self):
        self.saldo += self.saldo * self.taxa


poupanca = ContaPoupanca("Bia", 1000)
poupanca.render()
poupanca.mostrar()

Se aparecer 'ContaPoupanca' object has no attribute 'saldo', o super().__init__ ainda não está lá. Repare que você não escreveu mostrar na poupança: ela herdou o da mãe.


Resumo

  • class Filha(Mae): cria uma classe que herda tudo da mãe: __init__, métodos, atributos de classe. Use quando a frase "a filha é uma mãe" fizer sentido.
  • A filha pode acrescentar métodos (só ela ganha) e sobrescrever métodos da mãe, com o mesmo nome exato.
  • O Python procura um método primeiro na classe do objeto, depois na mãe, até object: é o __mro__.
  • Se a filha tem __init__, ela precisa chamar super().__init__(…), sem self, para a mãe fazer a parte dela.
  • super().metodo() aproveita o método da mãe para "fazer o mesmo e mais um pouco".
  • isinstance(obj, Mae) é True também para objetos das filhas; issubclass(Filha, Mae) pergunta sobre as classes.
  • "Tem um" é composição (self.endereco = …), não herança. Herança múltipla existe, mas uma mãe basta para quase tudo.