Herança em Python

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:
    __init__(self, tipo, chassi, marca, modelo, ano):
        self.tipo = tipo
        self.chassi = chassi
        self.marca = marca
        self.modelo = modelo
        self.ano = ano

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): (1)
    __init__(self, tipo, chassi, marca, modelo, ano, cilindrada):
        super().__init__(tipo, chassi, marca, modelo, ano) (2)
        self.cilindrada = cilindrada
1 Indica que a classe Motocicleta herda da classe Veiculo.
2 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():
    desenha(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.14159 (1)

    __init__(self, r):
        self.raio = r

    def desenha(self):
        pass

    def area(self):
        return Circulo.PI * (self.raio ** 2) (2)
1 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.
2 Note 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.