Em OOP, herança é o mecanismo pelo qual estendemos a funcionalidade de uma classe. Por exemplo, suponha que a gente precise representar veículos de diferentes marcas e modelos em um programa. Uma abordagem é criar uma classe para representar cada veículo diferente. Porém, existem informações que são comuns a todos os veículos, como o tipo do veículo (motocicleta, carro, caminhão, etc.), chassi, marca, modelo, ano, e etc.
Uma abordagem mais elegante é usar herança de modo a criar uma classe que armazena informações comuns a todos os tipos de veículos que precisamos representar e subsequentemente estender essa classe para representar veículos específicos. O exemplo abaixo mostra a classe Veiculo
.
class Veiculo:
def __init__(self, tipo, chassi, marca, modelo, ano):
self.tipo = tipo
self.chassi = chassi
self.marca = marca
self.modelo = modelo
self.ano = ano
Como implementar herança em Python?
Agora que sabemos como representar um veículo genérico, desejamos estender essa classe de modo a representar motocicletas.
Suponha que, além das informações comuns a todos os veículos, motocicletas também contenham dados sobre a cilindrada da motocicleta em questão. Para representar uma motocicleta, podemos criar uma classe que herda o estado e comportamento da classe Veiculo
e a estende de modo a adicionar informação sobre a cilidrada. O exemplo abaixo ilustra como fazer isso em Python.
class Motocicleta(Veiculo):
def __init__(self, tipo, chassi, marca, modelo, ano, cilindrada):
super().__init__(tipo, chassi, marca, modelo, ano)
self.cilindrada = cilindrada
Note que:
class Motocicleta(Veiculo)
indica que a classeMotocicleta
herda da classeVeiculo
.super().__init__(tipo, chassi, marca, modelo, ano)
invoca o método__init__
da super classe (ou classe base,Veiculo
).
No exemplo acima, dizemos que a classe Veiculo
é uma classe base ou super classe e que Motocicleta
é uma classe derivada ou uma classe filha da classe Veiculo
. Dizemos também que a classe Motocicleta
herda da classe Veiculo
(daí o nome herança) ou que a classe Motocicleta
estende a classe Veiculo
.
Objetos da classe Motocicleta
têm uma propriedade importante: eles possuem todas as funcionalidades de objetos da classe Veiculo
mais algumas funcionalidades extra (no nosso exemplo, informação sobre a cilindrada da motocicleta). Em outras palavras, uma motocicleta é um veículo. Este é um conceito importantíssimo em OOP porque sempre que uma função esperar receber como parâmetro um objeto do tipo Veiculo
, podemos passar um objeto do tipo Motocicleta
, dado que uma motocicleta é um veículo.
Este conceito é tão importante em programação orientada a objetos que recebeu um nome específico: Princípio da Substituição de Liskov, em homenagem a Barbara Liskov, pioneira em Ciência da Computação e ganhadora do Prêmio Turing (o “Prêmio Nobel da Computação”).
Python nos permite verificar se um objeto é uma instância de uma determinada classe por meio da função isinstance()
, cujo comportamento é ilustrado no exemplo abaixo.
v = Veiculo('carro', '9BGRD08X04G117974', 'Ferrari', 'F112', '2017')
m = Motocicleta('motocicleta', '5AZKG01Z12A339037', 'Honda', 'CG', '2015')
print(isinstance(v, Veiculo), isinstance(v, Motocicleta))
print(isinstance(m, Veiculo), isinstance(m, Motocicleta))
True False
True True
Dados de Instância versus Dados de Classe
Frequentemente, quando criamos hierarquias de classe (classes que herdam de outras classes), existem dados que são específicos de cada objeto de uma classe, mas podem existir dados que sejam comuns a todos os objetos de uma determinada classe ou até mesmo de todas as classes em uma hierarquia.
Dados específicos de cada objeto são chamados dados do nível da instância (ou instance-level data, do inglês), ao passo que dados comuns a todos os objetos de uma classe ou hierarquia são chamados dados do nível da classe (ou class-level data, do inglês).
Retomando nosso exemplo de classes para representar formas geométricas, podemos ter uma classe base chamada FormaGeometrica
, mostrada no exemplo abaixo, da qual outras formas geométricas irão herdar.
class FormaGeometrica():
area(self):
pass
Após isso, desejamos criar uma classe Circulo
, que também é uma forma geométrica, e portanto herda de FormaGeometrica
.
Entretanto, existe uma informação que é compartilhada por todos os objetos da classe Circulo
: o valor da constante Pi, usado para calcular a área e o perímetro de um círculo. Esta é uma informação do nível da classe. Esse tipo de informação é armazenado de forma diferente de informações do nível da instância, como mostrado no exemplo abaixo.
class Circulo(FormaGeometrica):
PI = 3.141592653
def __init__(self, raio):
self.raio = raio
def area(self):
return Circulo.PI * (self.raio ** 2)
Note a constante PI
é declarada no escopo da classe, ou seja, fora da função init
, onde variáveis do nível da instância são inicializadas. Note também o uso Circulo.PI
e não self.PI
para indicar o uso de uma variável do nível da classe.
Agora que adquirimos um entendimento de como herança funciona, vamos partir para outro importante conceito em OOP, o conceito de encapsulamento.