Orientação a objetos (OOP) é um modelo de organização de código onde dados e comportamentos ficam juntos em classes. Para projetos de análise de dados e automação que crescem além de scripts simples — múltiplos conectores, pipelines reutilizáveis, lógica de negócio compartilhada — OOP torna o código mais fácil de manter, testar e estender.

Classe básica e __init__

class RelatorioVendas:
    def __init__(self, arquivo, regiao):
        self.arquivo = arquivo
        self.regiao = regiao
        self.df = None   # será carregado depois

    def carregar(self):
        import pandas as pd
        self.df = pd.read_csv(self.arquivo)
        return self  # retornar self permite encadear chamadas

    def filtrar_regiao(self):
        self.df = self.df[self.df["regiao"] == self.regiao]
        return self

    def total(self):
        return self.df["valor"].sum()

# Uso:
rel = RelatorioVendas("vendas.csv", "Sul")
total = rel.carregar().filtrar_regiao().total()
print(f"Total Sul: R$ {total:,.2f}")

Métodos especiais (dunder)

class Pedido:
    def __init__(self, id_pedido, cliente, valor):
        self.id_pedido = id_pedido
        self.cliente = cliente
        self.valor = valor

    def __repr__(self):
        return f"Pedido(id={self.id_pedido}, cliente='{self.cliente}', valor={self.valor})"

    def __str__(self):
        return f"Pedido #{self.id_pedido} — {self.cliente}: R$ {self.valor:,.2f}"

    def __eq__(self, outro):
        return self.id_pedido == outro.id_pedido

    def __lt__(self, outro):
        return self.valor < outro.valor

pedidos = [Pedido(3, "Carlos", 800), Pedido(1, "Ana", 1500), Pedido(2, "Bruno", 300)]
for p in sorted(pedidos):  # usa __lt__
    print(p)

Encapsulamento e propriedades

class Contrato:
    def __init__(self, valor):
        self._valor = valor   # convenção: _prefixo = "interno"

    @property
    def valor(self):
        return self._valor

    @valor.setter
    def valor(self, novo):
        if novo < 0:
            raise ValueError("Valor do contrato não pode ser negativo")
        self._valor = novo

    @property
    def valor_com_imposto(self):
        return self._valor * 1.15   # propriedade calculada, sem setter

c = Contrato(10000)
print(c.valor_com_imposto)  # 11500.0
c.valor = -100              # raises ValueError

Herança

class Conector:
    def __init__(self, nome):
        self.nome = nome

    def conectar(self):
        raise NotImplementedError("Subclasse deve implementar conectar()")

    def extrair(self, query):
        raise NotImplementedError

class ConectorSQL(Conector):
    def __init__(self, url):
        super().__init__("SQL")
        from sqlalchemy import create_engine
        self.engine = create_engine(url)

    def conectar(self):
        print(f"Conectado ao banco SQL: {self.engine.url}")
        return self

    def extrair(self, query):
        import pandas as pd
        return pd.read_sql(query, self.engine)

class ConectorCSV(Conector):
    def __init__(self, pasta):
        super().__init__("CSV")
        self.pasta = pasta

    def conectar(self):
        print(f"Pasta CSV: {self.pasta}")
        return self

    def extrair(self, arquivo):
        import pandas as pd
        return pd.read_csv(f"{self.pasta}/{arquivo}")

Quando NÃO usar OOP

Para scripts de análise de 50-100 linhas que rodam uma vez, funções simples são mais diretas. OOP compensa quando: o mesmo conceito aparece em vários lugares do código, o projeto tem mais de uma pessoa, ou você precisa testar partes isoladas. Não force OOP por "parecer profissional" — o melhor código é o mais simples que resolve o problema.

Perguntas frequentes

@staticmethod vs @classmethod: qual a diferença?

@staticmethod é uma função que vive dentro da classe por organização, mas não acessa nem self (instância) nem cls (classe). @classmethod recebe cls como primeiro argumento — útil para factory methods como Pedido.from_dict(dados) que criam instâncias de formas alternativas.

Devo usar dataclasses em vez de classes normais?

Para classes que são principalmente contêineres de dados (sem muita lógica), sim: @dataclass gera automaticamente __init__, __repr__ e __eq__. Use from dataclasses import dataclass e adicione @dataclass antes da definição da classe.