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:

  1. Reutilização de código: a subclasse herda atributos e métodos da superclasse, o que evita a reescrita de código.

  2. 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.

  3. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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 de Pessoa (cachorro é um tipo de pessoa, oi??), ou então uma classe AparelhoEletronico que herda de Televisã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.

⚒️👷 (WIP) capítulo em construção ⚒️👷#