Introdução#

Ah, a tal da programação orientada a objetos!

Muita gente não entende bem o que é isso, e por não entender acaba não usando ou usando da forma errada. Nesta seção eu vou tentar explicar esse tema de uma forma simples e prática, para que você possa entender e considerar usar essa nova forma de programar daqui para a frente.

Os paradigmas de programação#

A programação orientada a objetos também chamada pelo acrônimo POO e a programação estruturada são dois jeitos diferentes de programar, cada um com suas características e finalidades.

Tudo que vimos até aqui foi programação estruturada, que segue uma sequência linear.

Na programação estruturada, o foco está em dividir o código em funções. Essas funções fazem tarefas específicas, como calcular algo ou mostrar uma mensagem na tela. O código segue uma sequência de passos, como uma receita de bolo, com começo, meio e fim. Os dados, ou seja, as informações que o código usa, ficam separados das funções e são manipulados diretamente por elas. Esse método funciona bem para códigos menores e mais simples, mas pode se tornar difícil de organizar à medida que o código cresce e fica mais complexo.

Já na POO, o foco é em criar objetos que representam coisas ou conceitos do mundo real. Esses objetos possuem tanto dados (as características do objeto) quanto comportamentos (as ações que ele pode fazer). Por exemplo, você pode imaginar um Carro como um objeto com características como cor e velocidade, e com comportamentos como acelerar e frear. Na POO, o código fica mais organizado, porque cada objeto é responsável pelos seus próprios dados e ações, o que facilita manter e melhorar o código com o tempo.

Outro exemplo é uma classe Televisão que tem características como marca, tamanho e canal atual, e comportamentos como ligar, desligar e trocar de canal. Cada televisão que você criar a partir dessa classe terá suas próprias características e comportamentos, mas todas seguirão o mesmo modelo.

Mas vamos com calma e entender melhor esse negócio de classes e objetos. Antes de ir direto ao ponto, vamos entender melhor o que é POO.

O que é POO?#

Quero deixar claro aqui que POO não é algo específico de Python, mas sim uma forma de programar que considera a construção de objetos personalizados para resolver problemas. Essa nova forma de programar é inclusive usada em várias outras linguagens de programação.

POO é um paradigma de programação (uma forma de programar) que utiliza objetos para representar entidades do mundo real. Falando especificamente de Python, absolutamente tudo em Python é um objeto. Nós já utilizamos POO em Python, mas não de forma explícita.

O que eu quero dizer com isso?

Nós vimos, por exemplo, como manipular valores numéricos (int e float), strings, listas, dicionários e conjuntos. Cada um destes tipos de dados representa um objeto em Python. Um exemplo básico e simples:

minha_lista = [1, 2, 3, 4, 5]

Nesse exemplo, a variável minha_lista representa um objeto do tipo list. Por ela ser deste tipo, eu posso utilizar todas as funcionalidades de listas, conforme vimos no capítulo de listas. E da mesma forma, vimos também operações que são específicas de strings, dicionários e conjuntos.

Esse é o poder da orientação a objetos. Só que a partir de agora nós não vamos trabalhar apenas com os objetos nativos do Python, e sim vamos aprender a construir nossos próprios objetos, que vão ter operações que nós mesmos podemos definir!

A melhor forma de entender POO é através de exemplos e prática, sem perder de vista a parte conceitual que será nova e é super importante. Então vamos lá? Vamos aprender a criar nossos próprios objetos em Python!

Conceitos fundamentais de POO#

Classe#

Uma classe em POO é como um molde ou modelo que descreve como um certo tipo de objeto deve ser. É uma forma de agrupar dados (chamados de atributos) e funções (chamadas de métodos) que trabalham juntos para representar algo do mundo real no código. E eu gosto ainda de dizer que atributos são o que o objeto tem, e métodos são o que o objeto faz.

Vamos para um exemplo prático.

Em Python (e acredito eu que em nenhuma outra linguagem), temos um objeto que represente um carro, certo? Pois bem, em Python, podemos criar uma classe que representa de forma genérica um carro. Veja como podemos fazer isso:

class Carro:
    pass

Nós definimos uma classe em Python conforme acima. Usamos a palavra-chave class seguida do nome da classe, e em seguida abrimos um bloco de código com :. Dentro desse bloco, vamos definir como os objetos criados a partir desse molde vão se comportar. A princípio a nossa classe Carro não faz nada, mas ela já é um objeto em si, e podemos criar objetos a partir dela.

Instância#

class Carro:
    pass


carro = Carro()

Aqui já entra outro conceito de POO, instância. A classe Carro representa de forma genérica um molde para todos os carros, e a partir desse molde, eu posso criar objetos específicos. Cada objeto destes chamamos de instância. No exemplo acima, a variável carro é uma instância da classe Carro. É um tipo específico de carro criado a partir do molde genérico. Vamos ver outro exemplo:

class Carro:
    pass


chevrolet_onix = Carro()
fiat_uno = Carro()

Neste outro exemplo, temos uma classe Carro e a partir dela, criamos 2 instâncias, chevrolet_onix e fiat_uno. Cada instância dessa é um objeto da classe Carro, e são 2 instâncias diferentes.

Nota (PascalCase)

Um padrão comum em POO é usar PascalCase para nomear classes. Isso significa que cada palavra no nome da classe começa com letra maiúscula, sem espaços. Alguns exemplos: Carro, Televisao, ContaCorrente, BancoDeDados, Conexao. Vamos seguir esse padrão daqui para frente!

Atributos (o que o objeto tem?)#

Vamos pensar na classe Carro. O que um carro tem? Um carro tem marca, modelo, cor, ano de fabricação, e quilometragem. Estas informações que um carro tem é o que chamamos de atributos.

Atributos basicamente respondem à pergunta o que esse objeto tem? e são definidos dentro da classe. Vamos adicionar esses atributos à nossa classe Carro:

Nota (muita calma nesta hora)

O que você verá a seguir parece algo assustador, mas fique comigo que vou explicar passo a passo, no detalhe, e você vá pensando e refletindo pra ver principalmente se compreende cada um dos conceitos.

class Carro:
    def __init__(self, marca: str):
        # O que um carro tem? Estes são os atributos da classe Carro
        self.marca = marca


onix = Carro(marca="Chevrolet")
uno = Carro(marca="Fiat")

print(onix.marca)
print(uno.marca)
Chevrolet
Fiat

Nós acrescentamos um método especial na nossa classe Carro chamado __init__. Métodos são exatamente como funções, tanto é que são definidos com a mesma palavra reservada def, a diferença é que métodos são funções que pertencem a um objeto. E todo método de uma classe deve ter como primeiro parâmetro a palavra-chave self.

Esse método __init__ é chamado de método construtor e é um método especial que é chamado toda vez que um objeto é criado a partir da classe. Ou seja, na criação do objeto, por exemplo, na linha onix = Carro(marca="Chevrolet"), o método __init__ da classe é chamado.

Quando nós inicializamos a instância onix com a linha onix = Carro(marca="Chevrolet"), percebam que após a instanciação eu consigo acessar o atributo marca da instância onix com a notação de ponto com onix.marca. O método __init__ que é responsável por inicializar os atributos da instância.

O gif abaixo mostra exatamente isso, quando as linhas onix = Carro(marca="Chevrolet") e uno = Carro(marca="Fiat"), o método construtor __init__ é executado.

../_images/10-01-execucao-metodo-construtor.gif

Dica (entendendo o tal do self)

Eu demorei uns 6 meses pra entender o que significa esse self. E depois que entendi, eu tento trazer clareza para as pessoas que estão começando.

O self em Python é uma maneira de a própria classe se referir ao objeto que está sendo criado ou manipulado. Eu sei que é difícil entender, mas vamos com calma. Leia e releia várias vezes e pense a respeito.

Quando executamos onix = Carro(marca="Chevrolet"), o self dentro do método __init__ se refere a esse objeto onix em específico. Em palavras mais simples, quando executamos onix = Carro(marca="Chevrolet"), o self representa a instância onix, e quando executamos uno = Carro(marca="Fiat"), o self representa a instância uno.

Quando fazemos self.marca = marca, estamos dizendo que o atributo .marca do objeto sendo criado vai receber algum valor, e esse valor é o parâmetro marca da função __init__.

Nós só conseguimos acessar o atributo marca com a notação de ponto, onix.marca e uno.marca, pois existe self.marca = marca no método __init__.

Pra finalizar, novamente: o self é um parâmetro obrigatório em todos os métodos de uma classe, e ele se refere ao objeto que está sendo criado ou manipulado.

É super importante você entenda o que é o self. Se você não entendeu, por favor, me chame na comunidade do Discord ou no meu LinkedIn que eu vou tentar explicar de uma forma que você consiga entender. Se for necessário eu até gravo um vídeo explicando isso. Só preciso saber de alguma forma se você entendeu somente com o texto ou não.

Atenção: não continue a leitura se não tiver um entendimento claro do que é o self. Me chame nos canais de comunicação acima se não entendeu!

A partir deste ponto do livro, eu vou pressupor que você entendeu o que é o self, ok?

Dado que você entendeu o que significa o self, vamos seguir com outros exemplos, acrescentando mais atributos à nossa classe Carro.

class Carro:
    def __init__(self, marca: str, modelo: str, cor: str, ano_fabricacao: int):
        # O que um carro tem? Estes são os atributos da classe Carro
        self.marca = marca
        self.modelo = modelo
        self.cor = cor
        self.ano_fabricacao = ano_fabricacao


onix = Carro(
    marca="Chevrolet", modelo="Onix Hatch 1.0", cor="prata", ano_fabricacao=2024
)
uno = Carro(marca="Fiat", modelo="Uno 1.6", cor="Vermelho", ano_fabricacao=1997)

print("Acessando os atributos do objeto onix:")
print(onix.marca)
print(onix.modelo)
print(onix.cor)
print(onix.ano_fabricacao)

print("Acessando os atributos do objeto uno:")
print(uno.marca)
print(uno.modelo)
print(uno.cor)
print(uno.ano_fabricacao)
Acessando os atributos do objeto onix:
Chevrolet
Onix Hatch 1.0
prata
2024
Acessando os atributos do objeto uno:
Fiat
Uno 1.6
Vermelho
1997

Olhem só que legal! Agora temos 4 atributos na nossa classe Carro: marca, modelo, cor e ano_fabricacao.

E quando vamos instanciar um novo carro, podemos passar valores diferentes para uma nova instância, e cada instância terá seus próprios valores para cada um dos atributos.

Notem bem os conceitos:

  • classe é o molde genérico que define o que tem em um carro. No nosso exemplo, a classe Carro.

  • instância é um objeto específico criado a partir do molde genérico. No nosso exemplo, temos 2 instâncias, onix e uno.

  • atributos são as características do objeto criado, o que ele tem. No nosso exemplo, a classe tem os seguintes atributos: marca, modelo, cor e ano_fabricacao.

É como se agrupássemos variáveis de algo (um objeto) para representar algo do mundo real. Esse é o poder da POO!

A parte mais interessante é que a estratégia para definir os atributos é você quem escolhe. Vamos modificar um pouco a nossa classe para representar um carro zero quilômetros.

class Carro:
    def __init__(self, marca: str, modelo: str, cor: str, ano_fabricacao: int):
        # O que um carro tem? Estes são os atributos da classe Carro
        self.marca = marca
        self.modelo = modelo
        self.cor = cor
        self.ano_fabricacao = ano_fabricacao
        # Aqui eu defino a quilometragem inicial de todas as instâncias da classe Carro como 0 por padrão
        self.quilometragem = 0


onix = Carro(
    marca="Chevrolet", modelo="Onix Hatch 1.0", cor="prata", ano_fabricacao=2024
)

print(f"O {onix.marca} {onix.modelo} está com {onix.quilometragem} km rodados")
O Chevrolet Onix Hatch 1.0 está com 0 km rodados

No exemplo acima, eu adicionei um novo atributo quilometragem que representa a quilometragem do carro, que, por padrão, é zero. Ou seja, qualquer instanciação de um carro da forma como foi feito, o objeto será representado por um carro zero quilômetros.

Quem define como os atributos serão criados é você, e para isso, é necessário bastante criatividade e poder de abstração!

Métodos (o que o objeto faz?)#

Da mesma forma que podemos pensar que atributos são o que um objeto tem, podemos pensar que métodos são o que um objeto faz. Métodos são funções que pertencem a um objeto, e são definidos dentro da classe. Eu te pergunto, o que um carro faz? Podemos pensar que um carro viaja, acelera, freia, liga, desliga, e por aí vai. Vamos ver como é a implementação do método viaja.

class Carro:
    def __init__(self, marca: str, modelo: str, cor: str, ano_fabricacao: int):
        self.marca = marca
        self.modelo = modelo
        self.cor = cor
        self.ano_fabricacao = ano_fabricacao
        self.quilometragem = 0

    def viaja(self, km_viajados: int) -> None:
        self.quilometragem += km_viajados


onix = Carro(
    marca="Chevrolet", modelo="Onix Hatch 1.0", cor="prata", ano_fabricacao=2024
)

print(
    f"O {onix.marca} {onix.modelo} está com {onix.quilometragem} km rodados antes de viajar."
)

# O carro viajou 100 km quando eu chamo o método
onix.viaja(100)  # O carro viajou 100 km quando eu chamo o método

print(
    f"Depois da primeira viagem o carro agora está com {onix.quilometragem} km rodados."
)

# O carro viajou mais 300 km na segunda chamada do método
onix.viaja(300)

print(
    f"Depois da segunda viagem o carro agora está com {onix.quilometragem} km rodados."
)
O Chevrolet Onix Hatch 1.0 está com 0 km rodados antes de viajar.
Depois da primeira viagem o carro agora está com 100 km rodados.
Depois da segunda viagem o carro agora está com 400 km rodados.

Observem só que legal!

Nós definimos um método viaja que recebe como parâmetro a distância que o carro vai viajar (e o self, mas já vimos que ele aparecerá como primeiro parâmetro em todos os métodos) e somamos esse valor ao atributo .quilometragem da instância onix criada. Percebam que o atributo é acessado via onix.quilometragem e o valor é acumulado. Toda vez que eu chamar onix.viaja(distancia), o valor do atributo .quilometragem será incrementado.

É literalmente como se fosse um estado do carro, que se inicia com quilometragem zero e se acumula a cada viagem (chamada do método) que o carro faz.

Não é simplesmente fantástico o que conseguimos fazer com POO? Daqui pra frente, é a imaginação e a criatividade que falam mais alto na hora de criar nossos objetos!

Exemplos reais#

Vamos ver alguns exemplos mais reais com contexto de negócio.

Exemplo 1: conta bancária#

class ContaBancaria:
    def __init__(self, numero: int, saldo=0):
        self.numero = numero
        self.saldo = saldo

    def depositar(self, valor):
        self.saldo += valor

    def sacar(self, valor):
        if self.saldo >= valor:
            self.saldo -= valor
            return True
        return False


conta_joao = ContaBancaria(numero=1234, saldo=1000)

conta_maria = ContaBancaria(numero=5678, saldo=500)

Aqui temos um exemplo bem clássico de uma classe que representa uma conta bancária. No nosso caso em específico, a classe ContaBancaria tem os atributos numero e saldo, e os métodos depositar e sacar (além do método construtor __init__).

Podemos até pensar em outros métodos, como transferir, extrato, render_juros, e por aí vai. A sua criatividade é o limite!

Exemplo 2: reserva de hotel#

class Reserva:
    def __init__(self, id_cliente, data, hora):
        self.id_cliente = id_cliente
        self.data = data
        self.hora = hora
        self.confirmada = False

    def confirmar(self):
        self.confirmada = True

Pensando em uma reserva, temos o cliente que fez a reserva, a data e hora da reserva. Nossa classe bem simples ainda tem implementado um método confirmar que atualiza o status do atributo .confirmada para True.

Exemplo 3: pedido de uma plataforma de e-commerce#

class Pedido:
    def __init__(self, numero: int, itens: list[dict[str, float]]):
        self.numero = numero
        self.itens = itens
        self.status = "Em processamento"

    def calcular_total(self):
        return sum(item["preco"] * item["quantidade"] for item in self.itens)

    def atualizar_status(self, novo_status: str):
        self.status = novo_status

Nada de novo até aqui. Temos uma classe Pedido, que contém os atributos numero representando o número do pedido e itens, que são os itens do pedido. A partir de um pedido podemos calcular o total do pedido baseado no preço unitário e quantidade de cada item da lista. É possível ainda atualizar o status de um pedido passando um novo status.

Não sei se vocês perceberam, mas a POO é abstração pura! Você consegue representar qualquer coisa do mundo real em código, e isso realmente é muito poderoso! Agora é com você, pratique bastante e crie seus próprios objetos!

Prática#

Agora é com você, pratique bastante e crie seus próprios objetos! Resolva o exercício 17 para fixar o conteúdo aprendido! Se tiver dúvidas, não hesite em me chamar!

Conclusão#

Vimos até aqui o básico de POO. Acredite, é o básico mesmo, pois este é um tema que é vasto e complexo. Mas o que eu quero que você entenda é que POO é uma forma de programar que considera a construção de objetos personalizados para resolver problemas. E a partir de agora, nós aprendemos a construir nossos próprios objetos em Python.

Neste capítulo, vimos os conceitos fundamentais de POO, como classe, instância, atributos e métodos, incluindo o método construtor. Vimos também exemplos práticos de como criar classes e objetos em Python, e como definir atributos e métodos.

Na próxima seção vamos aprofundar um pouco mais em POO, e vamos ver conceitos mais avançados, como atributos de classe, herança, polimorfismo e encapsulamento! Até lá!