Orientação a Objetos: classes e objetos

1. Introdução à Programação Orientada a Objetos em Python

A Programação Orientada a Objetos (POO) é um paradigma de programação que organiza o código em torno de "objetos" — entidades que combinam dados (atributos) e comportamentos (métodos). Em Python, a POO não é apenas suportada, mas profundamente integrada à linguagem: praticamente tudo em Python é um objeto, de inteiros a funções.

Os conceitos fundamentais são simples: uma classe funciona como um molde ou planta, definindo a estrutura e o comportamento que seus objetos terão. Um objeto (ou instância) é uma concretização desse molde, com valores próprios para cada atributo.

Diferente de linguagens como Java ou C++, Python adota uma abordagem mais flexível. Não existe encapsulamento rígido (atributos privados são uma convenção, não uma imposição) e o sistema de tipos é dinâmico, favorecendo o duck typing: "se anda como um pato e grasna como um pato, então é um pato". Isso significa que o que importa são os métodos e atributos que um objeto possui, não sua classe formal.

2. Definindo Classes e Criando Objetos

A sintaxe para definir uma classe em Python utiliza a palavra-chave class, seguida do nome em PascalCase (primeira letra de cada palavra em maiúsculo):

class Pessoa:
    pass  # classe vazia, apenas para demonstração

Para criar um objeto, chamamos a classe como se fosse uma função:

pessoa1 = Pessoa()
pessoa2 = Pessoa()

O método especial __init__ é o construtor da classe. Ele é executado automaticamente quando criamos um novo objeto e serve para inicializar seus atributos:

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

p = Pessoa("Alice", 30)
print(p.nome)  # Alice

3. Atributos de Instância

Atributos de instância são variáveis que pertencem a cada objeto individualmente. Eles são definidos dentro de __init__ usando o parâmetro self, que representa a própria instância:

class Carro:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.ligado = False  # valor padrão

meu_carro = Carro("Toyota", "Corolla")
print(meu_carro.marca)    # Toyota
print(meu_carro.ligado)   # False

Acessamos e modificamos atributos usando a notação ponto:

meu_carro.ligado = True
print(meu_carro.ligado)  # True

Uma característica interessante (e controversa) do Python é a possibilidade de adicionar atributos dinamicamente fora da classe:

meu_carro.cor = "Azul"  # atributo criado apenas para esta instância

Isso é flexível, mas pode levar a bugs difíceis de rastrear. Por isso, é boa prática declarar todos os atributos esperados no __init__.

4. Métodos de Instância

Métodos de instância são funções definidas dentro da classe que operam sobre os dados do objeto. O primeiro parâmetro de todo método de instância é self:

class Calculadora:
    def __init__(self, valor_inicial=0):
        self.valor = valor_inicial

    def somar(self, numero):
        self.valor += numero
        return self.valor

    def subtrair(self, numero):
        self.valor -= numero
        return self.valor

calc = Calculadora(10)
print(calc.somar(5))     # 15
print(calc.subtrair(3))  # 12

Métodos podem interagir entre si através de self:

class Contador:
    def __init__(self):
        self.contagem = 0

    def incrementar(self):
        self.contagem += 1

    def resetar(self):
        self.contagem = 0
        print("Contador zerado!")

    def processar(self, vezes=1):
        for _ in range(vezes):
            self.incrementar()
        self.resetar()

c = Contador()
c.processar(5)  # Contador zerado!

5. O Parâmetro self e o Escopo de Objeto

O self é uma referência explícita à própria instância do objeto. Diferente de linguagens como Java ou C++, onde this é implícito, Python exige que self seja sempre o primeiro parâmetro dos métodos de instância.

Isso torna o código mais explícito: quando você vê self.atributo, sabe imediatamente que está acessando um atributo daquele objeto específico.

class Comparador:
    def __init__(self, valor):
        self.valor = valor

    def comparar_com(self, outro_objeto):
        if self.valor > outro_objeto.valor:
            return f"{self.valor} é maior que {outro_objeto.valor}"
        elif self.valor < outro_objeto.valor:
            return f"{self.valor} é menor que {outro_objeto.valor}"
        else:
            return "São iguais"

a = Comparador(10)
b = Comparador(20)
print(a.comparar_com(b))  # 10 é menor que 20

6. Comparação entre Objetos e Identidade

Python oferece duas formas principais de comparar objetos:

  • ==: verifica igualdade de valor (pode ser personalizada)
  • is: verifica identidade de objeto (se duas variáveis referenciam o mesmo objeto na memória)
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True (mesmo conteúdo)
print(a is b)  # False (objetos diferentes)
print(a is c)  # True (mesmo objeto)

Podemos personalizar a comparação com == implementando o método especial __eq__:

class Pessoa:
    def __init__(self, nome, cpf):
        self.nome = nome
        self.cpf = cpf

    def __eq__(self, outra):
        if not isinstance(outra, Pessoa):
            return False
        return self.cpf == outra.cpf

p1 = Pessoa("João", "123")
p2 = Pessoa("João", "123")
p3 = Pessoa("Maria", "456")

print(p1 == p2)  # True (mesmo CPF)
print(p1 == p3)  # False (CPFs diferentes)

Objetos em Python são mutáveis por padrão — podemos alterar seus atributos após a criação. Isso difere de tipos imutáveis como int ou str, onde qualquer "modificação" cria um novo objeto.

7. Boas Práticas com Classes e Objetos

Algumas práticas recomendadas ao trabalhar com classes em Python:

  • Nomenclatura: Classes em PascalCase (MinhaClasse), instâncias e métodos em snake_case (meu_objeto, meu_metodo)
  • Mantenha o __init__ simples: O construtor deve apenas inicializar atributos. Lógica complexa vai para métodos separados
  • Documente atributos esperados: Use docstrings ou type hints para deixar claro quais atributos uma instância deve ter
  • Evite atributos dinâmicos excessivos: Eles tornam o código imprevisível. Declare tudo no __init__
class Produto:
    """Representa um produto em estoque."""

    def __init__(self, nome: str, preco: float, quantidade: int = 0):
        self.nome = nome
        self.preco = preco
        self.quantidade = quantidade

    def aplicar_desconto(self, percentual: float):
        """Aplica um percentual de desconto ao preço."""
        if 0 <= percentual <= 100:
            self.preco *= (1 - percentual / 100)

8. Exemplo Prático Combinando os Conceitos

Vamos criar uma classe ContaBancaria que demonstra todos os conceitos vistos:

class ContaBancaria:
    def __init__(self, titular: str, saldo_inicial: float = 0.0):
        self.titular = titular
        self.saldo = saldo_inicial
        self._transacoes = []  # convenção: _ indica "privado"

    def depositar(self, valor: float) -> str:
        if valor <= 0:
            return "Valor inválido para depósito"
        self.saldo += valor
        self._transacoes.append(f"Depósito: +R${valor:.2f}")
        return f"Depósito de R${valor:.2f} realizado. Saldo: R${self.saldo:.2f}"

    def sacar(self, valor: float) -> str:
        if valor <= 0:
            return "Valor inválido para saque"
        if valor > self.saldo:
            return "Saldo insuficiente"
        self.saldo -= valor
        self._transacoes.append(f"Saque: -R${valor:.2f}")
        return f"Saque de R${valor:.2f} realizado. Saldo: R${self.saldo:.2f}"

    def extrato(self) -> str:
        return f"Titular: {self.titular}\nSaldo: R${self.saldo:.2f}"

    def __str__(self) -> str:
        return f"Conta de {self.titular} - Saldo: R${self.saldo:.2f}"

    def __repr__(self) -> str:
        return f"ContaBancaria('{self.titular}', {self.saldo})"

# Testando a classe
conta1 = ContaBancaria("Alice", 1000)
conta2 = ContaBancaria("Bob", 500)

print(conta1.depositar(200))     # Depósito de R$200.00 realizado. Saldo: R$1200.00
print(conta1.sacar(300))         # Saque de R$300.00 realizado. Saldo: R$900.00
print(conta2.sacar(600))         # Saldo insuficiente

print(conta1)  # Conta de Alice - Saldo: R$900.00 (usando __str__)
print(repr(conta2))  # ContaBancaria('Bob', 500) (usando __repr__)

# Independência entre objetos
print(conta1.saldo)  # 900.0
print(conta2.saldo)  # 500.0

O método __str__ fornece uma representação amigável para usuários finais (usada por print()), enquanto __repr__ oferece uma representão técnica que idealmente poderia recriar o objeto (usada no console interativo).

A POO em Python é poderosa e flexível. Dominar classes e objetos é o primeiro passo para escrever código mais organizado, reutilizável e alinhado com o espírito da linguagem.

Referências