Orientação a Objetos (OOP) em Python — classes, métodos e herança
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.@propertycria 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, recebecls— bom para factory methods.@staticmethod: método semselfnemcls— 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étodo | Uso |
|---|---|
__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
dataclassespara modelos imutáveis/imutáveis (comfrozen=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 (
array,numpy,databases).
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)
marshmallow, dataclasses-json (serialização)
pytest, hypothesis (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)
Transforme o sistema de notas procedural em classes (Student, Course, GradeBook) e persista em JSON.
Implemente um Mixin
CSVExportMixinque adiciona métodoto_csv()a qualquer classe que tenha__dict__.Crie um plugin system usando classes e registro automático via decorator (útil para comandos CLI).
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
Postar um comentário