Programação orientada a objectos
Apresentação
UC00608 (50h · 4,5 pts) introduz o paradigma orientado a objectos (OO) usando Python. Aborda classes, atributos, métodos, encapsulamento, herança, polimorfismo, classes abstractas e composição vs herança.
OO não é universalmente "melhor" que estruturada. É mais adequado em certos problemas: simulações, GUIs, modelos de domínio, jogos.
Do procedural ao OO
Problema motivador
Programa que gere alunos e calcula médias. Versão procedural:
def criar_aluno(nome, idade):
return {"nome": nome, "idade": idade, "notas": []}
def adicionar_nota(aluno, nota):
aluno["notas"].append(nota)
def media(aluno):
n = aluno["notas"]
return sum(n) / len(n) if n else 0
ana = criar_aluno("Ana", 17)
adicionar_nota(ana, 15)
print(media(ana))
Problemas:
- Funções soltas, fácil esquecer qual aplicar a quê.
- Estrutura interna (dict) acessível: alguém pode fazer ana["notas"] = "abc".
- Difícil estender (e se quisermos sub-tipo de aluno?).
Versão OO:
class Aluno:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
self.notas = []
def adicionar_nota(self, nota):
self.notas.append(nota)
def media(self):
return sum(self.notas) / len(self.notas) if self.notas else 0
ana = Aluno("Ana", 17)
ana.adicionar_nota(15)
print(ana.media())
Vantagens:
- Tudo o que é de Aluno está junto.
- Interface clara: ana.media() em vez de media(ana).
- Extensível por herança.
Vocabulário
- Classe — molde, definição.
- Objecto / instância — exemplar concreto criado da classe.
- Atributo — propriedade (variável) da instância.
- Método — função associada à classe.
- Construtor —
__init__()em Python.
Classes em Python
Sintaxe básica
class Aluno:
# Atributo de classe (partilhado por todas as instâncias)
escola = "Aulify"
def __init__(self, nome, idade):
# Atributos de instância
self.nome = nome
self.idade = idade
def saudar(self):
return f"Olá, sou {self.nome}"
def __repr__(self):
return f"Aluno({self.nome!r}, {self.idade})"
self é a instância actual; primeiro argumento de todos os métodos de instância. Não se passa explicitamente ao chamar — Python fá-lo automaticamente.
ana = Aluno("Ana", 17)
print(ana.saudar()) # "Olá, sou Ana"
print(ana.escola) # "Aulify" (atributo de classe)
print(ana) # Aluno('Ana', 17) — usa __repr__
Métodos especiais (dunder)
Métodos com __nome__ controlam comportamento integrado:
| Método | Quando é chamado |
|---|---|
__init__ |
Criação (Aluno(...)) |
__repr__ |
repr(obj) ou consola |
__str__ |
str(obj) ou print(obj) |
__eq__ |
obj1 == obj2 |
__lt__ |
obj1 < obj2 |
__len__ |
len(obj) |
__getitem__ |
obj[i] |
__iter__ |
for x in obj: |
Exemplo de classe rica:
class Vetor:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vetor({self.x}, {self.y})"
def __add__(self, outro):
return Vetor(self.x + outro.x, self.y + outro.y)
def __eq__(self, outro):
return self.x == outro.x and self.y == outro.y
def __abs__(self):
return (self.x**2 + self.y**2) ** 0.5
v1 = Vetor(3, 4)
v2 = Vetor(1, 2)
print(v1 + v2) # Vetor(4, 6)
print(abs(v1)) # 5.0
print(v1 == Vetor(3, 4)) # True
Encapsulamento
Esconder estado interno e expor interface controlada.
Convenção de privacidade
Python não força privacidade (ao contrário de Java/C#). Usa convenção:
nome— público._nome— convencionalmente privado (não tocar de fora).__nome— name mangling (_Classe__nome); raro.
class ContaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self._saldo = saldo_inicial # "privado"
def depositar(self, valor):
if valor <= 0:
raise ValueError("Valor tem de ser positivo")
self._saldo += valor
def levantar(self, valor):
if valor > self._saldo:
raise ValueError("Saldo insuficiente")
self._saldo -= valor
def saldo(self):
return self._saldo
Properties
Pattern para expor atributos com lógica:
class Temperatura:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, valor):
if valor < -273.15:
raise ValueError("Impossível abaixo do zero absoluto")
self._celsius = valor
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
t = Temperatura(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = 30
# t.celsius = -300 # ValueError
fahrenheit é só leitura (não tem setter). Mudanças nele teriam de passar por celsius.
Herança
Sintaxe
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def saudar(self):
return f"Olá, sou {self.nome}"
class Aluno(Pessoa): # herda de Pessoa
def __init__(self, nome, idade, numero):
super().__init__(nome, idade)
self.numero = numero
self.notas = []
def adicionar_nota(self, nota):
self.notas.append(nota)
super() chama o construtor da classe pai para reaproveitar inicialização.
ana = Aluno("Ana", 17, 42)
print(ana.saudar()) # herda
print(ana.numero) # próprio
print(isinstance(ana, Pessoa)) # True
print(isinstance(ana, Aluno)) # True
Polimorfismo
Subclasses podem redefinir métodos do pai:
class Animal:
def som(self):
return "..."
class Cao(Animal):
def som(self):
return "Au au!"
class Gato(Animal):
def som(self):
return "Miau!"
class Vaca(Animal):
def som(self):
return "Muuu!"
# Polimorfismo: mesmo código funciona para tipos diferentes
def fazer_som(animal):
print(animal.som())
for a in [Cao(), Gato(), Vaca()]:
fazer_som(a)
Cada subclasse responde à mesma interface (som()) de forma própria.
Herança múltipla
Python permite herdar de várias classes:
class Voador:
def voar(self):
return "Voando!"
class Nadador:
def nadar(self):
return "Nadando!"
class Pato(Voador, Nadador):
pass
donald = Pato()
print(donald.voar()) # "Voando!"
print(donald.nadar()) # "Nadando!"
Cuidados: - MRO (Method Resolution Order) — Python resolve via algoritmo C3. - Diamond problem — duas classes herdam de comum; cuidado em métodos com mesmo nome. - Mixin — pattern de classe que só adiciona comportamento (sem estado próprio).
Classes abstractas
Não devem ser instanciadas; servem só para herdar.
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimetro(self):
pass
def descrever(self):
# Método concreto, pode ser usado por subclasses
return f"Área {self.area():.2f}, perímetro {self.perimetro():.2f}"
class Rectangulo(Figura):
def __init__(self, largura, altura):
self.largura = largura
self.altura = altura
def area(self):
return self.largura * self.altura
def perimetro(self):
return 2 * (self.largura + self.altura)
class Circulo(Figura):
def __init__(self, raio):
self.raio = raio
def area(self):
return 3.14159 * self.raio ** 2
def perimetro(self):
return 2 * 3.14159 * self.raio
# Figura() # TypeError: abstract class
r = Rectangulo(3, 4)
print(r.descrever()) # Área 12.00, perímetro 14.00
Composição vs herança
Herança — relação "é-um". Aluno é Pessoa. Composição — relação "tem-um". Carro tem Motor.
class Motor:
def __init__(self, cilindrada):
self.cilindrada = cilindrada
self.ligado = False
def ligar(self):
self.ligado = True
return f"Motor {self.cilindrada}cc a trabalhar"
class Carro:
def __init__(self, marca, modelo, cilindrada):
self.marca = marca
self.modelo = modelo
self.motor = Motor(cilindrada) # composição
def arrancar(self):
return self.motor.ligar()
c = Carro("Toyota", "Corolla", 1800)
print(c.arrancar())
Princípio: "Prefere composição a herança." A herança acopla fortemente — mudar a classe-pai pode partir filhas. Composição permite trocar partes sem mexer no todo.
Padrões e boas práticas
Single Responsibility Principle (SRP)
Cada classe deve ter uma razão para mudar. Se uma classe gere alunos e também grava em ficheiro e envia email, está a fazer demais — separar em três.
Open/Closed Principle (OCP)
Classes devem ser abertas a extensão (via herança / polimorfismo) mas fechadas a modificação (não estar a mudar a classe original cada vez que aparece um caso novo).
Don't Repeat Yourself (DRY)
Código duplicado entre classes vai para a classe-pai ou para utilitário comum.
Liskov Substitution Principle (LSP)
Subclasses devem poder substituir a classe-pai sem partir o programa. Se Quadrado herda de Rectangulo mas não respeita o contrato (mudar largura também muda altura), está a violar LSP.
Quando NÃO usar OO
OO custa complexidade. Em scripts curtos é frequentemente desnecessário.
# Errado — OO sem razão
class Calculadora:
def somar(self, a, b):
return a + b
c = Calculadora()
c.somar(2, 3)
# Certo — função basta
def somar(a, b):
return a + b
somar(2, 3)
Regra: objectos quando há estado que muda ao longo do tempo. Funções quando só transformam input em output sem memória.
Apêndices
A · Cheatsheet
class Nome(ClassePai):
atributo_classe = "valor"
def __init__(self, a, b):
self.a = a
self.b = b
def metodo(self, x):
return self.a + x
@property
def computed(self):
return self.a * 2
@classmethod
def factory(cls, dado):
return cls(dado, dado * 2)
@staticmethod
def util(x):
return x ** 2
B · Glossário
Abstract. Classe que não pode ser instanciada. Encapsulamento. Esconder estado interno. Herança. Reaproveitar de classe-pai. Polimorfismo. Mesma interface, comportamento diferente. self. Referência à instância actual. super(). Acede à classe-pai.
C · Recursos
- Real Python · OO Programming · realpython.com/python3-object-oriented-programming/
- Fluent Python (livro) · Luciano Ramalho.
- Refactoring Guru · Design Patterns · refactoring.guru.