Tutorial Completo Python 🐍

Dominando
POO em Python

Classes, objetos, herança, encapsulamento, polimorfismo e composição — do zero ao exemplo real.

12 Módulos
4 Pilares POO
3.x Versão Python
classes herança encapsulamento polimorfismo composição @property dunder methods
01

🧠 O que é Programação Orientada a Objetos?

POO é um paradigma onde você organiza o código em objetos, que representam coisas do mundo real.

🚗 Carro

Atributos: cor, modelo
Ações: acelerar, frear

👤 Usuário

Atributos: nome, email
Ações: fazer login

🏦 Conta

Atributos: saldo, titular
Ações: depositar, sacar

🐾 Animal

Atributos: nome, espécie
Ações: falar, mover

Classe = molde Objeto = instância Atributo = dado Método = ação
02

🧱 Classe, Objeto, Atributos e Métodos

Classe — um molde para criar objetos:

class Pessoa:
    pass               # classe vazia por enquanto

Objeto — uma instância criada a partir da classe:

p1 = Pessoa()          # cria um objeto do tipo Pessoa
p2 = Pessoa()          # outro objeto, independente

Atributos e Métodos juntos:

estrutura completa
class Pessoa:
    def __init__(self, nome, idade):
        self.nome  = nome       # atributo
        self.idade = idade      # atributo

    def apresentar(self):           # método
        print(f"Olá, meu nome é {self.nome}")
criando e usando o objeto
p1 = Pessoa("Igor", 25)
p1.apresentar()         # Olá, meu nome é Igor
print(p1.nome)           # Igor
print(p1.idade)          # 25
03

🔹 __init__ e self

__init__ é o construtor — executado automaticamente ao criar um objeto:

class Carro:
    def __init__(self, modelo, cor):
        self.modelo = modelo
        self.cor    = cor

meu_carro = Carro("Fusca", "azul")
# __init__ roda aqui automaticamente ↑

self representa o próprio objeto — permite acessar atributos e métodos da instância:

class Carro:
    def __init__(self, modelo):
        self.modelo = modelo       # "self" = este objeto

    def descrever(self):
        print(self.modelo)          # acessa atributo da instância
💡
self é apenas uma convenção de nome — poderia ser qualquer nome, mas sempre deve ser o primeiro parâmetro dos métodos de instância.
04

🔹 Encapsulamento

Serve para proteger e controlar o acesso aos dados de um objeto.

self.nome

🟢 Público — acessível de qualquer lugar

self._nome

🟡 Protegido — convenção: não usar fora da classe

self.__nome

🔴 Privado — name mangling, não acessível diretamente

exemplo com atributo privado
class Conta:
    def __init__(self, saldo):
        self.__saldo = saldo      # privado

    def ver_saldo(self):
        return self.__saldo       # acesso controlado via método

conta = Conta(1000)
print(conta.ver_saldo())    # ✅ 1000
# print(conta.__saldo)      # ❌ AttributeError
💡
Python não bloqueia acesso privado com força bruta. O __nome vira _Classe__nome internamente (name mangling) — é uma proteção por convenção, não por segurança absoluta.
05

🔹 Getters, Setters e @property

Controlam o acesso e modificação de atributos protegidos.

Forma manual (getters/setters):

class Pessoa:
    def __init__(self, nome):
        self._nome = nome

    def get_nome(self):           # getter
        return self._nome

    def set_nome(self, nome):     # setter
        self._nome = nome

✨ Forma moderna com @property:

pythônico
class Pessoa:
    def __init__(self, nome):
        self._nome = nome

    @property
    def nome(self):               # getter
        return self._nome

    @nome.setter
    def nome(self, valor):        # setter
        if len(valor) > 0:
            self._nome = valor

p = Pessoa("Igor")
print(p.nome)               # Igor — chama getter automaticamente
p.nome = "Carlos"           # chama setter automaticamente
💡
Com @property você acessa como atributo (p.nome), mas por baixo dos panos roda o método. É o jeito mais pythônico de criar getters/setters.
06

🔹 Herança

Permite que uma classe herde atributos e métodos de outra — reutilização de código.

classe pai → classe filha
class Animal:                    # classe pai (base)
    def __init__(self, nome):
        self.nome = nome

    def falar(self):
        print("Som genérico")

class Cachorro(Animal):          # herda de Animal
    def falar(self):               # sobrescreve o método
        print(f"{self.nome} diz: Au au!")

class Gato(Animal):
    def falar(self):
        print(f"{self.nome} diz: Miau!")

rex  = Cachorro("Rex")
bilu = Gato("Bilu")
rex.falar()    # Rex diz: Au au!
bilu.falar()   # Bilu diz: Miau!

Usando super() para chamar o constructor do pai:

class ContaCorrente(Conta):
    def __init__(self, titular, saldo, limite):
        super().__init__(titular, saldo)   # chama __init__ do pai
        self.limite = limite
07

🔹 Polimorfismo

Mesma chamada de método — comportamentos diferentes dependendo do objeto.

animais = [Cachorro("Rex"), Gato("Bilu"), Animal("Peixe")]

for animal in animais:
    animal.falar()
# Rex diz: Au au!
# Bilu diz: Miau!
# Som genérico
🧠
O polimorfismo permite escrever código genérico que funciona com qualquer tipo de objeto, desde que ele tenha o mesmo método. Isso é o coração da flexibilidade em POO.
08

🔹 Abstração

Oculta a complexidade e expõe apenas o necessário. Em Python usamos classes abstratas com o módulo abc.

from abc import ABC, abstractmethod

class Forma(ABC):               # classe abstrata
    @abstractmethod
    def area(self):               # método abstrato
        pass

class Circulo(Forma):
    def __init__(self, raio):
        self.raio = raio

    def area(self):               # obrigado a implementar
        return 3.14 * self.raio ** 2

class Retangulo(Forma):
    def __init__(self, l, a):
        self.l, self.a = l, a

    def area(self):
        return self.l * self.a
⚠️
Você não pode instanciar uma classe abstrata diretamente. Tentar fazer Forma() vai lançar TypeError. As subclasses são obrigadas a implementar todos os métodos abstratos.
09

🔹 Métodos especiais (dunder methods)

Métodos com duplo underscore — chamados automaticamente pelo Python em situações específicas.

exemplo: __str__ e __repr__
class Pessoa:
    def __init__(self, nome, idade):
        self.nome  = nome
        self.idade = idade

    def __str__(self):                      # para print()
        return f"{self.nome} ({self.idade} anos)"

    def __repr__(self):                     # representação técnica
        return f"Pessoa(nome='{self.nome}', idade={self.idade})"

    def __len__(self):                      # para len()
        return len(self.nome)

p = Pessoa("Igor", 25)
print(p)           # Igor (25 anos)
print(repr(p))      # Pessoa(nome='Igor', idade=25)
print(len(p))       # 4

Principais dunder methods:

MétodoQuando é chamado
__init__Ao criar o objeto (Classe())
__str__Ao usar print(obj) ou str(obj)
__repr__Representação técnica do objeto
__len__Ao usar len(obj)
__add__Ao usar obj1 + obj2
__eq__Ao usar obj1 == obj2
__lt__ / __gt__Comparações < e >
__del__Ao deletar o objeto
10

🔹 Métodos de instância, classe e estáticos

class Exemplo:

    contador = 0                        # atributo de classe

    def metodo_instancia(self):          # acessa self
        print("instância")

    @classmethod
    def metodo_classe(cls):              # acessa a classe (cls)
        cls.contador += 1
        print(f"Total criados: {cls.contador}")

    @staticmethod
    def metodo_estatico():              # não acessa self nem cls
        print("utilitário independente")
def met(self)

Método de instância — acessa self. O mais comum.

@classmethod

Método de classe — acessa cls. Útil para factories.

@staticmethod

Método estático — não acessa instância nem classe. Funções utilitárias.

11

🔹 Composição

Em vez de herdar, você usa objetos dentro de outros objetos. Muitas vezes é preferível à herança.

herança vs composição
# herança (Carro É UM veículo)
class Carro(Veiculo): pass

# composição (Carro TEM UM motor)
class Motor:
    def ligar(self):
        print("Motor ligado")

class Carro:
    def __init__(self):
        self.motor = Motor()   # composição

    def ligar(self):
        self.motor.ligar()   # delega ao motor

meu_carro = Carro()
meu_carro.ligar()          # Motor ligado
💡
Regra prática: prefira composição quando a relação é "tem um" (Carro tem motor). Use herança quando é "é um" (ContaCorrente é uma Conta). Composição tende a gerar código mais flexível.
12

🚀 Exemplo completo + Boas práticas

mini sistema bancário
class Conta:
    def __init__(self, titular, saldo=0):
        self.titular  = titular
        self.__saldo  = saldo       # privado

    def depositar(self, valor):
        self.__saldo += valor

    def sacar(self, valor):
        if valor <= self.__saldo:
            self.__saldo -= valor
        else:
            print("Saldo insuficiente")

    @property
    def saldo(self):
        return self.__saldo

    def __str__(self):
        return f"Conta({self.titular}) — R$ {self.__saldo:.2f}"


class ContaCorrente(Conta):       # herança
    def sacar(self, valor):
        print("Sacando da conta corrente…")
        super().sacar(valor)         # chama método do pai


# uso
conta = ContaCorrente("Igor", 100)
conta.depositar(50)
conta.sacar(30)
print(conta)                       # Conta(Igor) — R$ 120.00
print(conta.saldo)                 # 120

✔ Boas práticas:

❌ Erros comuns:

# ❌ Esquecer o self no método
def metodo():                   # falta self
    pass

# ❌ Atributo de classe ao invés de instância
class Pessoa:
    nome = "Igor"               # compartilhado entre todas as instâncias

# ✅ Correto: atributo de instância no __init__
class Pessoa:
    def __init__(self, nome):
        self.nome = nome          # cada objeto tem o seu

# ❌ Acessar atributo privado diretamente
obj.__saldo                      # AttributeError
# ✅ Use o @property ou método público
🚀
Caminho recomendado: Classes + objetos → __init__ + self → Métodos → Encapsulamento → Herança → Polimorfismo → Composição