Herança#
Neste capítulo vamos falar sobre herança em POO.
Herança é um dos quatro pilares da programação orientada a objetos. Os outros três são encapsulamento, polimorfismo e abstração. Veremos os outros em capítulos seguintes.
O que é herança?#
Herança é um conceito em POO que permite a criação de uma nova classe que herda atributos e métodos de uma classe existente. A nova classe é chamada de subclasse ou classe filha, e a classe existente é chamada de superclasse ou classe pai/mãe. A subclasse usa atributos e métodos da superclasse sem precisar reescrevê-los.
É importante ressaltar que a herança estabelece uma relação na qual a subclasse é um tipo da superclasse. Por exemplo, se temos uma classe Animal
e uma classe Cachorro
, podemos dizer que Cachorro
é um tipo de Animal
. Ou então, se tivermos uma classe Veículo
e uma classe Carro
, podemos dizer que Carro
é um tipo de Veículo
.
Caso essa relação não exista, talvez a herança não seja a melhor opção. Nesse caso, podemos usar a composição, que é outro conceito da POO o qual veremos também mais adiante aqui no livro. Portanto, sempre que for pensar em herança, pergunte-se: a subclasse é um tipo da superclasse?
Vantagens da herança#
As principais vantagens de usar herança são:
Reutilização de código: a subclasse herda atributos e métodos da superclasse, o que evita a reescrita de código.
Facilidade de manutenção: se precisarmos alterar um método ou atributo, basta alterar na superclasse, e a mudança será refletida em todas as subclasses.
Facilidade de extensão: podemos adicionar novos métodos e atributos na subclasse sem alterar a superclasses.
Sintaxe de herança#
Para usar herança em Python, basta passar a superclasse entre parênteses na definição da subclasse. Veja o passo a passo de como construir uma estrutura simples com classes e subclasses.
1. Definindo a superclasse#
Primeiro, precisamos criar a superclasse que servirá como base para as subclasses. Vamos definir a classe Animal
, que terá atributos e métodos comuns a todos os animais:
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
Aqui, criamos um método construtor __init__
, que recebe o nome do animal como parâmetro e o armazena em um atributo self.nome
. Isso será comum a todos os animais.
Agora, adicionamos alguns métodos genéricos que qualquer animal pode ter, como comer
e dormir
:
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
2. Criando uma subclasse#
Agora que temos nossa superclasse Animal
, vamos criar uma subclasse Cachorro
, que herda de Animal
. Na definição da classe Cachorro
, passamos Animal
entre parênteses. Isso indica que Cachorro
é uma subclasse de Animal
. Ou seja, Cachorro
herda atributos e métodos de Animal
sem precisar reescrevê-los.
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
# Subclasse
class Cachorro(Animal):
pass
Aqui, estamos dizendo que Cachorro
herda de Animal
, mas ainda não adicionamos nada específico à subclasse. Vamos expandir essa subclasse.
3. Definindo o construtor da subclasse#
Aqui talvez seja o ponto principal. Na subclasse Cachorro
, estamos executando o construtor da superclasse Animal
com a linha super().__init__(nome)
. Isso é necessário para que a subclasse tenha acesso aos atributos e métodos da superclasse. Percebam que o método construtor __init__
da superclasse recebe apenas nome
como parâmetro.
A subclasse Cachorro precisa ter alguns atributos próprios, como a raça
. Para isso fazemos uma atribuindo o parâmetro nome
ao self.
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
# Subclasse
class Cachorro(Animal):
def __init__(self, nome: str, raca: str):
super().__init__(nome) # Chamando o construtor da superclasse Animal
self.raca = raca # Novo atributo exclusivo da subclasse Cachorro
4. Adicionando métodos específicos a subclasse#
Agora podemos definir um método que apenas a classe Cachorro
possui, como latir
:
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
# Subclasse
class Cachorro(Animal):
def __init__(self, nome: str, raca: str):
super().__init__(nome) # Chamando o construtor da superclasse Animal
self.raca = raca # Novo atributo exclusivo da subclasse Cachorro
def latir(self):
print(f"{self.nome}, o {self.raca}, está latindo!")
5. Reutilizando métodos da superclasse#
Com a subclasse Cachorro
pronta, podemos agora criar uma instância dela e testá-la:
cachorro = Cachorro("Rex", "Labrador")
Aqui, criamos um objeto cachorro
do tipo Cachorro
, passando o nome «Rex» e a raça «Labrador».
Graças à herança, podemos utilizar os métodos comer e dormir definidos na superclasse Animal (comer
e dormir
), mesmo que eles não existam diretamente na subclasse Cachorro. Estamos reutilizando o método da superclasse Animal
.
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
# Subclasse
class Cachorro(Animal):
def __init__(self, nome: str, raca: str):
super().__init__(nome) # Chamando o construtor da superclasse Animal
self.raca = raca # Novo atributo exclusivo da subclasse Cachorro
def latir(self):
print(f"{self.nome}, o {self.raca}, está latindo!")
cachorro = Cachorro("Rex", "Labrador") # Criando instância da subclasse Cachorro
cachorro.comer() # Reutilizando métodos da superclasse Animal
cachorro.dormir() # Reutilizando métodos da superclasse Animal
Rex está comendo.
Rex está dormindo.
6. Usando métodos da subclasse#
E finalmente chamamos o método latir
, que é exclusivo da subclasse Cachorro
:
# Superclasse
class Animal:
def __init__(self, nome): # Método construtor da superclasse Animal
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
# Subclasse
class Cachorro(Animal):
def __init__(self, nome: str, raca: str):
super().__init__(nome) # Chamando o construtor da superclasse Animal
self.raca = raca # Novo atributo exclusivo da subclasse Cachorro
def latir(self):
print(f"{self.nome}, o {self.raca}, está latindo!")
cachorro = Cachorro("Rex", "Labrador") # Criando instância da subclasse Cachorro
cachorro.comer() # Reutilizando métodos da superclasse Animal
cachorro.dormir() # Reutilizando métodos da superclasse Animal
cachorro.latir() # Usando método exclusivo da subclasse Cachorro
Rex está comendo.
Rex está dormindo.
Rex, o Labrador, está latindo!
Agora, e se quisermos adicionar outras subclasses de Animal
, como Gato
, Peixe
, ou outros animais? Basta criar novas subclasses de Animal
e seguir a mesma lógica que fizemos com Cachorro
.
# Superclasse
class Animal:
def __init__(self, nome):
self.nome = nome
def comer(self):
print(f"{self.nome} está comendo.")
def dormir(self):
print(f"{self.nome} está dormindo.")
class Cachorro(Animal):
def __init__(self, nome: str, raca: str):
super().__init__(nome)
self.raca = raca
def latir(self):
print(f"{self.nome}, o {self.raca}, está latindo!")
class Gato(Animal):
def __init__(self, nome: str, cor: str):
super().__init__(nome)
self.cor = cor
def miar(self):
print(f"{self.nome}, o gato {self.cor}, está miando: 'Miau!'")
def escalar(self):
print(f"{self.nome}, o gato {self.cor}, está escalando uma árvore!")
cachorro = Cachorro("Rex", "Labrador")
cachorro.comer()
cachorro.dormir()
cachorro.latir()
gato = Gato("Mingau", "tigrado")
gato.comer()
gato.dormir()
gato.miar()
gato.escalar()
Rex está comendo.
Rex está dormindo.
Rex, o Labrador, está latindo!
Mingau está comendo.
Mingau está dormindo.
Mingau, o gato tigrado, está miando: 'Miau!'
Mingau, o gato tigrado, está escalando uma árvore!
Da mesma forma que Cachorro
, a subclasse Gato
herda os atributos e métodos da superclasse Animal
, implementando apenas o atributo específico cor
e os métodos .miar()
e .escalar()
.
Atenção (relação entre subclasses e superclasse)
Só podemos usar herança no caso acima pois a relação entre as subclasses e a superclasse faz sentido. Ou seja, Cachorro
é um tipo de Animal
, e Gato
é um tipo de Animal
. Se tal relação não fizer sentido, talvez o melhor seja não usar herança!
Vamos ver um outro exemplo real para fixar o conceito de herança.
Exemplo: jogo online#
class Personagem:
def __init__(self, nome: str, vida: int, ataque: int, defesa: int):
self.nome = nome
self.vida = vida
self.ataque = ataque
self.defesa = defesa
def atacar(self):
print(f"{self.nome} atacou com {self.ataque} pontos!")
def defender(self):
print(f"{self.nome} está se defendendo com {self.defesa} pontos!")
def status(self):
print(f"{self.nome}: Vida = {self.vida}; Poder de ataque = {self.ataque}")
class Guerreiro(Personagem):
def __init__(self, nome, vida, ataque, defesa, forca_extra):
# Chamando o construtor da superclasse Personagem
super().__init__(nome, vida, ataque, defesa)
self.forca_extra = forca_extra
def golpe_espada(self):
dano = self.ataque + self.forca_extra
print(f"{self.nome} desferiu um golpe de espada com {dano} de dano!")
class Mago(Personagem):
def __init__(self, nome, vida, ataque, defesa, mana):
super().__init__(nome, vida, ataque, defesa)
self.mana = mana
def lançar_magia(self):
if self.mana >= 10:
self.mana -= 10
print(f"{self.nome} lançou uma magia poderosa e consumiu 10 de mana!")
else:
print(f"{self.nome} não tem mana suficiente!")
guerreiro = Guerreiro(nome="Thor", vida=100, ataque=20, defesa=10, forca_extra=50)
guerreiro.atacar()
guerreiro.defender()
guerreiro.golpe_espada()
guerreiro.status()
mago = Mago("Gandalf", vida=80, ataque=15, defesa=2, mana=15)
mago.atacar()
mago.lançar_magia()
mago.lançar_magia()
mago.status()
Thor atacou com 20 pontos!
Thor está se defendendo com 10 pontos!
Thor desferiu um golpe de espada com 70 de dano!
Thor: Vida = 100; Poder de ataque = 20
Gandalf atacou com 15 pontos!
Gandalf lançou uma magia poderosa e consumiu 10 de mana!
Gandalf não tem mana suficiente!
Gandalf: Vida = 80; Poder de ataque = 15
Observem bem como o código acima fica agradável de ler! Parece realmente uma história, ainda mais considerando bons nomes para as variáveis, classes e métodos! E aqui nem começamos a brincar com a interação entre as classes, estamos trabalhando com elas (Mago
e Guerreiro
) de forma isolada usando herança.
Poderíamos, por exemplo, fazer as classes interagirem entre si, e quando uma atacar, a outra perde pontos de vida baseado na diferença entre ataque e defesa… enfim, as possibilidades são infinitas!
Mas vamos com calma, um passo de cada vez.
O problema da herança#
Nem tudo são flores! A herança pode trazer alguns problemas, como:
Acoplamento: a subclasse fica muito dependente da superclasse, o que pode dificultar a manutenção do código. Se a superclasse mudar, todas as subclasses serão impactadas. E isso, se não for bem controlado, pode virar um cenário caótido.
Herança múltipla: Python permite herança múltipla, ou seja, uma subclasse pode herdar de mais de uma superclasse. Eu sequer vou explicar isso aqui, pois é confuso e difícil demais pra entender (e não tem muita utilidade também). A subclasse herda atributos e métodos de várias superclasses, o que pode gerar conflitos e dores de cabeça pra entender.
Explosão de subclasses: Se não tivermos cuidado, podemos criar subclasses em excesso, o que pode dificultar e muito a manutenção do código. Se tivermos muitas subclasses, talvez seja melhor repensar a estrutura do código. Para evitar esse tipo de problema, podemos usar uma outra estratégia chamada composição, que falarei mais à frente no livro.
Não respeitar a relação: quando a relação subclasse é um tipo da superclasse não fizer sentido, usar herança é conceitualmente errado, e o código fica horrível de ler. Ou então inverter a relação, ou seja, a superclasse ser um tipo da subclasse. Imagine que você implemente uma classe
Cachorro
que herda dePessoa
(cachorro é um tipo de pessoa, oi??), ou então uma classeAparelhoEletronico
que herda deTelevisão
(um aparelho eletrônico é um tipo de TV, não seria o contrário?)… enfim, são exemplos de relações que não fazem sentido.
Conclusão#
Nesta seção você aprendeu sobre herança em POO. A herança é um conceito importante que permite a reutilização de código. Vimos uma sintaxe básica de implementação, com a expansão de 2 subclasses de Animal
: Cachorro
e Gato
. Também vimos um exemplo mais real de herança com as classes Mago
e Guerreiro
em um jogo online. Ao final, discutimos os problemas da herança e quando não devemos usá-la.
No próximo capítulo vamos falar sobre encapsulamento, que é outro pilar da programação orientada a objetos. As classes devem ser como caixas pretas, e o encapsulamento é o conceito que nos ajuda a manter essa caixa preta fechada.