Herança em Python
LidoNesta aula, exploramos a herança em Python, um pilar da programação orientada a objetos. Aprendemos a sintaxe para criar subclasses, a importância do super() para inicializar a classe base, a sobrescrita de métodos e a ordem de resolução de métodos (MRO) com a C3 linearization. Inclui exemplos práticos, exercícios e referências.
A herança é um dos conceitos fundamentais da programação orientada a objetos (POO). Em Python, ela permite que uma classe (chamada de subclasse ou classe filha) herde atributos e métodos de outra classe (chamada de superclasse ou classe mãe). Isso promove a reutilização de código, a hierarquia lógica e a extensibilidade. Ao dominar a herança, você conseguirá modelar sistemas mais complexos, evitando duplicação e facilitando a manutenção.
Nesta aula, vamos nos aprofundar na sintaxe da herança, no uso de super(), na sobrescrita de métodos e no mecanismo de resolução de ordem de métodos (MRO). Vamos também discutir boas práticas e apresentar exercícios para fixar o conteúdo.
Sintaxe
Em Python, para criar uma subclasse, basta definir uma nova classe e colocar o nome da superclasse entre parênteses na declaração. A sintaxe é simples e direta:
class Animal:
def __init__(self, nome):
self.nome = nome
def fazer_som(self):
print("Som genérico")
class Cachorro(Animal):
def buscar(self):
print(f"{self.nome} está buscando a bola!")
No exemplo acima, Cachorro herda de Animal. Isso significa que um objeto Cachorro terá o atributo nome (inicializado pelo construtor de Animal) e o método fazer_som(), além de seus próprios métodos como buscar(). A herança pode ser múltipla, ou seja, uma classe pode herdar de várias superclasses, mas isso deve ser usado com cuidado.
Também é possível verificar a relação entre classes com a função issubclass() e a relação entre um objeto e uma classe com isinstance():
print(issubclass(Cachorro, Animal)) # True
c = Cachorro("Rex")
print(isinstance(c, Animal)) # True
Essas funções são úteis para validações em tempo de execução.
super()
A função super() é uma ferramenta essencial quando trabalhamos com herança. Ela permite acessar métodos da superclasse a partir da subclasse. O uso mais comum é no método __init__, para inicializar a parte da classe base.
class Animal:
def __init__(self, nome):
self.nome = nome
class Cachorro(Animal):
def __init__(self, nome, raca):
super().__init__(nome) # chama o __init__ de Animal
self.raca = raca
Sem a chamada a super(), o atributo nome não seria definido, causando erro. Além disso, super() pode ser usado para chamar qualquer método da superclasse, não apenas o construtor. Isso é útil quando queremos estender o comportamento do método original.
Em herança múltipla, super() segue a MRO, garantindo que cada classe na cadeia seja inicializada corretamente. Por isso, é recomendado usar super() em vez de chamar diretamente a classe base pelo nome, para evitar problemas em hierarquias complexas.
Sobrescrita
Sobrescrita (override) é a capacidade de uma subclasse fornecer uma implementação específica de um método que já existe na superclasse. Em Python, isso é feito simplesmente definindo um método com o mesmo nome na subclasse. O método da subclasse será chamado quando o objeto for da subclasse.
class Animal:
def fazer_som(self):
print("Som genérico")
class Cachorro(Animal):
def fazer_som(self):
print("Au au!")
class Gato(Animal):
def fazer_som(self):
print("Miau!")
Quando chamamos fazer_som() em um objeto Cachorro, obtemos "Au au!", e em um Gato, "Miau!". Isso é polimorfismo: o mesmo método se comporta de maneiras diferentes conforme a classe.
Muitas vezes, ao sobrescrever, queremos aproveitar o comportamento da superclasse e adicionar algo. Nesse caso, usamos super():
class Cachorro(Animal):
def fazer_som(self):
super().fazer_som() # chama o método original
print("Au au!")
Isso é útil quando a superclasse tem uma lógica complexa que não queremos duplicar.
MRO (visão geral)
A MRO (Method Resolution Order) é a ordem em que Python procura por métodos e atributos em uma hierarquia de classes. Em casos de herança múltipla, essa ordem é crucial para evitar ambiguidades. Python usa o algoritmo C3 linearization, que garante que cada classe seja visitada apenas uma vez e que as subclasses apareçam antes das superclasses.
Podemos inspecionar a MRO de uma classe usando o atributo __mro__ ou o método mro():
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
A MRO é importante porque afeta a ordem de chamada de super() e a resolução de métodos. Um padrão comum em herança múltipla é o uso de mixins, que são classes pequenas que fornecem funcionalidades específicas. A MRO garante que os mixins sejam chamados na ordem correta.
Para entender melhor, considere um exemplo de herança em diamante: D herda de B e C, que herdam de A. A MRO lineariza para D, B, C, A, evitando que A seja chamada duas vezes. Isso é essencial para a consistência do estado.
Boas práticas e observações finais
Ao usar herança, siga estas boas práticas:
- Prefira composição em vez de herança quando a relação não for um "é um" claro.
- Use
super()em vez de chamar a classe base diretamente para garantir consistência. - Evite heranças múltiplas profundas; prefira mixins pequenos.
- Documente a intenção das classes e métodos.
Herança é uma ferramenta poderosa, mas deve ser usada com moderação e clareza. Entender esses conceitos é fundamental para escrever código Python idiomático e eficiente.
Exercícios
Crie uma classe
Veiculocom atributosmarcaemodelo. Depois, crie uma subclasseCarroque adicione o atributonumero_portas. Instancie um objeto e imprima seus atributos.✓ Resposta:class Veiculo: def __init__(self, marca, modelo): self.marca = marca self.modelo = modelo class Carro(Veiculo): def __init__(self, marca, modelo, numero_portas): super().__init__(marca, modelo) self.numero_portas = numero_portas carro = Carro("Fiat", "Uno", 4) print(carro.marca, carro.modelo, carro.numero_portas) # Saída: Fiat Uno 4Dada a classe
Pessoacom o métodoapresentar(), crie uma subclasseEstudanteque sobrescreva esse método para incluir o curso. Usesuper()para chamar o método original.✓ Resposta:class Pessoa: def __init__(self, nome): self.nome = nome def apresentar(self): print(f"Olá, eu sou {self.nome}.") class Estudante(Pessoa): def __init__(self, nome, curso): super().__init__(nome) self.curso = curso def apresentar(self): super().apresentar() print(f"Estudo {self.curso}.") e = Estudante("Maria", "Engenharia") e.apresentar() # Saída: # Olá, eu sou Maria. # Estudo Engenharia.Considere a seguinte hierarquia:
Aé a classe base,Bherda deA, eCherda deB. Escreva as classes com um métodoquem_sou()que imprima o nome da classe. Chame o método em uma instância deCe verifique a MRO com__mro__.✓ Resposta:class A: def quem_sou(self): print("A") class B(A): def quem_sou(self): print("B") class C(B): pass c = C() c.quem_sou() # Saída: B (porque B sobrescreve, e C não) print(C.__mro__) # (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)Explique o que acontece se a classe
Dherdar deBeC, que herdam deA. Como a MRO resolve a ordem? Escreva um exemplo com um métodoacao()que chamesuper().acao()e observe a saída.✓ Resposta:class A: def acao(self): print("A") class B(A): def acao(self): print("B") super().acao() class C(A): def acao(self): print("C") super().acao() class D(B, C): def acao(self): print("D") super().acao() d = D() d.acao() # Saída: # D # B # C # A print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)Crie uma classe
Pontocom coordenadas x e y. Depois, crie uma subclassePonto3Dque adicione z. Sobrescreva o métododistancia_origempara calcular a distância tridimensional. Usesuper()para reutilizar a lógica bidimensional.✓ Resposta:import math class Ponto: def __init__(self, x, y): self.x = x self.y = y def distancia_origem(self): return math.sqrt(self.x**2 + self.y**2) class Ponto3D(Ponto): def __init__(self, x, y, z): super().__init__(x, y) self.z = z def distancia_origem(self): dist2d = super().distancia_origem() return math.sqrt(dist2d**2 + self.z**2) p = Ponto3D(3, 4, 12) print(p.distancia_origem()) # 13.0