
Classes, super() e múltiplos arquivos
Programação Orientada a Objetos · 2º ano EMI
Já estudamos abstração para escolher quais características de uma pessoa ou situação precisamos representar no programa. Também estudamos encapsulamento para organizar o acesso aos dados e controlar as alterações feitas nos objetos.
Agora vamos estudar herança, que permite relacionar classes e criar tipos específicos a partir de um tipo mais geral.
Esses conceitos se complementam na organização de um programa orientado a objetos.
Herança é um mecanismo da orientação a objetos que permite definir uma classe a partir de outra. A nova classe pode usar os comportamentos definidos na classe base e acrescentar características próprias.
Usamos essa relação para representar uma especialização: um tipo mais específico continua sendo um tipo do mais geral.
Ou seja, não precisamos escrever novamente tudo o que já está definido na base para criar uma classe especializada.
Na herança, cada classe ocupa um papel em relação à outra.
A subclasse pode manter o comportamento que recebeu e especializar apenas o que precisa ser diferente.
Primeiro, declaramos qual é a classe base. Depois, podemos criar objetos da subclasse e chamar os métodos que ela oferece, inclusive os definidos na base.
Ao procurar um método, Python verifica a subclasse e, se não o encontrar ali, continua a busca na classe base. Por isso, o código do método não precisa ser copiado para o arquivo da subclasse.
Os atributos de cada objeto são criados pelas instruções de inicialização executadas para ele. Quando aproveitamos a inicialização da base, seus dados são guardados no mesmo objeto da subclasse.
Vamos aplicar a herança em um cadastro escolar. Imagine que começamos por uma classe para representar professores, com nome, idade e disciplina.
Além de guardar esses dados, queremos registrar um aniversário e produzir uma apresentação. O código ao lado mostra uma primeira versão, ainda independente de outras classes.
class Professor: def __init__(self, nome, idade, disciplina): self.nome = nome self.idade = idade self.disciplina = disciplina def fazer_aniversario(self): self.idade += 1 def apresentar(self): return f"{self.nome}, {self.idade} anos"
O mesmo sistema também precisa cadastrar alunos. Para eles, guardamos nome, idade e matrícula, além de oferecer as operações de aniversário e apresentação.
Ao comparar com Professor, vemos que parte da implementação é igual. A diferença, neste primeiro cadastro, está na matrícula do aluno e na disciplina do professor.
class Aluno: def __init__(self, nome, idade, matricula): self.nome = nome self.idade = idade self.matricula = matricula def fazer_aniversario(self): self.idade += 1 def apresentar(self): return f"{self.nome}, {self.idade} anos"
Professor e Aluno guardam nome e idade e executam os mesmos métodos de aniversário e apresentação. Se mantivermos cópias dessas operações, uma alteração na regra precisará ser feita em mais de um lugar.
Podemos organizar a parte comum em Pessoa, pois tanto um professor quanto um aluno são pessoas. Essa relação dá sentido ao uso de herança.
Vamos usar Pessoa como superclasse e criar Aluno e Professor como subclasses. Cada uma aproveitará a parte comum e manterá os dados específicos do seu cadastro.
Professor não herda de Aluno; as duas classes são especializações de Pessoa.
Vamos reunir em Pessoa os dados e comportamentos comuns aos dois cadastros. Cada objeto guarda seu nome e sua idade, que podemos consultar por uma propriedade.
Ou seja, definimos na classe as operações que fazem sentido para qualquer pessoa do cadastro.
class Pessoa: def __init__(self, nome, idade): if idade < 0: raise ValueError("A idade não pode ser negativa.") self.nome = nome self.__idade = idade @property def idade(self): return self.__idade def fazer_aniversario(self): self.__idade += 1 def apresentar(self): return f"{self.nome}, {self.idade} anos"
Vamos guardar a classe em um arquivo e usá-la em outro. Um arquivo Python também é um módulo, do qual podemos importar definições.
Separar arquivos organiza o programa; a relação de herança será declarada entre as classes.
cadastro_escolar/
pessoa.py
main.pyfrom pessoa import Pessoa pessoa = Pessoa("Ana", 16) print(pessoa.apresentar()) pessoa.fazer_aniversario() print(pessoa.idade)
Em from pessoa import Pessoa, o primeiro nome identifica o módulo, e o segundo, a classe que queremos usar. Não escrevemos a extensão .py na importação.
Importar a classe não cria uma pessoa. O objeto é criado quando chamamos Pessoa("Ana", 16), informando os dados esperados pelo inicializador.
No terminal aberto na pasta cadastro_escolar, executar python main.py.
Indicamos a classe base entre os parênteses da declaração. Primeiro, vamos criar Aluno sem acrescentar dados ou métodos próprios.
pass mantém o corpo da classe válido quando ainda não escrevemos instruções nele. A relação de herança é declarada por (Pessoa).
Arquivo novo: aluno.py, na mesma pasta.
Já podemos criar alunos que usam os comportamentos definidos em Pessoa.
from pessoa import Pessoa class Aluno(Pessoa): pass
A classe Aluno ainda não declara __init__, apresentar() ou fazer_aniversario(). Podemos usá-los porque estão definidos em Pessoa.
Como ainda não definimos um inicializador na subclasse, usamos o herdado. Ele recebe nome e idade e cria esses dados no próprio objeto aluno.
Arquivo: substituir o conteúdo de main.py pelo código ao lado.
from aluno import Aluno aluno = Aluno("Ana", 16) print(aluno.apresentar()) aluno.fazer_aniversario() print(aluno.idade)
Desafio: criar outro aluno e verificar se sua idade depende das alterações feitas em Ana.
Cada chamada à classe cria um objeto com seus próprios dados. Ao executar ana.fazer_aniversario(), o parâmetro self representa Ana; por isso, o método não altera a idade de Bruno.
Mantemos as classes como estavam e modificamos apenas main.py.
A definição do método é reaproveitada, enquanto os dados continuam pertencendo a cada objeto.
from aluno import Aluno ana = Aluno("Ana", 16) bruno = Aluno("Bruno", 17) ana.fazer_aniversario() print(ana.nome, ana.idade) print(bruno.nome, bruno.idade)
Um aluno também pode ser reconhecido como uma pessoa. Usamos isinstance(objeto, Classe) para verificar essa relação.
Um objeto criado diretamente como Pessoa, porém, não passa automaticamente a ser um aluno.
Arquivo: main.py, ainda com a primeira versão de Aluno.
from pessoa import Pessoa from aluno import Aluno aluno = Aluno("Ana", 16) pessoa = Pessoa("Carlos", 40) print(isinstance(aluno, Aluno)) print(isinstance(aluno, Pessoa)) print(isinstance(pessoa, Aluno))
Usamos herança quando os tipos específicos preservam o sentido e as operações do tipo geral.
Antes de criar a subclasse, precisamos explicar por que ela é um tipo da classe base.
Agora precisamos distinguir o cadastro do aluno pela matrícula. Nome e idade continuam sendo dados de pessoa; a matrícula pertence à especialização Aluno.
Para receber essa informação, vamos escrever um __init__ na subclasse. Ao fazer isso, precisamos executar também a inicialização que já está definida em Pessoa.
Definir um novo __init__ não chama automaticamente o inicializador da base.
Neste exemplo de herança simples, super().__init__(nome, idade) executa a inicialização de Pessoa. Depois, acrescentamos a matrícula no mesmo objeto.
Ou seja, aproveitamos a inicialização comum e completamos os dados específicos.
Arquivo: substituir aluno.py por esta versão.
from pessoa import Pessoa class Aluno(Pessoa): def __init__(self, nome, idade, matricula): super().__init__(nome, idade) self.matricula = matricula
def __init__(self, nome, idade): if idade < 0: raise ValueError("A idade não pode ser negativa.") self.nome = nome self.__idade = idade
def __init__(self, nome, idade, matricula): super().__init__(nome, idade) self.matricula = matricula
from aluno import Aluno aluno = Aluno("Ana", 16, "2026001") print(aluno.nome) print(aluno.idade) print(aluno.matricula)
Ao criar o aluno, agora informamos nome, idade e matrícula. Vamos acompanhar como esses argumentos chegam às duas partes da inicialização.
def __init__(self, nome, idade):◀ entra aqui if idade < 0: raise ValueError("A idade não pode ser negativa.") self.nome = nome self.__idade = idade
def __init__(self, nome, idade, matricula): super().__init__(nome, idade)▶ chama a base self.matricula = matricula
from aluno import Aluno aluno = Aluno("Ana", 16, "2026001") print(aluno.nome) print(aluno.idade) print(aluno.matricula)
Entramos no inicializador de Aluno. A chamada a super().__init__(nome, idade) encaminha esses dois dados para Pessoa, usando o mesmo objeto.
def __init__(self, nome, idade): if idade < 0: raise ValueError("A idade não pode ser negativa.") self.nome = nome self.__idade = idade↩ volta para Aluno
def __init__(self, nome, idade, matricula): super().__init__(nome, idade) self.matricula = matricula◀ retorna aqui
from aluno import Aluno aluno = Aluno("Ana", 16, "2026001") print(aluno.nome) print(aluno.idade) print(aluno.matricula)
O inicializador de Pessoa verifica a idade e guarda nome e idade no objeto. Ao terminar, a execução retorna à chamada feita em Aluno.
def __init__(self, nome, idade): if idade < 0: raise ValueError("A idade não pode ser negativa.") self.nome = nome self.__idade = idade
def __init__(self, nome, idade, matricula): super().__init__(nome, idade) self.matricula = matricula
from aluno import Aluno aluno = Aluno("Ana", 16, "2026001") print(aluno.nome) print(aluno.idade) print(aluno.matricula)
De volta ao inicializador de Aluno, guardamos a matrícula. Depois que ele termina, continuamos a execução em main.py. O mesmo objeto reúne os dados inicializados por Pessoa e a matrícula acrescentada por Aluno.
A apresentação herdada informa nome e idade, mas o cadastro do aluno precisa mostrar também a matrícula. Podemos declarar na subclasse um método com o mesmo nome para definir sua versão desse comportamento.
Chamamos isso de sobrescrita. Vamos aproveitar o texto produzido pela base e acrescentar a matrícula.
Arquivo: versão completa atualizada de aluno.py.
from pessoa import Pessoa class Aluno(Pessoa): def __init__(self, nome, idade, matricula): super().__init__(nome, idade) self.matricula = matricula def apresentar(self): dados_pessoa = super().apresentar() return f"{dados_pessoa} | Matrícula: {self.matricula}"
from pessoa import Pessoa from aluno import Aluno pessoa = Pessoa("Carlos", 40) aluno = Aluno("Ana", 16, "2026001") print(pessoa.apresentar()) print(aluno.apresentar())
As duas chamadas usam apresentar(), mas o método escolhido depende da classe do objeto. Para a pessoa, executamos a versão da base; para o aluno, a versão sobrescrita.
Dentro da versão de Aluno, usamos super().apresentar() para obter nome e idade antes de acrescentar a matrícula.
Desafio: criar um segundo aluno com outra matrícula e comparar as apresentações.
A versão de Aluno que sobrescreve apresentar() inicializa a matrícula e especializa a apresentação. O método de aniversário continua vindo de Pessoa.
Quando apresentamos o aluno depois do aniversário, a chamada à apresentação da base consulta a idade atualizada.
Arquivo: main.py. Abrir também aluno.py durante a discussão.
from aluno import Aluno aluno = Aluno("Bruno", 17, "2026002") print(aluno.apresentar()) aluno.fazer_aniversario() print(aluno.apresentar())
O aluno usa a propriedade idade e o método fazer_aniversario() definidos em Pessoa. Assim, consultamos e alteramos a idade pela interface que a base oferece.
from aluno import Aluno aluno = Aluno("Ana", 16, "2026001") aluno.fazer_aniversario() print(aluno.idade) try: aluno.idade = -5 except AttributeError: print("A idade não permite atribuição direta.")
Escrever self.__idade dentro de Aluno não acessa automaticamente o atributo definido com esse nome em Pessoa.
Por isso, usamos a propriedade e os métodos disponíveis.
Quando o programa falhar, vamos conferir o ponto em que a execução parou e a mensagem apresentada.
A mensagem orienta a investigação; esconder todos os erros com um except genérico dificulta encontrar a causa.
Antes de criar uma subclasse, precisamos conferir o significado da relação. Ter dados em comum, por si só, não prova que uma classe é um tipo da outra.
Organizar um objeto com outros objetos é uma forma de composição. Uma turma não precisa herdar de pessoa para usar alunos e professores.
Vamos verificar os dados específicos do professor e os comportamentos que ele compartilha com outras pessoas.
Desafio: reunir uma pessoa, um aluno e um professor em uma lista e exibir suas apresentações.
Por que Professor herda de Pessoa, e não de Aluno?
Validamos a disciplina antes de continuar a inicialização. strip() devolve uma cópia do texto sem espaços nas extremidades; quando não sobra conteúdo, recusamos o cadastro.
Para os valores válidos, usamos Pessoa para inicializar nome e idade e guardamos a disciplina em Professor. A apresentação acrescenta esse dado ao texto comum.
Arquivo: professor.py.
from pessoa import Pessoa class Professor(Pessoa): def __init__(self, nome, idade, disciplina): if not disciplina.strip(): raise ValueError("Informe a disciplina.") super().__init__(nome, idade) self.disciplina = disciplina def apresentar(self): dados_pessoa = super().apresentar() return f"{dados_pessoa} | Disciplina: {self.disciplina}"
Vamos testar uma professora válida e duas disciplinas inválidas. A classe detecta o problema; o programa principal decide como apresentar a mensagem.
Também precisamos verificar a regra da base: ao tentar criar uma professora com idade negativa e disciplina válida, a criação deve lançar ValueError.
Arquivo: main.py.
from professor import Professor professor = Professor("Carla", 35, "Programação") print(professor.apresentar()) professor.fazer_aniversario() print(professor.idade) for disciplina in ("", " "): try: Professor("Carla", 35, disciplina) except ValueError as erro: print(erro)
Podemos reunir pessoas, alunos e professores em uma lista e pedir a apresentação de cada objeto. O laço mantém a mesma chamada, enquanto cada objeto responde usando o método correspondente à sua classe.
Essa possibilidade de usar uma operação comum com comportamentos específicos é uma forma de polimorfismo.
Na hierarquia que construímos, a herança permite reaproveitar os dados e comportamentos comuns e especializar as apresentações.
from pessoa import Pessoa from aluno import Aluno from professor import Professor pessoas = [ Pessoa("Carlos", 40), Aluno("Ana", 16, "2026001"), Professor("Carla", 35, "Programação"), ] for pessoa in pessoas: print(pessoa.apresentar())
Nosso cadastro tem uma classe geral, duas especializações e um arquivo que usa os objetos.
cadastro_escolar/
pessoa.py
aluno.py
professor.py
main.pyA seta de herança aponta para a base. Aluno e Professor compartilham Pessoa; main.py usa essas classes, mas não participa da hierarquia.


Quer trocar mais uma ideia? Me chama!