Partilhar: WhatsApp
aulify · Sebenta
UC · Unidade de Competência · UC00608

Programação orientada a objectos

Sebenta · Classes, herança, polimorfismo
50h · 4.5 pontos crédito Curso: T. Desenv. Software ↗ Referencial oficial SNQ
Índice

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

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:

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