Orientação a Objetos (OOP) em Python — classes, métodos e herança

Uma guia extensa e prática para dominar os conceitos essenciais de OOP em Python, com exemplos reais, boas práticas, armadilhas comuns e ideias inesperadas para aplicar em projetos.


Por que OOP?

A programação orientada a objetos ajuda a organizar código modelando entidades do mundo real (pessoas, pedidos, contas) como objetos que têm estado (atributos) e comportamento (métodos). OOP facilita legibilidade, manutenção, reaproveitamento e modelagem de regras de negócio.


Visão geral rápida (terminologia)

  • Classe: projeto/receita (tipo) — descreve propriedades e comportamentos.

  • Objeto (instância): uma ocorrência concreta da classe.

  • Atributo: dado associado à instância ou à classe.

  • Método: função definida na classe (comportamento).

  • Herança: criar classe filha que reaproveita/completa comportamento da classe pai.

  • Encapsulamento: esconder detalhes internos (convenção _attr__attr).

  • Polimorfismo: objetos de tipos diferentes respondendo à mesma interface.


1) Criando uma classe simples (exemplo prático)

class Student:
    school = "Escola Exemplo"   # atributo de classe (compartilhado)

    def __init__(self, name, grades=None):
        self.name = name
        # cuidado com valores mutáveis como default -> usar None e criar novo objeto aqui
        self.grades = list(grades) if grades is not None else []

    def add_grade(self, value):
        if not (0 <= value <= 10):
            raise ValueError("Nota inválida")
        self.grades.append(value)

    def average(self):
        return sum(self.grades) / len(self.grades) if self.grades else 0.0

    @property
    def status(self):
        return "Aprovado" if self.average() >= 7 else "Reprovado"

    def __repr__(self):
        return f"Student(name={self.name!r}, avg={self.average():.2f})"

Pontos importantes:

  • __init__ inicializa o estado.

  • @property cria um atributo calculado (status) sem chamar método.

  • __repr__ facilita debugging/printing.


2) Dataclasses — forma moderna e prática

Python oferece dataclasses para modelos de dados concisos:

from dataclasses import dataclass, field

@dataclass
class StudentDC:
    name: str
    grades: list[float] = field(default_factory=list)

    def average(self):
        return sum(self.grades) / len(self.grades) if self.grades else 0.0

Use dataclasses quando o objetivo é representar dados; elas geram __init____repr____eq__ automaticamente.

Sugestão inesperada: usar frozen=True para objetos imutáveis (útil em caches ou IDs).


3) Métodos de classe e estáticos

  • @classmethod: método ligado à classe, recebe cls — bom para factory methods.

  • @staticmethod: método sem self nem cls — utilitário relacionado à classe.

class Student:
    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], data.get("grades", []))

    @staticmethod
    def valid_grade(g):
        return 0 <= g <= 10

4) Herança e sobrescrita (override)

Crie hierarquias para especializar comportamento:

class Person:
    def __init__(self, name):
        self.name = name

    def role(self):
        return "Pessoa"

class Teacher(Person):
    def __init__(self, name, subjects):
        super().__init__(name)
        self.subjects = subjects

    def role(self):
        return "Professor"

Dica: use super() para chamar métodos do pai (compatível com múltipla herança cooperativa).


5) Polimorfismo: mesma interface, várias implementações

Você pode passar objetos diferentes para a mesma função:

def print_role(entity):
    print(entity.role())

print_role(Teacher("Ana", ["Matemática"]))  # "Professor"
print_role(Student("João"))                 # "Pessoa" ou outro role

6) Múltipla herança e Mixins

Mixins são classes pequenas que adicionam comportamento:

class JsonSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__, ensure_ascii=False)

class SerializableStudent(JsonSerializableMixin, Student):
    pass

Atenção à MRO (Method Resolution Order): ordem em que Python procura métodos em classes. Para heranças complexas, entenda class.__mro__.


7) Composição vs Herança (regra prática)

Prefira composição quando possível: “um objeto tem outro” em vez de “é um”. Herança cria acoplamento forte.

Exemplo (composição):

class GradeBook:
    def __init__(self):
        self.students = []

    def add_student(self, student):
        self.students.append(student)

Uso inesperado: combine composição + injeção de dependência para facilitar testes (por ex., passar repositório de dados como argumento).


8) Abstrações e ABCs (Abstract Base Classes)

Use abc para definir interfaces formais:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): import math; return math.pi * self.r**2

9) Descritores — validação reutilizável

Descritores permitem controlar acesso a atributos:

class NonNegative:
    def __init__(self, name):
        self.name = name
    def __get__(self, obj, objtype):
        return obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError("Negativo não permitido")
        obj.__dict__[self.name] = value

class Account:
    balance = NonNegative("balance")
    def __init__(self, balance=0):
        self.balance = balance

Use para validação centralizada (elegante e performática).


10) Dunder methods úteis (cheat-sheet)

Estes métodos controlam comportamento “mágico”:

MétodoUso
__init__inicialização
__repr__represent. oficial (debug)
__str__represent. legível
__eq____lt__comparações
__hash__para usar em sets/dicts
__len____getitem____iter__container protocol
__call__tornar instância chamável
__enter____exit__context manager

Exemplo __enter__/__exit__:

import time
class Timer:
    def __enter__(self):
        self.start = time.perf_counter(); return self
    def __exit__(self, exc_type, exc, tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")

11) Boas práticas e armadilhas (prático)

  • Nunca use valores mutáveis como default: use None + cria novo objeto.

  • Prefira composição ao invés de herança quando não houver relação “é-um”.

  • Evite God objects (classe que faz tudo).

  • Use __slots__ para reduzir memória se tiver milhares de instâncias.

  • Para identidade vs igualdade: implemente __eq__ e __hash__ com cuidado.

  • Use dataclasses para modelos imutáveis/imutáveis (com frozen=True).

  • Teste comportamento com unit tests; classes são fáceis de isolar.


12) Exemplo real: refatorando o Sistema de Notas (do projeto anterior) em OOP

from dataclasses import dataclass, field, asdict
import json

@dataclass
class Student:
    id: int
    name: str
    grades: list[float] = field(default_factory=list)

    def add_grade(self, grade: float):
        if not (0 <= grade <= 10):
            raise ValueError("Nota inválida")
        self.grades.append(grade)

    def average(self) -> float:
        return sum(self.grades) / len(self.grades) if self.grades else 0.0

    def status(self) -> str:
        return "Aprovado" if self.average() >= 7 else "Reprovado"

class GradeSystem:
    def __init__(self, storage_path="alunos.json"):
        self.storage_path = storage_path
        self.students: dict[int, Student] = {}
        self._load()

    def add_student(self, student: Student):
        self.students[student.id] = student
        self._save()

    def get_student(self, student_id):
        return self.students.get(student_id)

    def _save(self):
        with open(self.storage_path, "w", encoding="utf-8") as f:
            json.dump([asdict(s) for s in self.students.values()], f, ensure_ascii=False, indent=2)

    def _load(self):
        try:
            with open(self.storage_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            for item in data:
                s = Student(**item)
                self.students[s.id] = s
        except FileNotFoundError:
            pass

Vantagens: responsabilidade separada (Student apenas dados/comportamento; GradeSystem persiste e gerencia).


13) Padrões de projeto úteis (aplicações reais)

  • Factory Method: criar objetos via métodos/fábricas.

  • Strategy: trocar algoritmo dinamicamente (ex.: diferentes cálculos de média).

  • Repository: abstrair persistência (arquivo, DB).

  • Decorator: adicionar comportamento sem alterar classe original.

Exemplo Strategy (cálculo de média):

class AvgStrategy:
    def compute(self, grades): pass

class SimpleAvg(AvgStrategy):
    def compute(self, grades): return sum(grades)/len(grades)

class WeightedAvg(AvgStrategy):
    def __init__(self, weights): self.weights = weights
    def compute(self, grades):
        return sum(g*w for g,w in zip(grades, self.weights))/sum(self.weights)

14) Tipagem e Protocols (PEP 544)

Use typing para documentação e segurança:

from typing import Protocol

class HasAverage(Protocol):
    def average(self) -> float: ...

def print_status(obj: HasAverage):
    print(obj.average())

Protocols permitem duck typing estrutural sem herança direta.


15) Performance e memória

  • __slots__ reduz overhead por instância (sem __dict__).

  • Evite criar objetos gigantes desnecessariamente; reutilize quando apropriado.

  • Para coleções grandes, prefira estruturas especializadas (arraynumpydatabases).


16) Testando classes — exemplo com pytest

# test_student.py
import pytest
from mymodule import Student

def test_average_and_status():
    s = Student("João")
    s.add_grade(8)
    s.add_grade(6)
    assert abs(s.average() - 7.0) < 1e-6
    assert s.status() == "Aprovado"

Testes simples evitam regressões ao refatorar classes.


17) Ferramentas e bibliotecas complementares

  • attrs (mais controle que dataclasses)

  • pydantic (validação + parsing, ótimo para APIs)

  • marshmallowdataclasses-json (serialização)

  • pytesthypothesis (testes)

Solução que talvez você não espere: usar pydantic para modelos de domínio e validação forte em vez de escrita manual de validadores — excelente para serviços web.


18) Anti-padrões comuns

  • Herança profunda (muitas camadas) → difícil de manter.

  • Getters/Setters estilo Java (Python privilegia propriedades).

  • Mutabilidade descontrolada (complicações em caches e comparações).

  • Usar herança só para reutilizar código (prefira funções utilitárias ou mixins).


19) Exercícios práticos (mão na massa)

  1. Transforme o sistema de notas procedural em classes (Student, Course, GradeBook) e persista em JSON.

  2. Implemente um Mixin CSVExportMixin que adiciona método to_csv() a qualquer classe que tenha __dict__.

  3. Crie um plugin system usando classes e registro automático via decorator (útil para comandos CLI).

  4. Implemente imutabilidade com dataclasses congeladas para um modelo “Contrato” e mostre como atualizar com replace().


20) Próximos passos recomendados

  • Estude Design Patterns (Factory, Strategy, Observer).

  • Aprenda Metaclasses (quando realmente necessário).

  • Use Domain-Driven Design (DDD) para modelos mais complexos.

  • Aprofunde em typing e Protocols.

  • Experimente Pydantic se for trabalhar com APIs.


Conclusão (prática)

OOP em Python é simples de começar, mas profundo o suficiente para modelar sistemas reais. Comece com classes pequenas e coesas, prefira composição quando fizer sentido, escreva testes e não esqueça: refatorar para clareza é mais valioso que clever code.

Comentários

Postagens mais visitadas deste blog

Manipulação de Arquivos no C#: Como Ler, Escrever e Trabalhar com Arquivos de Forma Simples

Gerando Relatórios em PDF com Python (ReportLab e FPDF)

C# e Banco de Dados: Como Conectar e Realizar Operações com SQL Server