Ir para o conteúdo

Herança e Polimorfismo em Python

Introdução

A herança e o polimorfismo são dois conceitos fundamentais da Programação Orientada a Objetos que permitem criar hierarquias de classes e reutilizar código de forma eficiente. Estes conceitos são cruciais para construir sistemas bem organizados, extensíveis e de fácil manutenção.

Objetivos de Aprendizado

  • Compreender o conceito de herança e sua implementação em Python
  • Entender como usar herança múltipla e suas implicações
  • Aprender o funcionamento do método super() para acessar métodos da classe pai
  • Dominar o conceito de polimorfismo e suas aplicações
  • Explorar classes abstratas e interfaces em Python
  • Aplicar boas práticas no uso de herança e polimorfismo

Herança em Python

A herança permite que uma classe (subclasse ou classe filha) herde atributos e métodos de outra classe (superclasse ou classe pai).

# Classe pai
class Animal:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def fazer_som(self):
        print("Algum som de animal")

    def apresentar(self):
        return f"Olá, eu sou {self.nome} e tenho {self.idade} anos."

# Classe filha
class Cachorro(Animal):
    def __init__(self, nome, idade, raca):
        # Chamando o construtor da classe pai
        super().__init__(nome, idade)
        self.raca = raca

    # Sobrescrevendo o método da classe pai
    def fazer_som(self):
        print("Au au!")

    # Método específico da classe filha
    def abanar_rabo(self):
        print(f"{self.nome} está abanando o rabo!")

# Usando as classes
animal = Animal("Animal Genérico", 5)
print(animal.apresentar())  # Olá, eu sou Animal Genérico e tenho 5 anos.
animal.fazer_som()  # Algum som de animal

rex = Cachorro("Rex", 3, "Labrador")
print(rex.apresentar())  # Olá, eu sou Rex e tenho 3 anos.
rex.fazer_som()  # Au au!
rex.abanar_rabo()  # Rex está abanando o rabo!

# Verificando relações de herança
print(isinstance(rex, Cachorro))  # True
print(isinstance(rex, Animal))  # True
print(issubclass(Cachorro, Animal))  # True
class Veiculo:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.ligado = False

    def ligar(self):
        self.ligado = True
        return f"{self.marca} {self.modelo} ligado."

    def desligar(self):
        self.ligado = False
        return f"{self.marca} {self.modelo} desligado."

    def info(self):
        return f"Veículo: {self.marca} {self.modelo}"

class Carro(Veiculo):
    def __init__(self, marca, modelo, portas):
        super().__init__(marca, modelo)
        self.portas = portas

    def info(self):
        return f"Carro: {self.marca} {self.modelo}, {self.portas} portas"

class SUV(Carro):
    def __init__(self, marca, modelo, portas, tracao):
        super().__init__(marca, modelo, portas)
        self.tracao = tracao

    def info(self):
        return f"SUV: {self.marca} {self.modelo}, {self.portas} portas, tração {self.tracao}"

# Criando instâncias
veiculo = Veiculo("Genérico", "Básico")
carro = Carro("Toyota", "Corolla", 4)
suv = SUV("Honda", "CR-V", 5, "4x4")

# Cada classe na hierarquia sobrescreve o método info
print(veiculo.info())  # Veículo: Genérico Básico
print(carro.info())    # Carro: Toyota Corolla, 4 portas
print(suv.info())      # SUV: Honda CR-V, 5 portas, tração 4x4

# Métodos da classe base funcionam em todas as subclasses
print(veiculo.ligar())  # Genérico Básico ligado.
print(carro.ligar())    # Toyota Corolla ligado.
print(suv.ligar())      # Honda CR-V ligado.
class Dispositivo:
    def __init__(self, nome, marca):
        self.nome = nome
        self.marca = marca
        self.ligado = False

    def ligar(self):
        self.ligado = True
        return f"{self.nome} ligado."

    def desligar(self):
        self.ligado = False
        return f"{self.nome} desligado."

class Conectavel:
    def __init__(self):
        self.conectado = False

    def conectar(self):
        self.conectado = True
        return "Conectado à internet."

    def desconectar(self):
        self.conectado = False
        return "Desconectado da internet."

class Bateria:
    def __init__(self):
        self.carga = 100

    def verificar_bateria(self):
        return f"Bateria: {self.carga}%"

    def carregar(self, quantidade):
        self.carga = min(100, self.carga + quantidade)
        return f"Carregado. Bateria: {self.carga}%"

# Herança múltipla
class Smartphone(Dispositivo, Conectavel, Bateria):
    def __init__(self, nome, marca, modelo, sistema_operacional):
        # Inicializando as superclasses
        Dispositivo.__init__(self, nome, marca)
        Conectavel.__init__(self)
        Bateria.__init__(self)

        # Atributos específicos
        self.modelo = modelo
        self.sistema_operacional = sistema_operacional

    def fazer_ligacao(self, numero):
        if not self.ligado:
            return "Smartphone desligado. Ligue-o primeiro."

        if self.carga < 10:
            return "Bateria fraca. Carregue o smartphone."

        self.carga -= 5
        return f"Ligando para {numero}..."

# Usando a classe com herança múltipla
meu_smartphone = Smartphone("Galaxy S21", "Samsung", "S21", "Android")

print(meu_smartphone.ligar())            # Galaxy S21 ligado.
print(meu_smartphone.conectar())         # Conectado à internet.
print(meu_smartphone.verificar_bateria())  # Bateria: 100%
print(meu_smartphone.fazer_ligacao("123-456-7890"))  # Ligando para 123-456-7890...
print(meu_smartphone.verificar_bateria())  # Bateria: 95%
# O MRO (Method Resolution Order) define a ordem em que Python
# procura métodos em classes com herança múltipla

class A:
    def quem_sou_eu(self):
        return "Eu sou A"

class B(A):
    def quem_sou_eu(self):
        return "Eu sou B"

class C(A):
    def quem_sou_eu(self):
        return "Eu sou C"

class D(B, C):
    pass

class E(C, B):
    pass

# Usando as classes
d = D()
e = E()

# D herda de B e C, nessa ordem
print(d.quem_sou_eu())  # Eu sou B

# E herda de C e B, nessa ordem
print(e.quem_sou_eu())  # Eu sou C

# Visualizando o MRO completo
print(D.mro())  # [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]
print(E.mro())  # [<class 'E'>, <class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>]

# O algoritmo C3 é usado para determinar o MRO
# e evitar ambiguidades na resolução de métodos

O método super()

O método super() é usado para chamar métodos da classe pai, o que é essencial para a extensão de comportamentos em subclasses.

class Forma:
    def __init__(self, cor):
        self.cor = cor

    def info(self):
        return f"Forma de cor {self.cor}"

class Retangulo(Forma):
    def __init__(self, cor, largura, altura):
        # Chama o construtor da classe pai
        super().__init__(cor)

        # Adiciona atributos específicos
        self.largura = largura
        self.altura = altura

    def info(self):
        # Estende o método da classe pai
        return f"{super().info()}, largura: {self.largura}, altura: {self.altura}"

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

class Quadrado(Retangulo):
    def __init__(self, cor, lado):
        # Chama o construtor da classe Retangulo
        super().__init__(cor, lado, lado)

    def info(self):
        # Como a classe pai já é Retangulo, podemos personalizar mais
        info_pai = super().info()
        return info_pai.replace("largura: {}, altura: {}".format(self.largura, self.altura), 
                              f"lado: {self.largura}")

# Usando as classes
forma = Forma("azul")
retangulo = Retangulo("verde", 10, 5)
quadrado = Quadrado("vermelho", 7)

print(forma.info())     # Forma de cor azul
print(retangulo.info())  # Forma de cor verde, largura: 10, altura: 5
print(quadrado.info())   # Forma de cor vermelho, lado: 7

print(f"Área do retângulo: {retangulo.area()}")  # Área do retângulo: 50
print(f"Área do quadrado: {quadrado.area()}")    # Área do quadrado: 49
class A:
    def metodo(self):
        print("Método na classe A")

class B(A):
    def metodo(self):
        print("Método na classe B")
        super().metodo()  # Chama A.metodo()

class C(A):
    def metodo(self):
        print("Método na classe C")
        super().metodo()  # Chama A.metodo()

class D(B, C):
    def metodo(self):
        print("Método na classe D")
        super().metodo()  # Chama B.metodo() (primeiro na ordem MRO)

# Usando a classe D
d = D()
d.metodo()
# Saída:
# Método na classe D
# Método na classe B
# Método na classe C
# Método na classe A

# Explicação:
# 1. D.metodo() chama super().metodo(), que é B.metodo()
# 2. B.metodo() imprime sua mensagem e chama super().metodo(), que é C.metodo()
# 3. C.metodo() imprime sua mensagem e chama super().metodo(), que é A.metodo()
# 4. A.metodo() imprime sua mensagem

# O MRO para D é: [D, B, C, A, object]
# super() sempre segue essa ordem
class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def apresentar(self):
        return f"Olá, eu sou {self.nome} e tenho {self.idade} anos."

class Aluno(Pessoa):
    def __init__(self, nome, idade, matricula):
        super().__init__(nome, idade)
        self.matricula = matricula

    def apresentar(self):
        return f"{super().apresentar()} Sou aluno com matrícula {self.matricula}."

class AlunoMonitor(Aluno):
    def __init__(self, nome, idade, matricula, disciplina):
        super().__init__(nome, idade, matricula)
        self.disciplina = disciplina

    def apresentar(self):
        # Usando super() com argumentos específicos
        # super(AlunoMonitor, self) se refere à classe Aluno
        apresentacao_base = super().apresentar()

        # Também poderia chamar diretamente a classe avô
        # apresentacao_avô = super(Aluno, self).apresentar()
        # ou apresentacao_avô = Pessoa.apresentar(self)

        return f"{apresentacao_base} Sou monitor da disciplina de {self.disciplina}."

# Usando as classes
pessoa = Pessoa("Ana", 25)
aluno = Aluno("João", 20, "A12345")
monitor = AlunoMonitor("Pedro", 22, "B67890", "Programação")

print(pessoa.apresentar())
# Olá, eu sou Ana e tenho 25 anos.

print(aluno.apresentar())
# Olá, eu sou João e tenho 20 anos. Sou aluno com matrícula A12345.

print(monitor.apresentar())
# Olá, eu sou Pedro e tenho 22 anos. Sou aluno com matrícula B67890. Sou monitor da disciplina de Programação.

Polimorfismo

O polimorfismo permite que objetos de diferentes classes sejam tratados como objetos de uma classe comum, possibilitando que métodos com o mesmo nome tenham comportamentos diferentes dependendo da classe.

# Diferentes classes implementando o mesmo método
class Animal:
    def falar(self):
        pass  # Método genérico a ser sobrescrito

    def emitir_som(self):
        # Esse método usa o método falar() polimórfico
        print(f"O animal emite: {self.falar()}")

class Cachorro(Animal):
    def falar(self):
        return "Au au!"

class Gato(Animal):
    def falar(self):
        return "Miau!"

class Pato(Animal):
    def falar(self):
        return "Quack quack!"

# Função que usa o polimorfismo
def fazer_animal_falar(animal):
    print(f"O {animal.__class__.__name__} diz: {animal.falar()}")

# Criando instâncias
rex = Cachorro()
felix = Gato()
donald = Pato()

# Chamando o mesmo método em diferentes objetos
fazer_animal_falar(rex)     # O Cachorro diz: Au au!
fazer_animal_falar(felix)   # O Gato diz: Miau!
fazer_animal_falar(donald)  # O Pato diz: Quack quack!

# Usando o método que chama o método polimórfico
rex.emitir_som()    # O animal emite: Au au!
felix.emitir_som()  # O animal emite: Miau!
donald.emitir_som() # O animal emite: Quack quack!

# Armazenando em uma lista (duck typing)
animais = [rex, felix, donald]
for animal in animais:
    fazer_animal_falar(animal)
# Duck Typing: "Se anda como um pato e faz quack como um pato, então é um pato"
# Em Python, o tipo exato de um objeto é menos importante que os métodos e atributos que ele possui

class Pato:
    def nadar(self):
        return "Pato nadando"

    def fazer_quack(self):
        return "Quack quack!"

class PessoaQueImitaPato:
    def nadar(self):
        return "Pessoa fingindo nadar como pato"

    def fazer_quack(self):
        return "Pessoa imitando: Quaaack!"

class Cisne:
    def nadar(self):
        return "Cisne nadando elegantemente"

    def cantar(self):
        return "♪♫♪"

# Função que usa duck typing
def fazer_pato_agir(pato):
    # Não verifica o tipo, apenas se tem os métodos necessários
    try:
        print(pato.nadar())
        print(pato.fazer_quack())
        print("Este objeto age como um pato!")
    except AttributeError as e:
        print(f"Este objeto não age como um pato: {e}")

# Testando com diferentes objetos
pato = Pato()
imitador = PessoaQueImitaPato()
cisne = Cisne()

print("Testando um pato real:")
fazer_pato_agir(pato)
# Pato nadando
# Quack quack!
# Este objeto age como um pato!

print("\nTestando um imitador de pato:")
fazer_pato_agir(imitador)
# Pessoa fingindo nadar como pato
# Pessoa imitando: Quaaack!
# Este objeto age como um pato!

print("\nTestando um cisne:")
fazer_pato_agir(cisne)
# Cisne nadando elegantemente
# Este objeto não age como um pato: 'Cisne' object has no attribute 'fazer_quack'
# Polimorfismo está presente em muitas operações built-in do Python

# O operador + é polimórfico
print(10 + 5)          # 15 (soma de inteiros)
print("Olá " + "mundo") # Olá mundo (concatenação de strings)
print([1, 2] + [3, 4])  # [1, 2, 3, 4] (concatenação de listas)

# O operador * também é polimórfico
print(5 * 3)     # 15 (multiplicação de inteiros)
print("Py" * 3)  # PyPyPy (repetição de string)
print([7] * 3)   # [7, 7, 7] (repetição de lista)

# A função len() funciona com diferentes tipos
print(len("Python"))        # 6 (comprimento de string)
print(len([1, 2, 3, 4, 5])) # 5 (comprimento de lista)
print(len({"a": 1, "b": 2})) # 2 (número de chaves em dicionário)

# Criando uma classe que implementa métodos especiais para operadores
class Ponto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __add__(self, outro):
        return Ponto(self.x + outro.x, self.y + outro.y)

    def __mul__(self, escalar):
        return Ponto(self.x * escalar, self.y * escalar)

    def __len__(self):
        import math
        # Retorna a distância Manhattan como um inteiro
        return int(abs(self.x) + abs(self.y))

p1 = Ponto(3, 4)
p2 = Ponto(2, 7)

print(f"p1 = {p1}")         # p1 = (3, 4)
print(f"p2 = {p2}")         # p2 = (2, 7)
print(f"p1 + p2 = {p1 + p2}")  # p1 + p2 = (5, 11)
print(f"p1 * 3 = {p1 * 3}")  # p1 * 3 = (9, 12)
print(f"len(p1) = {len(p1)}")  # len(p1) = 7

Classes Abstratas e Interfaces

Python não tem interfaces formais como algumas linguagens, mas classes abstratas podem ser usadas para definir estruturas semelhantes.

from abc import ABC, abstractmethod

# Classe abstrata
class FormaGeometrica(ABC):
    @abstractmethod
    def area(self):
        """Calcula a área da forma."""
        pass

    @abstractmethod
    def perimetro(self):
        """Calcula o perímetro da forma."""
        pass

    def descricao(self):
        """Método concreto (não abstrato)."""
        return "Esta é uma forma geométrica."

# Não podemos instanciar uma classe abstrata
# forma = FormaGeometrica()  # Isso causaria um erro

# Classes concretas que implementam a classe abstrata
class Retangulo(FormaGeometrica):
    def __init__(self, largura, altura):
        self.largura = largura
        self.altura = altura

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

    def perimetro(self):
        return 2 * (self.largura + self.altura)

    def descricao(self):
        return f"Este é um retângulo de {self.largura}x{self.altura}."

class Circulo(FormaGeometrica):
    def __init__(self, raio):
        self.raio = raio

    def area(self):
        import math
        return math.pi * self.raio ** 2

    def perimetro(self):
        import math
        return 2 * math.pi * self.raio

    # Usamos a implementação padrão de descricao()

# Agora podemos instanciar as classes concretas
retangulo = Retangulo(10, 5)
circulo = Circulo(7)

# E usar seus métodos
print(f"Área do retângulo: {retangulo.area()}")  # Área do retângulo: 50
print(f"Perímetro do retângulo: {retangulo.perimetro()}")  # Perímetro do retângulo: 30
print(retangulo.descricao())  # Este é um retângulo de 10x5.

print(f"Área do círculo: {circulo.area():.2f}")  # Área do círculo: 153.94
print(f"Perímetro do círculo: {circulo.perimetro():.2f}")  # Perímetro do círculo: 43.98
print(circulo.descricao())  # Esta é uma forma geométrica.
from abc import ABC, abstractmethod

# Interface para dispositivos que podem conectar-se
class Conectavel(ABC):
    @abstractmethod
    def conectar(self):
        pass

    @abstractmethod
    def desconectar(self):
        pass

# Interface para dispositivos que têm bateria
class ComBateria(ABC):
    @abstractmethod
    def verificar_bateria(self):
        pass

    @abstractmethod
    def carregar(self, quantidade):
        pass

# Classes que implementam as interfaces
class Smartphone(Conectavel, ComBateria):
    def __init__(self, modelo):
        self.modelo = modelo
        self.conectado = False
        self.bateria = 100

    def conectar(self):
        self.conectado = True
        return f"{self.modelo} conectado à rede."

    def desconectar(self):
        self.conectado = False
        return f"{self.modelo} desconectado da rede."

    def verificar_bateria(self):
        return f"Bateria do {self.modelo}: {self.bateria}%"

    def carregar(self, quantidade):
        self.bateria = min(100, self.bateria + quantidade)
        return f"{self.modelo} carregado. Bateria: {self.bateria}%"

class Laptop(Conectavel, ComBateria):
    def __init__(self, marca):
        self.marca = marca
        self.conectado = False
        self.bateria = 100

    def conectar(self):
        self.conectado = True
        return f"Laptop {self.marca} conectado ao Wi-Fi."

    def desconectar(self):
        self.conectado = False
        return f"Laptop {self.marca} desconectado do Wi-Fi."

    def verificar_bateria(self):
        return f"Bateria do laptop {self.marca}: {self.bateria}%"

    def carregar(self, quantidade):
        self.bateria = min(100, self.bateria + quantidade)
        return f"Laptop {self.marca} carregado. Bateria: {self.bateria}%"

# Função que trabalha com objetos que implementam Conectavel
def conectar_dispositivo(dispositivo):
    if isinstance(dispositivo, Conectavel):
        return dispositivo.conectar()
    else:
        return "Este dispositivo não suporta conexão."

# Função que trabalha com objetos que implementam ComBateria
def mostrar_bateria(dispositivo):
    if isinstance(dispositivo, ComBateria):
        return dispositivo.verificar_bateria()
    else:
        return "Este dispositivo não tem bateria."

# Criando instâncias
celular = Smartphone("iPhone 13")
notebook = Laptop("Dell")

# Usando as funções
print(conectar_dispositivo(celular))  # iPhone 13 conectado à rede.
print(conectar_dispositivo(notebook))  # Laptop Dell conectado ao Wi-Fi.

print(mostrar_bateria(celular))  # Bateria do iPhone 13: 100%
print(mostrar_bateria(notebook))  # Bateria do laptop Dell: 100%

# Usando o polimorfismo através das interfaces
dispositivos = [celular, notebook]
for d in dispositivos:
    print(d.conectar())
    print(d.verificar_bateria())
    print(d.carregar(10))  # Isso não terá efeito visível pois as baterias já estão em 100%
    print(d.desconectar())
from abc import ABC, abstractmethod
import re

# Interface para validadores
class Validador(ABC):
    @abstractmethod
    def validar(self, valor):
        """Retorna True se válido, False caso contrário."""
        pass

    @abstractmethod
    def mensagem_erro(self):
        """Retorna a mensagem de erro."""
        pass

# Implementações concretas
class ValidadorEmail(Validador):
    def __init__(self):
        self._ultimo_erro = ""
        self._padrao = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

    def validar(self, email):
        if not isinstance(email, str):
            self._ultimo_erro = "Email deve ser uma string."
            return False

        if not re.match(self._padrao, email):
            self._ultimo_erro = "Formato de email inválido."
            return False

        self._ultimo_erro = ""
        return True

    def mensagem_erro(self):
        return self._ultimo_erro

class ValidadorSenha(Validador):
    def __init__(self, min_tamanho=8):
        self._ultimo_erro = ""
        self._min_tamanho = min_tamanho

    def validar(self, senha):
        if not isinstance(senha, str):
            self._ultimo_erro = "Senha deve ser uma string."
            return False

        if len(senha) < self._min_tamanho:
            self._ultimo_erro = f"Senha deve ter pelo menos {self._min_tamanho} caracteres."
            return False

        if not any(c.isupper() for c in senha):
            self._ultimo_erro = "Senha deve conter pelo menos uma letra maiúscula."
            return False

        if not any(c.islower() for c in senha):
            self._ultimo_erro = "Senha deve conter pelo menos uma letra minúscula."
            return False

        if not any(c.isdigit() for c in senha):
            self._ultimo_erro = "Senha deve conter pelo menos um número."
            return False

        self._ultimo_erro = ""
        return True

    def mensagem_erro(self):
        return self._ultimo_erro

# Função que usa polimorfismo
def validar_entrada(valor, validador):
    if validador.validar(valor):
        return "Válido!"
    else:
        return f"Inválido: {validador.mensagem_erro()}"

# Testando os validadores
email_validador = ValidadorEmail()
senha_validador = ValidadorSenha()

print(validar_entrada("[email protected]", email_validador))  # Válido!
print(validar_entrada("usuario@", email_validador))  # Inválido: Formato de email inválido.

print(validar_entrada("Senha123", senha_validador))  # Válido!
print(validar_entrada("senha123", senha_validador))  # Inválido: Senha deve conter pelo menos uma letra maiúscula.
print(validar_entrada("SENHA123", senha_validador))  # Inválido: Senha deve conter pelo menos uma letra minúscula.
print(validar_entrada("Senhafraca", senha_validador))  # Inválido: Senha deve conter pelo menos um número.

Exemplo Prático: Sistema de Formas Geométricas

Um exemplo completo que demonstra herança, polimorfismo e os princípios SOLID.

from abc import ABC, abstractmethod
import math

# Classe abstrata base
class Forma(ABC):
    @abstractmethod
    def area(self):
        """Calcula a área da forma."""
        pass

    @abstractmethod
    def perimetro(self):
        """Calcula o perímetro da forma."""
        pass

    def __str__(self):
        return f"{self.__class__.__name__}: área={self.area():.2f}, perímetro={self.perimetro():.2f}"


# Implementações concretas de formas geométricas
class Retangulo(Forma):
    def __init__(self, largura, altura):
        if largura <= 0 ou altura <= 0:
            raise ValueError("Dimensões devem ser positivas")
        self.largura = largura
        self.altura = altura

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

    def perimetro(self):
        return 2 * (self.largura + self.altura)

    def __str__(self):
        return f"{super().__str__()}, largura={self.largura}, altura={self.altura}"


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

    def __str__(self):
        return f"{self.__class__.__name__}: área={self.area():.2f}, perímetro={self.perimetro():.2f}, lado={self.lado}"


class Circulo(Forma):
    def __init__(self, raio):
        if raio <= 0:
            raise ValueError("Raio deve ser positivo")
        self.raio = raio

    def area(self):
        return math.pi * self.raio ** 2

    def perimetro(self):
        return 2 * math.pi * self.raio

    def __str__(self):
        return f"{super().__str__()}, raio={self.raio}"


class Triangulo(Forma):
    def __init__(self, a, b, c):
        # Verificação da desigualdade triangular
        if a + b <= c ou a + c <= b ou b + c <= a:
            raise ValueError("Estas medidas não formam um triângulo válido")
        self.a = a
        self.b = b
        self.c = c

    def area(self):
        # Fórmula de Heron
        s = (self.a + self.b + self.c) / 2
        return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

    def perimetro(self):
        return self.a + self.b + self.c

    def __str__(self):
        return f"{super().__str__()}, lados={self.a}, {self.b}, {self.c}"


# Classe com funcionalidade adicional
class FormaColorida:
    def __init__(self, forma, cor):
        self.forma = forma
        self.cor = cor

    def area(self):
        return self.forma.area()

    def perimetro(self):
        return self.forma.perimetro()

    def __str__(self):
        return f"{self.forma} (cor: {self.cor})"


# Gerenciador de formas
class GerenciadorFormas:
    def __init__(self):
        self.formas = []

    def adicionar_forma(self, forma):
        if not isinstance(forma, Forma) e não tem hasattr(forma, 'area') e não tem hasattr(forma, 'perimetro'):
            raise TypeError("O objeto deve ser uma forma válida com métodos area() e perimetro()")
        self.formas.append(forma)

    def area_total(self):
        return sum(forma.area() para forma em self.formas)

    def perimetro_total(self):
        return sum(forma.perimetro() para forma em self.formas)

    def listar_formas(self):
        if not self.formas:
            return "Nenhuma forma registrada."

        resultado = "Formas registradas:\n"
        for i, forma em enumerate(self.formas, 1):
            resultado += f"{i}. {forma}\n"

        resultado += f"\nÁrea total: {self.area_total():.2f}"
        resultado += f"\nPerímetro total: {self.perimetro_total():.2f}"
        return resultado


# Demonstração do uso
def main():
    # Criando formas
    try:
        retangulo = Retangulo(5, 3)
        quadrado = Quadrado(4)
        circulo = Circulo(7)
        triangulo = Triangulo(3, 4, 5)

        # Usando composição para adicionar cor
        retangulo_vermelho = FormaColorida(retangulo, "vermelho")
        circulo_azul = FormaColorida(circulo, "azul")

        # Adicionando formas ao gerenciador
        gerenciador = GerenciadorFormas()
        gerenciador.adicionar_forma(retangulo)
        gerenciador.adicionar_forma(quadrado)
        gerenciador.adicionar_forma(circulo)
        gerenciador.adicionar_forma(triangulo)
        gerenciador.adicionar_forma(retangulo_vermelho)
        gerenciador.adicionar_forma(circulo_azul)

        # Exibindo informações
        print(gerenciador.listar_formas())

        # Demonstrando o tratamento de erro
        try:
            triangulo_invalido = Triangulo(1, 1, 10)  # Não é um triângulo válido
        except ValueError como e:
            print(f"\nErro ao criar triângulo: {e}")

    exceto Exception como e:
        print(f"Ocorreu um erro: {e}")


if __name__ == "__main__":
    main()

Boas Práticas

# Use herança quando há uma relação clara "é um"

# BOM: Um carro é um veículo
class Veiculo:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo

    def mover(self):
        return "Veículo se movendo"

class Carro(Veiculo):  # Um carro É UM veículo
    def __init__(self, marca, modelo, portas):
        super().__init__(marca, modelo)
        self.portas = portas

    def mover(self):
        return "Carro dirigindo na estrada"

# RUIM: Uma loja não é um produto
class Produto:
    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco

# Herança incorreta
class Loja(Produto):  # Uma loja NÃO É um produto!
    def __init__(self, nome, endereco):
        super().__init__(nome, 0)  # Forçando a herança
        self.endereco = endereco
        self.produtos = []

# MELHOR: Use composição em vez de herança quando não for "é um"
class Loja:
    def __init__(self, nome, endereco):
        self.nome = nome
        self.endereco = endereco
        self.produtos = []  # Uma loja TEM produtos

    def adicionar_produto(self, produto):
        self.produtos.append(produto)
# O Princípio de Substituição de Liskov (LSP) afirma que 
# objetos de uma superclasse devem poder ser substituídos 
# por objetos de uma subclasse sem afetar a correção do programa

# Violação do LSP
class Retangulo:
    def __init__(self, largura, altura):
        self._largura = largura
        self._altura = altura

    def get_largura(self):
        return self._largura

    def set_largura(self, largura):
        self._largura = largura

    def get_altura(self):
        return self._altura

    def set_altura(self, altura):
        self._altura = altura

    def area(self):
        return self._largura * self._altura

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

    # Sobrescrevendo métodos para manter o quadrado válido
    def set_largura(self, largura):
        self._largura = largura
        self._altura = largura  # Altera altura também

    def set_altura(self, altura):
        self._altura = altura
        self._largura = altura  # Altera largura também

# Função que usa Retangulo
def aumentar_largura(retangulo):
    largura_original = retangulo.get_largura()
    altura_original = retangulo.get_altura()

    retangulo.set_largura(largura_original + 1)

    # Para um retângulo, apenas a largura muda
    # Para um quadrado, a altura também muda!
    return retangulo.area() == (largura_original + 1) * altura_original

# Teste
retangulo = Retangulo(5, 10)
quadrado = Quadrado(5)

print(f"Teste com retângulo: {aumentar_largura(retangulo)}")  # True
print(f"Teste com quadrado: {aumentar_largura(quadrado)}")  # False - Viola o LSP!

# Solução: hierarquia de classes diferente ou usar composição
# A composição é frequentemente mais flexível que a herança

# Abordagem com herança (pode ser inflexível)
class Ave:
    def comer(self):
        return "Comendo sementes"

    def voar(self):
        return "Voando alto"

class Pato(Ave):
    def nadar(self):
        return "Nadando no lago"

class Pinguim(Ave):
    # Problema: pinguins não voam!
    def voar(self):
        return "Não posso voar!"  # Sobrescrever com comportamento nulo

    def nadar(self):
        return "Nadando no oceano gelado"

# Abordagem com composição (mais flexível)
class Comportamento:
    pass

class Voo(Comportamento):
    def voar(self):
        return "Voando alto"

class SemVoo(Comportamento):
    def voar(self):
        return "Não posso voar!"

class Nado(Comportamento):
    def nadar(self):
        return "Nadando na água"

class Alimentacao(Comportamento):
    def comer(self):
        return "Comendo sementes"

class AnimalComposicao:
    def __init__(self, nome):
        self.nome = nome
        self.comportamentos = []

    def adicionar_comportamento(self, comportamento):
        self.comportamentos.append(comportamento)

    def voar(self):
        for c em self.comportamentos:
            if hasattr(c, 'voar'):
                return c.voar()
        return "Este animal não tem comportamento de voo"

    def nadar(self):
        for c em self.comportamentos:
            if hasattr(c, 'nadar'):
                return c.nadar()
        return "Este animal não tem comportamento de nado"

    def comer(self):
        for c em self.comportamentos:
            if hasattr(c, 'comer'):
                return c.comer()
        return "Este animal não tem comportamento de alimentação"

# Criando animais com composição
pombo = AnimalComposicao("Pombo")
pombo.adicionar_comportamento(Voo())
pombo.adicionar_comportamento(Alimentacao())

pinguim = AnimalComposicao("Pinguim")
pinguim.adicionar_comportamento(SemVoo())
pinguim.adicionar_comportamento(Nado())
pinguim.adicionar_comportamento(Alimentacao())

print(f"{pombo.nome}: {pombo.voar()}, {pombo.comer()}")
# Pombo: Voando alto, Comendo sementes

print(f"{pinguim.nome}: {pinguim.voar()}, {pinguim.nadar()}, {pinguim.comer()}")
# Pinguim: Não posso voar!, Nadando na água, Comendo sementes

Resumo

Nesta aula, você aprendeu sobre os seguintes tópicos:

  • Herança: como criar hierarquias de classes e reutilizar código
  • Herança múltipla: como uma classe pode herdar de várias classes pai e como o MRO funciona
  • O método super(): como chamar métodos da classe pai, inclusive em herança múltipla
  • Polimorfismo: como usar a mesma interface para diferentes tipos de objetos
  • Duck Typing: como o Python trata o polimorfismo através de comportamentos em vez de tipos
  • Classes abstratas e interfaces: como definir contratos para classes
  • Boas práticas: como e quando usar herança e composição corretamente

Próximos Passos

Na próxima aula, iremos explorar o tópico de Testes em Python, aprendendo como verificar se nosso código funciona corretamente e como garantir a qualidade do software.

Avance para a próxima aula →

← Voltar para POO em Python