A programação orientada a objetos (POO) é uma das abordagens mais eficazes para escrever software. Na programação orientada a objetos, você escreve classes que representam coisas e situações do mundo real, e você cria objetos com base nessas classes. Quando você escreve uma classe, você define o comportamento geral que uma categoria inteira de objetos pode ter.
Quando você cria objetos individuais da classe, cada objeto é automaticamente equipado com o comportamento geral; você pode então dar a cada objeto quaisquer características únicas que desejar. Você ficará surpreso com o quão bem situações do mundo real podem ser modeladas com programação orientada a objetos.
Criar um objeto a partir de uma classe é chamado de instanciação, e você trabalha com instâncias de uma classe. Neste capítulo, você escreverá classes e criará instâncias dessas classes. Você especificará o tipo de informação que pode ser armazenada em instâncias e definirá ações que podem ser tomadas com essas instâncias. Você também escreverá classes que estendem a funcionalidade de classes existentes, para que classes semelhantes possam compartilhar funcionalidades comuns, e você possa fazer mais com menos código. Você armazenará suas classes em módulos e importará classes escritas por outros programadores em seus próprios arquivos de programa.
Aprender sobre programação orientada a objetos ajudará você a ver o mundo como um programador vê. Ajudará você a entender seu código — não apenas o que está acontecendo linha por linha, mas também os conceitos maiores por trás dele. Conhecer a lógica por trás das classes o treinará para pensar logicamente, para que você possa escrever programas que efetivamente abordem quase qualquer problema que você encontrar.
As aulas também facilitam a vida para você e os outros programadores com quem você trabalhará, à medida que você enfrenta desafios cada vez mais complexos. Quando você e outros programadores escrevem código com base no mesmo tipo de lógica, vocês serão capazes de entender o trabalho um do outro. Seus programas farão sentido para as pessoas com quem você trabalha, permitindo que todos realizem mais.
Criando e usando uma classe
Você pode modelar quase tudo usando classes. Vamos começar escrevendo uma classe simples, Cachorro, que representa um cachorro — não um cachorro em particular, mas qualquer cachorro. O que sabemos sobre a maioria dos cachorros de estimação? Bem, todos eles têm um nome e uma idade. Também sabemos que a maioria dos cachorros senta e rola. Essas duas informações (nome e idade) e esses dois comportamentos (sentar e rolar) irão para nossa classe Cachorro porque são comuns à maioria dos cachorros. Essa classe dirá ao Python como fazer um objeto representando um cachorro. Depois que nossa classe for escrita, nós a usaremos para fazer instâncias individuais, cada uma representando um cachorro específico.
Criando a classe Cachorro
Cada instância criada da classe Cachorro armazenará um nome e uma idade, e daremos a cada cão a capacidade de sentar() e rolar():
class Cachorro: # 1
"""Um modelo simples de um cachorro."""
def __init__(self, nome, idade): # 2
"""Inicializa os atributos nome e idade."""
self.nome = nome # 3
self.idade = idade
def sentar(self): # 4
"""Simula o cachorro a sentar após o comando."""
print(f'{self.nome} está sentado')
def rolar(self):
"""Simula o cachorro rolar após o comando."""
print(f'{self.nome} rolou')
Há muito o que notar aqui, mas não se preocupe. Você verá essa estrutura ao longo deste capítulo e terá muito tempo para se acostumar com ela. Primeiro definimos uma classe chamada Cachorro (1). Por convenção, nomes em maiúsculas se referem a classes em Python. Não há parênteses na definição da classe porque estamos criando essa classe do zero. Então escrevemos uma docstring descrevendo o que essa classe faz.
O método __init__()
Uma função que faz parte de uma classe é um método. Tudo o que você aprendeu sobre funções se aplica a métodos também; a única diferença prática por enquanto é a maneira como chamaremos os métodos. O método __init__() (2) é um método especial que o Python executa automaticamente sempre que criamos uma nova instância com base na classe Cachorro. Este método tem dois sublinhados iniciais e dois sublinhados finais, uma convenção que ajuda a evitar que os nomes de métodos padrão do Python entrem em conflito com os nomes dos seus métodos. Certifique-se de usar dois sublinhados em cada lado de __init__(). Se você usar apenas um em cada lado, o método não será chamado automaticamente quando você usar sua classe, o que pode resultar em erros difíceis de identificar.
Definimos o método __init__() para ter três parâmetros: self, nome e idade. O parâmetro self é necessário na definição do método e deve vir primeiro, antes dos outros parâmetros. Ele deve ser incluído na definição, porque quando o Python chama esse método posteriormente (para criar uma instância de Cachorro), a chamada do método passará automaticamente no argumento próprio. Toda chamada de método associada a uma instância passa automaticamente a si mesmo, que é uma referência à própria instância; Ele fornece acesso à instância individual aos atributos e métodos da classe. Quando fazemos uma instância de Cachorro, Python chama o método __init__() da classe Cachorro. Passaremos a Cachorro() um nome e uma idade como argumentos; o self é passado automaticamente, por isso não precisamos passar. Sempre que queremos fazer uma instância da classe Cachorro, forneceremos valores para apenas os dois últimos parâmetros, nome e idade.
As duas variáveis definidas no corpo do método __init__() têm o prefixo self (3). Qualquer variável prefixada com self está disponível para todos os métodos da classe e também poderemos acessar essas variáveis por meio de qualquer instância criada a partir de a classe. A linha self.nome = nome leva o valor associado ao nome do parâmetro e o atribui ao nome da variável, que é então anexado à instância que está sendo criada. O mesmo processo acontece com self.idade = idade. Variáveis acessíveis por meio de instâncias como essa são chamadas de atributos.
A classe Cachorro possui outros dois métodos definidos: sentar() e rolar() (4). Como esses métodos não precisam de informações adicionais para executar, apenas os definimos para ter um parâmetro, self. As instâncias que criamos mais tarde terão acesso a esses métodos. Em outras palavras, eles poderão sentar e rolar. Por enquanto, sentar() e rolar() não fazem muito. Eles simplesmente imprimem uma mensagem dizendo que o cachorro está sentado ou rolando. Mas o conceito pode ser estendido a situações realistas: se essa classe fizesse parte de um jogo de computador, esses métodos conteriam código para fazer um cachorro animado sentar e rolar. Se essa classe fosse escrita para controlar um robô, esses métodos direcionariam os movimentos que faria com que um cão robótico se sente e role.
Fazendo uma instância de uma classe
Pense em uma classe como um conjunto de instruções sobre como fazer uma instância. A classe Cachorro é um conjunto de instruções que dizem a Python como fazer instâncias individuais representando cães específicos. Vamos fazer uma instância representando um cão específico:
meu_cachorro = Cachorro('Rex', 5) # 1
print(f'Meu cachorro se chama {meu_cachorro.nome}') # 2
print(f'Ele tem {meu_cachorro.idade} anos') # 3
A classe de Cachorro que estamos usando aqui é a que acabamos de escrever no exemplo anterior. Aqui, dizemos a Python para criar um cachorro cujo nome é Rex e cuja idade é 5 (1). O método __init__() cria uma instância que representa esse cão em particular e define os atributos de nome e idade usando os valores que fornecemos. Python então retorna uma instância que representa esse cão. Atribuímos essa instância à variável meu_cachorro. A convenção de nomenclatura é útil aqui; normalmente, podemos assumir que um nome capitalizado como Cachorro se refere a uma classe e um nome em minúsculas como meu_cachorro se refere a uma única instância criada a partir de uma classe.
Acessando atributos
Para acessar os atributos de uma instância, você usa a notação de pontos. Acessamos o valor do nome do atributo do meu_cachorro (2) escrevendo:
meu_cachorro.nome
A notação de pontos é usada frequentemente em Python. Essa sintaxe demonstra como o Python encontra o valor de um atributo. Aqui, Python analisa a instância meu_cachorro e encontra o atributo nome associado ao meu_cachorro. Este é o mesmo atributo referido como self.nome na classe Cachorro. Usamos a mesma abordagem para trabalhar com o atributo idade (3). A saída é um resumo do que sabemos sobre o meu_cachorro.
Métodos de chamada
Depois de criarmos uma instância da classe Cachorro, podemos usar a notação de ponto para chamar qualquer método definido em Cachorro. Vamos fazer nosso cachorro sentar e rolar:
meu_cachorro = Cachorro('Rex', 5)
meu_cachorro.sentar()
meu_cachorro.rolar()
Para chamar um método, dê o nome da instância (neste caso, meu_cachorro) e o método que você deseja chamar, separado por um ponto. Quando o Python lê meu_cachorro.sentar(), ele procura o método sentar() na classe Cachorro e executa esse código. O Python interpreta a linha meu_cachorro.rolar()da mesma maneira. Agora Rex faz o que dizemos a ele.
Esta sintaxe é bastante útil. Quando atributos e métodos receberam nomes apropriadamente descritivos, como nome, idade, sentar() e rolar(), podemos inferir facilmente o bloco de código, mesmo que nunca vimos antes.
Criando várias instâncias
Você pode criar quantas instâncias a partir de uma classe necessário. Vamos criar um segundo cachorro chamado cachorro_vizinho:
meu_cachorro = Cachorro('Rex', 5)
cachorro_vizinho = Cachorro('Pluto', 11)
print(f'Meu cachorro se chama {meu_cachorro.nome}')
print(f'Ele tem {meu_cachorro.idade} anos')
meu_cachorro.sentar()
print(f'\nO cachorro do meu vizinho se chama {cachorro_vizinho.nome}')
print(f'Ele tem {cachorro_vizinho.idade} anos')
cachorro_vizinho.sentar()
Mesmo se usássemos o mesmo nome e idade para o segundo cachorro, o Python ainda criaria uma instância separada da classe de Cachorro. Você pode fazer quantas instâncias de uma classe precisar, desde que você dê a cada instância um nome de variável exclusivo ou ocupe um local único em uma lista ou dicionário.
Tente você mesmo!
Exercício 01: Faça uma aula chamada Restaurante. O método __init__() para restaurante deve armazenar dois atributos: um restaurante_nome e uma tipo_comida. Faça um método chamado descrever_restaurante() que imprime essas duas informações e um método chamado restaurante_aberto() que imprime uma mensagem indicando que o restaurante está aberto. Faça uma instância que chame a classe Restaurante. Imprima os dois atributos individualmente e chame os dois métodos.
Exercício 02: Comece com sua aula do exercício 1. Crie três instâncias diferentes da classe e chame descrever_restaurante() para cada instância.
Exercício 03: Faça uma classe chamada Usuario. Crie dois atributos chamados nome e sobrenome e, em seguida, crie vários outros atributos que normalmente são armazenados em um perfil de usuário. Faça um método chamado descrever_usuario() que imprime um resumo das informações do usuário. Faça outro método chamado saudar_usuario() que imprime uma saudação personalizada ao usuário. Crie várias instâncias representando usuários diferentes e chame os dois métodos para cada usuário.
Trabalhando com classes e instâncias
Você pode usar classes para representar muitas situações do mundo real. Depois de escrever uma classe, você passará a maior parte do seu tempo trabalhando com instâncias criadas a partir dessa classe. Uma das primeiras tarefas que você desejará fazer é modificar os atributos associados a uma instância específica. Você pode modificar os atributos de uma instância diretamente ou escrever métodos que atualizem atributos de maneiras específicas.
A Classe Carro
Vamos escrever uma nova classe representando um carro. Nossa classe armazenará informações sobre o tipo de carro com o qual estamos trabalhando, e terá um método que resume essas informações:
class Carro:
"""Classe que descreve um carro."""
def __init__(self, marca, modelo, ano): # 1
"""Função que inicializa atributos da classe."""
self.marca = marca
self.modelo = modelo
self.ano = ano
def descricao(self): # 2
"""Função que retorna a descrição completa do carro."""
descricao_completa = f'{self.ano} {self.marca} {self.modelo}'
return descricao_completa.title()
meu_carro_novo = Carro('ford', 'fiesta', 2018) # 3
print(meu_carro_novo.descricao())
Na classe Carro, definimos o método __init__() com o parâmetro self primeiro (1), assim como fizemos com a classe Cachorro. Também damos a ele três outros parâmetros: marca, modelo e ano. O método __init__() recebe esses parâmetros e os atribui aos atributos que serão associados às instâncias feitas a partir desta classe. Quando criamos uma nova instância Carro, precisamos especificar uma marca, modelo e ano para nossa instância.
Definimos um método chamado descricao() (2) que coloca o ano, a marca e o modelo de um carro em uma string descrevendo o carro de forma organizada. Isso nos poupará de ter que imprimir o valor de cada atributo individualmente. Para trabalhar com os valores de atributo neste método, usamos self.marca, self.modelo e self.ano. Fora da classe, criamos uma instância da classe Carro e a atribuímos à variável meu_carro_novo (3). Então chamamos descricao() para mostrar que tipo de carro temos.
Para tornar a classe mais interessante, vamos adicionar um atributo que muda ao longo do tempo. Adicionaremos um atributo que armazena a quilometragem geral do carro.
Definindo um valor padrão para um atributo
Quando uma instância é criada, atributos podem ser definidos sem serem passados como parâmetros. Esses atributos podem ser definidos no método __init__(), onde recebem um valor padrão.
Vamos adicionar um atributo chamado odometro que sempre começa com um valor de 0. Também adicionaremos um método ler_odometro() que nos ajuda a ler o odômetro de cada carro:
class Carro:
"""Classe que descreve um carro."""
def __init__(self, marca, modelo, ano):
"""Função que inicializa atributos da classe."""
self.marca = marca
self.modelo = modelo
self.ano = ano
self.odometro = 0 # 1
def descricao(self):
"""Função que retorna a descrição completa do carro."""
descricao_completa = f'{self.ano} {self.marca} {self.modelo}'
return descricao_completa
def ler_odometro(self): # 2
"""Função que retorna o quanto o carro andou."""
print(f'Esse carro tem {self.odometro} Km rodados')
meu_carro_novo = Carro('ford', 'fiesta', 2018)
print(meu_carro_novo.descricao())
meu_carro_novo.ler_odometro()
Desta vez, quando Python chama o método __init__() para criar uma nova instância, ele armazena os valores de marca, modelo e ano como atributos, como fez no exemplo anterior. Então Python cria um novo atributo chamado odometro e define seu valor inicial para 0 (1). Também temos um novo método chamado ler_odometro() (2) que facilita a leitura da quilometragem de um carro. Nosso carro começa com uma quilometragem de 0.
Poucos carros são vendidos com exatamente 0 quilômetros no odômetro, então precisamos de uma maneira de alterar o valor desse atributo.
Modificando valores de atributos
Você pode alterar o valor de um atributo de três maneiras: você pode alterar o valor diretamente por meio de uma instância, definir o valor por meio de um método ou incrementar o valor (adicionar uma certa quantia a ele) por meio de um método. Vamos dar uma olhada em cada uma dessas abordagens.
Modificando o valor de um atributo diretamente
A maneira mais simples de modificar o valor de um atributo é acessar o atributo diretamente por meio de uma instância. Aqui, definimos a leitura do odômetro para 25 diretamente:
meu_carro_novo.odometro = 25
meu_carro_novo.ler_odometro()
Usamos a notação de ponto para acessar o atributo odometro do carro e definir seu valor diretamente. Esta linha diz ao Python para pegar a instância meu_carro_novo, encontrar o atributo odometro associado a ela e definir o valor desse atributo como 25. Às vezes, você desejará acessar atributos diretamente assim, mas outras vezes desejará escrever um método que atualize o valor para você.
Modificando o valor de um atributo por meio de um método
Pode ser útil ter métodos que atualizem certos atributos para você. Em vez de acessar o atributo diretamente, você passa o novo valor para um método que lida com a atualização internamente. Aqui está um exemplo mostrando um método chamado atualizar_odometro():
class Carro:
"""Classe que descreve um carro."""
def __init__(self, marca, modelo, ano):
"""Função que inicializa atributos da classe."""
self.marca = marca
self.modelo = modelo
self.ano = ano
self.odometro = 0
def descricao(self):
"""Função que retorna a descrição completa do carro."""
descricao_completa = f'{self.ano} {self.marca} {self.modelo}'
return descricao_completa
def ler_odometro(self):
"""Função que retorna o quanto o carro andou."""
print(f'Esse carro tem {self.odometro} Km rodados')
def atualizar_odometro(self, quilometragem):
"""Atualizar o odômetro para um valor fornecido."""
self.odometro = quilometragem
meu_carro_novo = Carro('Ford', 'Fiesta', 2018)
meu_carro_novo.atualizar_odometro(25)
meu_carro_novo.ler_odometro() # 1
A única modificação em Carro é a adição de atualizar_odometro(). Este método recebe um valor de quilometragem e o atribui a self.odometro. Usando a instância meu_carro_novo, chamamos atualizar_odometro() com 25 como argumento (1). Isso define a leitura do odômetro para 25, e ler_odometro() imprime a leitura.
Podemos estender o método atualizar_odometro() para fazer trabalho adicional toda vez que a leitura do odômetro for modificada. Vamos adicionar um pouco de lógica para garantir que ninguém tente reverter a leitura do odômetro:
def atualizar_odometro(self, quilometragem):
"""Atualizar o odômetro para um valor fornecido.
Se o valor fornecido for menor do que o do odometro, impedir a mudança."""
if quilometragem >= self.odometro: # 1
self.odometro = quilometragem
else:
print('Você não pode diminuir o valor do odômetro!') # 2
Agora atualizar_odometro() verifica se a nova leitura faz sentido antes de modificar o atributo. Se o valor fornecido para quilometragem for maior ou igual à quilometragem existente, self.odometro, você pode atualizar a leitura do odômetro para a nova quilometragem (1). Se a nova quilometragem for menor que a quilometragem existente, você receberá um aviso de que não pode reverter um odômetro (2).
Incrementando o valor de um atributo por meio de um método
Às vezes, você vai querer incrementar o valor de um atributo em uma certa quantia, em vez de definir um valor totalmente novo. Digamos que compramos um carro usado e rodamos 100 quilômetros nele entre o momento em que o compramos e o momento em que o registramos. Aqui está um método que nos permite passar essa quantia incremental e adicionar esse valor à leitura do odômetro:
class Carro:
"""Classe que descreve um carro."""
def __init__(self, marca, modelo, ano):
"""Função que inicializa atributos da classe."""
self.marca = marca
self.modelo = modelo
self.ano = ano
self.odometro = 0
def descricao(self):
"""Função que retorna a descrição completa do carro."""
descricao_completa = f'{self.ano} {self.marca} {self.modelo}'
return descricao_completa
def ler_odometro(self):
"""Função que retorna o quanto o carro andou."""
print(f'Esse carro tem {self.odometro} Km rodados')
def atualizar_odometro(self, quilometragem):
"""Atualizar o odômetro para um valor fornecido.
Se o valor fornecido for menor do que o do odometro, impedir a mudança."""
if quilometragem >= self.odometro:
self.odometro = quilometragem
else:
print('Você não pode diminuir o valor do odômetro!')
def aumentar_odometro(self, quilometragem):
"""Função que aumenta a quilometragem conforme o carro anda."""
self.odometro += quilometragem
meu_carro_velho = Carro('VW', 'Fusca', 1994) # 1
print(meu_carro_velho.descricao())
meu_carro_velho.atualizar_odometro(35000) # 2
meu_carro_velho.ler_odometro()
meu_carro_velho.aumentar_odometro(100)
meu_carro_velho.ler_odometro()
O novo método aumentar_odometro() recebe um número de quilômetros e adiciona esse valor a self.odometro. Primeiro, criamos um carro usado, meu_carro_velho (1). Definimos seu odômetro para 35.000 chamando atualizar_odometro() (2). Finalmente, chamamos aumentar_odometro() e passamos 100 para adicionar os 100 quilômetros que dirigimos entre a compra do carro e seu registro.
Você pode modificar esse método para rejeitar incrementos negativos para que ninguém use essa função para reverter um odômetro também.
Você pode usar métodos como este para controlar como os usuários do seu programa atualizam valores como uma leitura do odômetro, mas qualquer um com acesso ao programa pode definir a leitura do odômetro para qualquer valor acessando o atributo diretamente. A segurança efetiva exige extrema atenção aos detalhes, além de verificações básicas como as mostradas aqui.
Tente você mesmo!
Exercício 04: Comece com seu programa do exercício 1. Adicione um atributo chamado clientes_atendidos com um valor padrão de 0. Crie uma instância chamada restaurante dessa classe. Imprima o número de clientes que o restaurante atendeu e, em seguida, altere esse valor e imprima-o novamente.
Adicione um método chamado atualizar_clientes_atendidos() que permite que você defina o número de clientes que foram atendidos. Chame esse método com um novo número e imprima o valor novamente.
Adicione um método chamado aumentar_clientes_atendidos() que permite incrementar o número de clientes que foram atendidos. Chame esse método com qualquer número que você quiser que possa representar quantos clientes foram atendidos em, digamos, um dia útil.
Exercício 05: Adicione um atributo chamado tentativas_login à sua classe Usuario do exercício 3. Escreva um método chamado aumentar_tentativas_login() que incrementa o valor de tentativas_login em 1. Escreva outro método chamado reset_tentativas_login() que redefine o valor de tentativas_login para 0.
Crie uma instância da classe Usuario e chame aumentar_tentativas_login() várias vezes. Imprima o valor de tentativas_login para garantir que ele foi incrementado corretamente e, em seguida, chame reset_tentativas_login(). Imprima tentativas_login novamente para garantir que ele foi redefinido para 0.
Herança
Você nem sempre precisa começar do zero ao escrever uma classe. Se a classe que você está escrevendo for uma versão especializada de outra classe que você escreveu, você pode usar herança. Quando uma classe herda de outra, ela assume os atributos e métodos da primeira classe. A classe original é chamada de classe pai, e a nova classe é a classe filha. A classe filha pode herdar qualquer um ou todos os atributos e métodos de sua classe pai, mas também é livre para definir novos atributos e métodos próprios.
O método __init__() para uma classe filha
Ao escrever uma nova classe com base em uma classe existente, você frequentemente desejará chamar o método __init__() da classe pai. Isso inicializará quaisquer atributos que foram definidos no método pai __init__() e os tornará disponíveis na classe filha.
Como exemplo, vamos modelar um carro elétrico. Um carro elétrico é apenas um tipo específico de carro, então podemos basear nossa nova classe CarroEletrico na classe Carro que escrevemos anteriormente. Então, só teremos que escrever código para os atributos e comportamentos específicos de carros elétricos.
Vamos começar criando uma versão simples da classe CarroEletrico, que faz tudo o que a classe Carro faz:
class CarroEletrico(Carro): # 1
"""Classe que representa o carro elétrico."""
def __init__(self, marca, modelo, ano): # 2
"""Inicializa a classe pai/mãe."""
super().__init__(marca, modelo, ano) # 3
carro_eletrico = CarroEletrico('BYD', 'dolphin', 2024) # 4
print(carro_eletrico.descricao())
Começamos coma classe Carro já criada. Quando você cria uma classe filha, a classe pai deve fazer parte do arquivo atual e deve aparecer antes da classe filha no arquivo. Então definimos a classe filha, CarroEletrico (1). O nome da classe pai deve ser incluído entre parênteses na definição de uma classe filha. O método __init__() recebe as informações necessárias para criar uma instância de Carro (2).
A função super() (3) é uma função especial que permite que você chame um método da classe pai. Esta linha diz ao Python para chamar o método __init__() de Carro, que fornece a instância CarroEletrico todos os atributos definidos naquele método. O nome super vem de uma convenção de chamar a classe pai de superclasse e a classe filha de subclasse.
Testamos se a herança está funcionando corretamente tentando criar um carro elétrico com o mesmo tipo de informação que forneceríamos ao fazer um carro comum. Criamos uma instância da classe CarroEletrico e a atribuímos a carro_eletrico (4). Esta linha chama o método __init__() definido em CarroEletrico, que por sua vez diz ao Python para chamar o método __init__() definido na classe pai Carro. Fornecemos os argumentos 'BYD', 'dolphin' e 2024.
Além de __init__(), ainda não há atributos ou métodos que sejam particulares a um carro elétrico. Neste ponto, estamos apenas nos certificando de que o carro elétrico tenha os comportamentos de Carro apropriados.
A instância CarroEletrico funciona como uma instância de Carro, então agora podemos começar a definir atributos e métodos específicos para carros elétricos.
Definindo atributos e métodos para a classe filha
Depois de ter uma classe filha que herda de uma classe pai, você pode adicionar quaisquer novos atributos e métodos necessários para diferenciar a classe filha da classe pai.
Vamos adicionar um atributo que seja específico para carros elétricos (uma bateria, por exemplo) e um método para relatar esse atributo. Armazenaremos o tamanho da bateria e escreveremos um método que imprima uma descrição da bateria:
class CarroEletrico(Carro):
"""Classe que representa o carro elétrico."""
def __init__(self, marca, modelo, ano):
"""Inicializa a classe pai/mãe."""
super().__init__(marca, modelo, ano)
self.bateria = 40 # 1
def descrever_bateria(self): # 2
"""Função que descreve a quantidade de carga máxima da bateria."""
print(f'Esse carro possui uma bateria de {self.bateria} kWh')
carro_eletrico = CarroEletrico('BYD', 'dolphin', 2024)
print(carro_eletrico.descricao())
carro_eletrico.descrever_bateria()
Adicionamos um novo atributo self.bateria e definimos seu valor inicial como 40 (1). Este atributo será associado a todas as instâncias criadas da classe CarroEletrico, mas não será associado a nenhuma instância de Carro. Também adicionamos um método chamado descrever_bateria() que imprime informações sobre a bateria (2). Quando chamamos este método, obtemos uma descrição que é claramente específica para um carro elétrico.
Não há limite para o quanto você pode especializar a classe CarroEletrico. Você pode adicionar quantos atributos e métodos precisar para modelar um carro elétrico com qualquer grau de precisão que precisar. Um atributo ou método que poderia pertencer a qualquer carro, em vez de um que seja específico para um carro elétrico, deve ser adicionado à classe Carro em vez da classe CarroEletrico. Então, qualquer pessoa que usar a classe Carro terá essa funcionalidade disponível também, e a classe CarroEletrico conterá apenas código para as informações e comportamento específicos para veículos elétricos.
Substituindo métodos da classe pai
Você pode sobrescrever qualquer método da classe pai que não se encaixe no que você está tentando modelar com a classe filha. Para fazer isso, você define um método na classe filha com o mesmo nome do método que você quer sobrescrever na classe pai. O Python desconsiderará o método da classe pai e prestará atenção somente ao método que você definir na classe filha.
Digamos que a classe Carro tinha um método chamado tanque_combustivel(). Esse método não tem sentido para um veículo totalmente elétrico, então você pode querer sobrescrever esse método. Aqui está uma maneira de fazer isso:
class Carro:
"""Classe que descreve um carro."""
def __init__(self, marca, modelo, ano):
"""Função que inicializa atributos da classe."""
...
def tanque_combustivel(self):
"""Função que mostra quanto de combustível há no tanque."""
print(f'Esse carro possui {self.tanque} litros')
class CarroEletrico(Carro):
"""Classe que representa o carro elétrico."""
def __init__(self, marca, modelo, ano):
"""Inicializa a classe pai/mãe."""
...
def tanque_combustivel(self):
"""Carros elétricos não possuem tanque de combustível."""
print('Carros elétricos não possuem tanque de combustível')
Agora, se alguém tentar chamar tanque_combustivel() com um carro elétrico, o Python ignorará o método tanque_combustivel() em Carro e executará este código em vez disso. Quando você usa herança, pode fazer suas classes filhas reterem o que você precisa e substituir qualquer coisa que você não precise da classe pai.
Instâncias como atributos
Ao modelar algo do mundo real em código, você pode descobrir que está adicionando mais e mais detalhes a uma classe. Você descobrirá que tem uma lista crescente de atributos e métodos e que seus arquivos estão se tornando longos. Nessas situações, você pode reconhecer que parte de uma classe pode ser escrita como uma classe separada. Você pode dividir sua classe grande em classes menores que funcionam juntas; essa abordagem é chamada de composição.
Por exemplo, se continuarmos adicionando detalhes à classe CarroEletrico, podemos notar que estamos adicionando muitos atributos e métodos específicos à bateria do carro. Quando vemos isso acontecendo, podemos parar e mover esses atributos e métodos para uma classe separada chamada Bateria. Então, podemos usar uma instância Bateria como um atributo na classe CarroEletrico:
class Bateria:
"""Classe que aramzena as informações da bateria."""
def __init__(self, capacidade_bateria=40): # 1
"""Função que inicializa os atributos da bateria."""
self.capacidade_bateria = capacidade_bateria
def descrever_bateria(self): # 2
"""Função que descreve a quantidade de carga da bateria."""
print(f'Esse carro possui uma bateria de {self.capacidade_bateria} kWh')
class CarroEletrico(Carro):
"""Classe que representa o carro elétrico."""
def __init__(self, marca, modelo, ano):
"""Inicializa a classe pai/mãe."""
super().__init__(marca, modelo, ano)
self.bateria = Bateria() # 3
def tanque_combustivel(self):
"""Carros elétricos não possuem tanque de combustível."""
print('Carros elétricos não possuem tanque de combustível')
carro_eletrico = CarroEletrico('BYD', 'dolphin', 2024)
print(carro_eletrico.descricao())
carro_eletrico.bateria.descrever_bateria()
Definimos uma nova classe chamada Bateria que não herda de nenhuma outra classe. O método __init__() (1) tem um parâmetro, capacidade_bateria, além de self. Este é um parâmetro opcional que define a carga da bateria para 40 se nenhum valor for fornecido. O método descrever_bateria() foi movido para esta classe também (2).
Na classe CarroEletrico, agora adicionamos um atributo chamado self.bateria (3). Esta linha diz ao Python para criar uma nova instância de Bateria (com a carga de 40, porque não estamos especificando um valor) e atribuir essa instância ao atributo self.bateria. Isso acontecerá toda vez que o método __init__( for chamado; qualquer instância de CarroEletrico agora terá uma instância de Bateria criada automaticamente.
Criamos um carro elétrico e o atribuímos à variável carro_eletrico. Quando queremos descrever a bateria, precisamos trabalhar com o atributo da bateria do carro:
carro_eletrico.bateria.descrever_bateria()
Esta linha diz ao Python para olhar a instância carro_eletrico, encontrar seu atributo bateria e chamar o método descrever_bateria() que está associado à instância Bateria atribuída ao atributo. A saída é idêntica à que vimos anteriormente.
Isso parece muito trabalho extra, mas agora podemos descrever a bateria com tantos detalhes quanto quisermos sem desorganizar a classe CarroEletrico. Vamos adicionar outro método a Bateria que relata o alcance do carro com base na carga da bateria:
class Bateria:
"""Classe que aramzena as informações da bateria."""
def __init__(self, capacidade_bateria=40):
"""Função que inicializa os atributos da bateria."""
self.capacidade_bateria = capacidade_bateria
def descrever_bateria(self):
"""Função que descreve a quantidade de carga da bateria."""
print(f'Esse carro possui uma bateria de {self.capacidade_bateria} kWh')
def obter_autonomia(self):
"""Função que mostra a autonomia do carro baseado na carga."""
if self.capacidade_bateria == 40:
autonomia = 150
elif self.capacidade_bateria == 65:
autonomia = 225
print(f'Esse carro pode percorrer {autonomia} Km com uma carga de {self.capacidade_bateria} kWh')
carro_eletrico = CarroEletrico('BYD', 'dolphin', 2024)
print(carro_eletrico.descricao())
carro_eletrico.bateria.descrever_bateria()
carro_eletrico.bateria.obter_autonomia() #1
O novo método obter_autonomia() realiza algumas análises simples. Se a capacidade da bateria for 40 kWh, obter_autonomia() define a autonomia para 150 quilômetros, e se a capacidade for 65 kWh, ele define a autonomia para 225 quialômetros. Ele então relata esse valor. Quando queremos usar esse método, temos que chamá-lo novamente por meio do atributo da bateria do carro (1). A saída nos diz a autonomia do carro com base na carga da bateria.
Modelagem de objetos do mundo real
À medida que você começa a modelar coisas mais complicadas, como carros elétricos, você vai lutar com questões interessantes. A autonomia de um carro elétrico é uma propriedade da bateria ou do carro? Se estivermos descrevendo apenas um carro, provavelmente não há problema em manter a associação do método obter_autonomia() com a classe Bateria. Mas se estivermos descrevendo toda a linha de carros de um fabricante, provavelmente queremos mover obter_autonomia() para a classe CarroEletrico. O método obter_autonomia() ainda verificaria a carga da bateria antes de determinar a autonomia, mas relataria uma autonomia específica para o tipo de carro ao qual está associado. Como alternativa, poderíamos manter a associação do método obter_autonomia() com a bateria, mas passar a ela um parâmetro como carro_modelo. O método obter_autonomia() então relataria uma autonomia com base na carga da bateria e no modelo do carro.
Isso o leva a um ponto interessante em seu crescimento como programador. Quando você luta com questões como essas, você está pensando em um nível lógico mais alto em vez de um nível focado na sintaxe. Você não está pensando em Python, mas em como representar o mundo real em código. Quando você chegar a esse ponto, perceberá que geralmente não há abordagens certas ou erradas para modelar situações do mundo real. Algumas abordagens são mais eficientes do que outras, mas é preciso prática para encontrar as representações mais eficientes. Se seu código estiver funcionando como você deseja, você está indo bem! Não desanime se perceber que está desmontando suas classes e as reescrevendo várias vezes usando abordagens diferentes. Na busca por escrever código preciso e eficiente, todos passam por esse processo.
Tente você mesmo!
Exercício 06: Um buffet de sorvete é um tipo específico de restaurante. Escreva uma classe chamada BuffetSorvete que herda a classe Restaurante no exercício 1 ou exercício 4. Qualquer versão da classe funcionará; basta escolher o que você mais gosta. Adicione um método chamado sabores() que armazena uma lista de sabores de sorvete. Escreva um método que exiba esses sabores. Crie uma instância do BuffetSorvete e chame esse método.
Exercício 07: Um administrador é um tipo especial de usuário. Escreva uma classe chamada Admin que herda da classe Usuario que você escreveu no exercício 3 ou exercício 5. Adicione um atributo, privilegios, que armazena uma lista de strings como "pode adicionar post", "pode excluir o post", "pode proibir o usuário" e assim por diante. Escreva um método chamado mostrar_privilegios() que lista o conjunto de privilégios do administrador. Crie uma instância de admin e chame o seu método.
Exercício 08: Escreva uma classe Privilegios separada. A classe deve ter um atributo, privilegios, que armazena uma lista de strings, conforme descrito no exercício 7. Mova o método mostrar_privilegios() para esta classe. Faça uma instância de Privilegios como um atributo na classe Admin. Crie uma nova instância de Admin e use seu método para mostrar seus privilégios.
Exercício 09: Use a versão final de CarroEletrico desta seção. Adicione um método à classe de bateria chamado atualizar_bateria(). Este método deve verificar o tamanho da bateria e definir a carga para 65, se ainda não estiver. Faça um carro elétrico com um tamanho de bateria padrão, chame obter_autonomia() uma vez e chame obter_autonomia() uma segunda vez depois de atualizar a bateria. Você deve ver um aumento no alcance do carro.
Classes de importação
À medida que você adiciona mais funcionalidade às suas classes, seus arquivos podem ficar longos, mesmo quando você usa a herança e a composição corretamente. De acordo com a filosofia geral do Python, convém manter seus arquivos o mais organizado possível. Para ajudar, o Python permite armazenar aulas em módulos e, em seguida, importar as classes necessárias para o seu programa principal.
Importando uma única classe
Vamos criar um módulo que contém apenas a classe de Carro. Isso traz à tona um problema sutil de nomeação: criaremos um arquivo chamado carro.py, mas este módulo deve ser chamado de carro.py porque contém código representando um carro. Aqui está carro.py com apenas o código do carro da classe:
"""Uma classe que pode ser usada para representar um carro.""" # 1
class Carro:
"""Classe que descreve um carro."""
def __init__(self, marca, modelo, ano):
"""Função que inicializa atributos da classe."""
self.marca = marca
self.modelo = modelo
self.ano = ano
self.odometro = 0
def descricao(self):
"""Função que retorna a descrição completa do carro."""
descricao_completa = f'{self.ano} {self.marca} {self.modelo}'
return descricao_completa
def ler_odometro(self):
"""Função que retorna o quanto o carro andou."""
print(f'Esse carro tem {self.odometro} Km rodados')
def atualizar_odometro(self, quilometragem):
"""Atualizar o odômetro para um valor fornecido.
Se o valor fornecido for menor do que o do odometro, impedir a mudança."""
if quilometragem >= self.odometro:
self.odometro = quilometragem
else:
print('Você não pode diminuir o valor do odômetro!')
def aumentar_odometro(self, quilometragem):
"""Função que aumenta a quilometragem conforme o carro anda."""
self.odometro += quilometragem
Incluímos um docstring no começo do módulo que descreve brevemente o conteúdo deste módulo (1). Você deve escrever um documento para cada módulo que criar.
Agora fazemos um arquivo separado chamado meu_carro.py. Este arquivo importará a classe Carro e, em seguida, criará uma instância dessa classe:
from carro import Carro # 1
meu_carro = Carro('Ford', 'Fiesta', 2018)
print(meu_carro.descricao())
meu_carro.odometro = 25
meu_carro.ler_odometro()
A declaração import (1) diz a Python para abrir o módulo carro e importar a classe Carro. Agora podemos usar a classe Carro como se estivesse definida neste arquivo. A saída é a mesma que vimos anteriormente.
Importar classes é uma maneira eficaz de programar. Imagine a quantidade de linhas que esse arquivo teria se toda a classe Carro fosse incluída. Quando você move a classe para um módulo e importa o módulo, ainda obtém todas as mesmas funcionalidades, mas mantém o arquivo principal do programa limpo e fácil de ler. Você também armazena a maior parte da lógica em arquivos separados; depois que suas classes funcionam como você deseja, você pode deixar esses arquivos em paz e se concentrar na lógica de nível superior do seu programa principal.
Armazenando várias classes em um módulo
Você pode armazenar quantas classes precisar em um único módulo, embora cada classe deva em um módulo próprio, fica mais organizado e relacionado desta maneira. As classes Bateria e CarroEletrico ajudam a representar carros, então vamos adicioná-los ao módulo carro.py:
"""Conjunto de classes usadas para representar carros a combustível e elétrico."""
class Carro:
"""Classe que descreve um carro."""
...
class Bateria:
"""Classe que aramzena as informações da bateria."""
...
class CarroEletrico(Carro):
"""Classe que representa o carro elétrico."""
...
Agora podemos criar um novo arquivo chamado meu_carro_eletrico.py, importar a classe CarroEletrico e criar um carro elétrico:
from carro import CarroEletrico
carro_eletrico = CarroEletrico('BYD', 'dolphin', 2024)
print(carro_eletrico.descricao())
carro_eletrico.bateria.descrever_bateria()
carro_eletrico.bateria.obter_autonomia()
Importando várias classes de um módulo
Você pode importar quantas classes precisar para um arquivo de programa. Se queremos fazer um carro comum e um carro elétrico no mesmo arquivo, precisamos importar as duas classes, Carro e CarroEletrico:
from carro import Carro, CarroEletrico # 1
meu_carro = Carro('Ford', 'Fiesta', 2018) # 2
print(meu_carro.descricao())
meu_carro_eletrico = CarroEletrico('BYD', 'Dolphin', 2024) # 3
print(meu_carro_eletrico.descricao())
Você importa várias classes de um módulo, separando cada classe com uma vírgula (1). Depois de importar as classes necessárias, você estará livre para fazer quantas instâncias de cada classe for necessário.
Neste exemplo, fabricamos um Ford Fiesta (2) movido a gasolina e, em seguida, um BYD Dolphin (3) elétrico.
Importando um módulo inteiro
Você também pode importar um módulo inteiro e, em seguida, acessar as classes necessárias usando a notação de pontos. Essa abordagem é simples e resulta em código fácil de ler. Como toda chamada que cria uma instância de uma classe inclui o nome do módulo, você não terá conflitos de nomeação com nenhum nome usado no arquivo atual.
Aqui está o que parece importar o módulo carro inteiro e cria um carro comum e um carro elétrico:
import carro # 1
meu_carro = carro.Carro('Ford', 'Fiesta', 2018) # 2
print(meu_carro.descricao())
meu_carro_eletrico = carro.CarroEletrico('BYD', 'Dolphin', 2024) # 3
print(meu_carro_eletrico.descricao())
Primeiro, importamos o módulo carro inteiro (1). Depois, acessamos as classes de que precisamos através da sintaxe nome_modulo.NomeClasse. Novamente, criamos um Ford Fiesta (2) e um BYD Dolphin (3).
Importando todas as classes de um módulo
Você pode importar todas as classes de um módulo usando a seguinte sintaxe:
from nome_modulo import *
Este método não é recomendado por dois motivos. Primeiro, é útil poder ler as instruções de importação no topo de um arquivo e ter uma noção clara de quais classes um programa usa. Com essa abordagem, não está claro quais classes você está usando no módulo. Essa abordagem também pode levar a confusão com os nomes no arquivo. Se você importar acidentalmente uma classe com o mesmo nome que outra coisa no arquivo do seu programa, poderá criar erros difíceis de diagnosticar. Eu mostro isso aqui porque, embora não seja uma abordagem recomendada, é provável que você o veja no código de outras pessoas em algum momento.
Se você precisar importar muitas classes de um módulo, é melhor importar o módulo inteiro e usar a sintaxe nome_modulo.NomeClasse. Você não verá todas as classes usadas na parte superior do arquivo, mas verá claramente onde o módulo é usado no programa. Você também evitará os possíveis conflitos de nomeação que podem surgir quando você importar todas as classes de um módulo.
Importando um módulo para um módulo
Às vezes, você deseja espalhar suas classes por vários módulos para impedir que qualquer arquivo fique muito grande e evite armazenar classes não relacionadas no mesmo módulo. Ao armazenar suas classes em vários módulos, você pode descobrir que uma classe em um módulo depende de uma classe em outro módulo. Quando isso acontece, você pode importar a classe necessária para o primeiro módulo.
Por exemplo, vamos armazenar a classe Carro em um módulo e CarroEletrico e Bateria em um módulo separado. Faremos um novo módulo chamado carro_eletrico.py e copiaremos apenas as classes CarroEletrico e Bateria neste arquivo:
"""Um conjunto de classes que podem ser usadas para representar um carro elétrico."""
from carro import Carro
class Bateria:
...
class CarroEletrico(carro):
...
O classe CarroEletrico precisa de acesso a classe pai, por isso importamos Carro diretamente para o módulo. Se esquecermos essa linha, o Python levantará um erro quando tentarmos importar o módulo CarroEletrico. Também precisamos atualizar o módulo carro para que ele contém apenas a classe Carro.
Agora podemos importar de cada módulo separadamente e criar qualquer tipo de carro que precisar:
from carro import carro
from carro_eletrico import CarroEletrico
meu_carro = Carro('Ford', 'Fiesta', 2018)
print(meu_carro.descricao())
meu_carro_eletrico = CarroEletrico('BYD', 'Dolphin', 2024)
print(meu_carro_eletrico.descricao())
Nós importamos Carro do seu módulo e CarroEletrico de seu módulo. Em seguida, criamos um carro comum e um carro elétrico. Ambos os carros são criados corretamente.
Usando apelidos
Como você viu no Capítulo 8, os apelidos podem ser bastante úteis ao usar módulos para organizar o código de seus projetos. Você também pode usar apelidos ao importar classes.
Como exemplo, considere um programa em que você deseja fazer um monte de carros elétricos. Pode ser entediante digitar (e ler) CarroEletrico repetidamente. Você pode dar ao CarroEletrico um apelido na declaração de importação:
from carro_eletrico import CarroEletrico as CE
Agora você pode usar esse apelido sempre que quiser fazer um carro elétrico:
meu_carro_eletrico = EC('BYD', 'Dolphin', 2024)
Você também pode dar um apelido a um módulo. Veja como importar todo o módulo carro_eletrico usando um apelido:
import carro_eletrico as ce
Agora você pode usar este apelido do módulo com o nome completo da classe:
meu_carro_eletrico = ce.CarroEletrico('BYD', 'Dolphin', 2024)
Encontrando seu próprio fluxo de trabalho
Como você pode ver, o Python oferece muitas opções de como estruturar o código em um grande projeto. É importante conhecer todas essas possibilidades para que você possa determinar as melhores maneiras de organizar seus projetos e entender os projetos de outras pessoas.
Ao começar, mantenha sua estrutura de código simples. Tente fazer tudo em um arquivo e mover suas classes para separar os módulos quando tudo estiver funcionando. Se você gosta de como os módulos e arquivos interagem, tente armazenar suas classes em módulos quando iniciar um projeto. Encontre uma abordagem que permita escrever código que funcione e vá a partir daí.
A biblioteca padrão do Python
A Biblioteca Padrão Python é um conjunto de módulos incluídos em todas as instalações do Python. Agora que você tem um entendimento básico de como as funções e as classes funcionam, você pode começar a usar módulos como esses que outros programadores escreveram. Você pode usar qualquer função ou classe na biblioteca padrão, incluindo uma instrução de importação simples na parte superior do seu arquivo. Vejamos um módulo, random, que pode ser útil para modelar muitas situações do mundo real.
Uma função interessante do módulo random é randint(). Esta função leva dois argumentos inteiros e retorna um número inteiro selecionado aleatoriamente entre (e incluindo) esses números.
Veja como gerar um número aleatório entre 1 e 6:
from random import randint
print(randint(1, 6))
Outra função útil é a choice(). Esta função recebe uma lista ou tupla e retorna um elemento escolhido aleatoriamente:
from random import choice
frutas = ['maçã', 'banana', 'melancia', 'abacate']
fruta_selecionada = choice(frutas)
print(fruta_selecionada)
O módulo random não deve ser usado ao criar aplicativos relacionados à segurança, mas funciona bem para muitos projetos divertidos e interessantes.
Tente você mesmo!
Exercício 10: Faça uma classe Dado com um atributo chamado lados, que possui um valor padrão de 6. Escreva um método chamado rolar_dado() que imprime um número aleatório entre 1 e o número de lados que o dado possui. Faça um dado de 6 lados e enrole-o 10 vezes. Faça um dado de 10 lados e um dado de 20 lados. Role cada dado 10 vezes.
Exercício 11: Faça uma lista contendo uma série de 60 números (1 a 60). Selecione aleatoriamente 6 números da lista e imprima uma mensagem dizendo que qualquer bilhete correspondendo a esses 6 números ganha um prêmio.
Estilo das classes
Vale a pena esclarecer alguns problemas de estilo relacionados às classes, especialmente quando seus programas se tornam mais complicados.
Os nomes da classe devem ser escritos em Camelcase. Para fazer isso, capitalize a primeira letra de cada palavra no nome e não use sublinhado. Os nomes de instância e módulos devem ser gravados em minúsculas, com sublinhados entre as palavras.
Toda classe deve ter um docstring imediatamente após a definição da classe. O docstring deve ser uma breve descrição do que a classe faz e você deve seguir as mesmas convenções de formatação que você usou para escrever docstring nas funções. Cada módulo também deve ter um docstring descrevendo a função de cada um e como utilizá-los.
Você pode usar linhas em branco para organizar o código, mas não as use excessivamente. Dentro de uma classe, você pode usar uma linha em branco entre os métodos e, dentro de um módulo, você pode usar duas linhas em branco para separar as classes.
Se você precisar importar um módulo da biblioteca padrão e um módulo que você escreveu, coloque primeiro a instrução import para o módulo da biblioteca padrão. Em seguida, adicione uma linha em branco e a declaração de importação para o módulo que você escreveu. Em programas com várias declarações de importação, esta convenção facilita a ver de onde vêm os diferentes módulos usados no programa.
Resumo
Neste capítulo, você aprendeu a escrever suas próprias classes. Você aprendeu a armazenar informações em uma classe usando atributos e como escrever métodos que dão às suas classes o comportamento de que precisam. Você aprendeu a escrever métodos __init __() que criam instâncias de suas classes com exatamente os atributos que você deseja. Você viu como modificar os atributos de uma instância diretamente e através de métodos. Você aprendeu que a herança pode simplificar a criação de classes relacionadas entre si e aprendeu a usar instâncias de uma classe como atributos em outra classe para simplificar cada classe.
Você viu como o armazenamento de classes em módulos e as classes de importação necessário para os arquivos onde eles serão usados podem manter seus projetos organizados. Você começou a aprender sobre a biblioteca padrão do Python e viu um exemplo baseado no módulo random. Por fim, você aprendeu a estilizar suas classes usando convenções Python.
No capítulo 10, você aprenderá a trabalhar com arquivos para salvar o trabalho que fez em um programa e o trabalho que permitiu que os usuários fizessem. Você também aprenderá sobre exceções, uma classe Python especial projetada para ajudá-lo a responder a erros quando surgirem.