Classes e Objetos (P.O.O)

Classes e Objetos (Programação Orientada a Objetos - POO)

Python é uma linguagem orientada a objetos, o que significa que podemos definir classes e criar objetos. 

Todos os tipos são objetos. Diferente do tipo primitivo (em outras linguagens), onde, nos tipos primitivos, manipulamos os valores diretamente, Porém, em Python, inteiros (e outros tipos primitivos) são implementados como objetos, com métodos associados, embora o comportamento do tipo em si seja similar ao de um tipo primitivo. 

Classes embutidas no núcleo da linguagem (como int, float, str, tuple) geralmente são nomeadas em letras minúsculas. Isso foi uma decisão de design para diferenciá-las visualmente das classes definidas pelo usuário ou de bibliotecas, que geralmente seguem o padrão CamelCase (primeira letra maiúscula).


Classes

A Classe é a estrutura/projeto de um objeto.

O método __init__ é o construtor da classe e é chamado quando criamos um objeto. Métodos sempre recebem como primeiro parametro self (diferente das funções). 

Todo atributo definido dentro do construtor pertence ao objeto e não apenas ao escopo do método.

O uso de self é obrigatório em métodos de instância em Python. Ele deve ser explicitamente definido como o primeiro parâmetro do método, mas não é necessário chamá-lo ao invocar o método pois python faz isso automaticamente. Você pode usar qualquer nome no lugar de self, como this, obj, etc ...

Atributos de instância são normalmente definidos usando a palavra-chave self, que refere-se à instância atual da classe e pode ser acessado em qualquer lugar da classe.

Atributos de classe são compartilhados por todas as instâncias da classe. São criados fora do método __init__ e acessados usando o nome da classe ou self.

Métodos de classe são métodos que pertencem à classe e não a uma instância específica da classe. Eles podem ser chamados diretamente na classe e têm acesso à própria classe, mas não aos dados de instância. O método de classe recebe como primeiro parâmetro o próprio objeto da classe, que é chamado de cls. São definidos com decorador @classmethod.

Toda classe criada pelo programador exibe a palavra __main__: 

    resultado ao conferir o tipo: <class '__main__.Pessoa'>

 

Aspecto Atributo de Classe Atributo de Instância
Definido em Corpo da classe Dentro de métodos (geralmente __init__)
Escopo Compartilhado por todas as instâncias Exclusivo para cada instância
Acesso Classe.atributo ou instancia.atributo Apenas instancia.atributo
Valor Comum para todas as instâncias Pode variar entre instâncias
Modificação Afeta todas as instâncias Afeta apenas a instância específica
classes.py
class Pessoa:

    # Atributo de classe e pode ser acessado por todas as instancias, tendo o mesmo valor.
    especie = "Humano"

    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
        self.seguidores_instagram = 0
        self.seguindo_instagram = 0
    
    # Self diferencia qual atributo deve ser atualizado o da minha classe ou o da recebida
    def seguir(self, pessoa):
        self.seguindo_instagram += 1
        pessoa.seguidores_instagram += 1

    def saudacao(self):
        return f"Olá, meu nome é {self.nome} e tenho {self.idade} anos."
    
    # metodo especial para imprimir o objeto
    def __str__(self):
        return f"{self.nome} - {self.idade} anos - {self.seguidores_instagram}"

# Cria um objeto/instancia da classe Pessoa
pessoa1 = Pessoa("Carlos", 30)
print(pessoa1.saudacao())
pessoa2 = Pessoa("João",14)

pessoa1.seguir(pessoa2)
print(pessoa2.seguidores_instagram)
print(pessoa1.seguindo_instagram)

# Verifica o tipo
print(type(pessoa1)) # Saída: <class '__main__.Pessoa'>

# utiliza o metodo especial __str__
print(pessoa1) # Saída: Carlos - 30 anos - ...

# Retorna todos atributos de um objeto em forma de dicionário 
print(vars(pessoa1))

Nesse caso nome só existe dentro do escopo do método, ao contrário de nome.self, que é um atributo de instância.

atributos de classe e instancia
class Game:
    pass  # Classe sem atributos ou métodos predefinidos

game1 = Game()
game1.name_game = "Mario Bros"  # Criando um atributo dinamicamente

game2 = Game()
game2.name_game = "Pac Man"  # Criando outro atributo dinamicamente

print(f"\nGame 1: {game1.name_game}")
print(f"\nGame 2: {game2.name_game}")

Nesse exemplo as instancias game1 e game2 geram dinamicamente o atributo name_game. A unica forma de alterar o atributo de classe é Game.name_game = "Nome".

metodos_de_classe.py
class Pessoa:
    # Variável de classe (compartilhada por todas as instâncias)
    quantidade_pessoas = 0
    
    def __init__(self, nome):
        self.nome = nome
        Pessoa.quantidade_pessoas += 1  # Cada instância criada incrementa a quantidade_pessoas

    # Método de classe
    @classmethod
    def get_quantidade_pessoas(cls):
        return cls.quantidade_pessoas

# Criando instâncias
pessoa1 = Pessoa("João")
pessoa2 = Pessoa("Maria")

# Chamando o método de classe diretamente na classe
print(Pessoa.get_quantidade_pessoas())  # Saída: 2

# Também podemos chamá-lo a partir de uma instância (mas é mais comum chamá-lo da classe)
print(pessoa1.get_quantidade_pessoas())  # Saída: 2
hasattr_getattr.py
hasattr(obj, "nome_do_atributo") # Retorna True ou False
print(getattr(p, "nome"))  # ✅ Retorna "Alice"
print(getattr(p, "idade", 25))  # ✅ Retorna 25 porque 'idade' não existe

A função hasattr(obj, "atributo") é usada para verificar se um objeto possui um determinado atributo ou método. Já a função getattr(obj, "atributo", valor_padrão), obtém o valor de um atributo de um objeto. Se o atributo não existir, pode retornar um valor padrão.


Mixin

Um mixin é uma técnica de herança múltipla usada para adicionar funcionalidades específicas a uma classe, sem ser a superclasse principal dela. Ou seja, os filhos herdam atributos de uma classe que é Pai dele. Mixins são classes que fornecem funcionalidades reutilizáveis para outras classes, mas não são usadas sozinhas — ou seja, são feitas para serem herdadas em conjunto com outras classes. Muito usada quando você quer compartilhar comportamento entre várias classes, mas não quer duplicar código.

mixin.py
class LogMixin:
    def log(self, message):
        print(f"[LOG]: {message}")

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

class Funcionario(Pessoa, LogMixin):
    def trabalhar(self):
        self.log(f"{self.nome} está trabalhando...")

Aqui, LogMixin adiciona um comportamento (log) para a classe Funcionario, mas não faria sentido ser usada sozinha.

f = Funcionario("Diogo")
f.trabalhar()
# Saída: [LOG]: Diogo está trabalhando...


Herança

Herança, faz uma classe herdar todas as caracteristicas da classe pai (atributos e métodos). Se a classe filha não tiver um construtor próprio, o construtor da classe pai será chamado automaticamente assim que uma instância da classe filha for criada. A herança é usada quando há uma relação "é um" (is-a).

| Comparativo Herança

heranca.py
class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def saudacao(self):
        return f"Olá, meu nome é {self.nome} e tenho {self.idade} anos."

# Herdo a classe Pessoa
class Crianca(Pessoa):

    def __init__(self, nome, idade, escola):
        # Inicializa um atributo da classe
        self.escola = escola

        # definir um atributo da classe diretamente (sem os argumentos recebido)
        self.qtd_olhos = 2

        # Chama o método pai para inicializar o nome e idade
        super().__init__(nome, idade)
    
    # Sobrescrevo o método da classe pai   
    def saudacao(self):
        return f"SOU UMA CRIANÇA e meu nome é {self.nome}, tenho {self.idade} anos."

c = Crianca("Pedro",10,"CEMEI MIMOSO" )
c.saudacao()

Chama o metodo __init__ da classe pai, de forma explicita (caso deseje reutilizar algo da classe pai),

iniciar_pai.py
class Crianca:
    def __init__(self):
        Pessoa.__init__(self, nome, idade)

Segunda maneira de inicializar o contrutor da classe pai. Referência ao codigo anterior.


Composição

A composição é um princípio da POO onde uma classe é composta por objetos de outras classes. Isso significa que, em vez de uma classe herdar atributos e métodos de outra (herança), ela possui instâncias de outras classes como atributos. A composição é usada quando há uma relação "tem um" (has-a).

composicao.py
class Motor:
    def __init__(self, potencia):
        self.potencia = potencia

    def ligar(self):
        return "Motor ligado!"

class Carro:
    def __init__(self, modelo, potencia_motor):
        self.modelo = modelo
        self.motor = Motor(potencia_motor)  # Composição: Carro possui um Motor

    def ligar_carro(self):
        return f"{self.modelo}: {self.motor.ligar()}"

# Criando um carro com um motor de 150 cavalos
carro1 = Carro("Ferrari", 150)

print(carro1.ligar_carro())  # Ferrari: Motor ligado!
print(f"Potência do motor: {carro1.motor.potencia} cavalos")  # Acessando atributo da composição

Encapsulamento

O encapsulamento refere-se ao conceito de ocultar a implementação interna de uma classe e expor apenas uma interface controlada para o exterior, permitindo que o acesso aos dados e comportamentos da classe seja feito de forma controlada.

Os métodos de acesso são funções que permitem o acesso a atributos privados ou protegidos de uma classe (Atributos podem ser públicos, protegidos ou privado). Getter é um método que permite acessar o valor de um atributo privado ou protegido. Setter é um método que permite modificar o valor de um atributo privado ou protegido. @property faz acessar uma função como se fosse um atributo. O decorador @atributo.setter só pode ser usado em um método que já tenha um getter definido anteriormente com o decorador @property.

Tabela de Métodos de Acesso e Tipos de Atributos
Conceito Descrição Exemplo
Getter Método que permite acessar o valor de um atributo privado ou protegido. @property def nome(self): return self._nome
Setter Método que permite modificar o valor de um atributo privado ou protegido. @nome.setter def nome(self, value): self._nome = value
Atributo Público Atributo acessível de qualquer lugar no código. self.nome (sem prefixo ou com prefixo _ em alguns casos)
Atributo Protegido Atributo acessível dentro da classe e subclasses, mas não fora delas. _nome (com prefixo _)
Atributo Privado Atributo acessível somente dentro da própria classe, geralmente indicado por __. __nome (com prefixo __)
encapsulamento.py
class Pessoa:
    def __init__(self, nome, idade):
        self._nome = nome       # Atributo protegido
        self.__idade = idade    # Atributo privado

    # Getter para o atributo protegido
    @property
    def nome(self):
        return self._nome

    # Setter para o atributo protegido
    @nome.setter
    def nome(self, value):
        if len(value) < 3:
            raise ValueError("Nome muito curto!")
        self._nome = value

    # Getter para o atributo privado
    @property
    def idade(self):
        return self.__idade

    # Não há setter para o atributo privado, assim ele é imutável
    def set_idade(self, idade):
        if idade < 0:
            raise ValueError("Idade não pode ser negativa.")
        self.__idade = idade

pessoa = Pessoa("João", 30)
print(pessoa.nome)      # Acessa o atributo protegido (getter)
pessoa.nome = "Carlos"  # Modifica o atributo protegido (setter)

# print(pessoa.__idade)  # Isso geraria um erro, pois __idade é privado
print(pessoa.idade)     # Acessa o atributo privado (getter)
pessoa.set_idade(35)    # Modifica o atributo privado com o setter

Com o encapsulamento, o nome é protegido e qualquer acesso ou modificação do valor passa pela lógica controlada do getter e setter.

Assuntos Relacionados


Classes e Métodos Abstratos

Uma classe abstrata é uma classe que não pode ser instanciada diretamente. Ela serve apenas como um modelo para outras classes. Garantindo que todas as subclasses implementem os métodos necessários.

Para definir uma classe abstrata em Python, usamos o módulo abc (Abstract Base Classes) e o decorador @abstractmethod.

abstrata.py
from abc import ABC, abstractmethod

class Animal(ABC):  # 🔹 Herdando de ABC -> Torna a classe abstrata
    @abstractmethod
    def emitir_som(self):
        pass  # Método sem implementação

# 🛑 Isso geraria erro, pois não podemos instanciar uma classe abstrata
# animal = Animal()  # TypeError

class Cachorro(Animal):  # Herda de Animal
    def emitir_som(self):
        return "Au Au!"  # Implementação do método abstrato

class Gato(Animal):
    def emitir_som(self):
        return "Miau!"

# Agora podemos instanciar as subclasses
dog = Cachorro()
print(dog.emitir_som())  # 🐶 "Au Au!"

cat = Gato()
print(cat.emitir_som())  # 🐱 "Miau!"

Métodos Embutidos (Built-in)

Métodos de objetos (built-in) estão associados a objetos ou classes específicos e operam sobre os dados contidos nesses objetos (como métodos de listas, strings, arrays, etc.).

metodos_embutidos_strings.py
nome = "Diogo, Marcelo"
# Transforma a String em minúsculo.
nome.lower()

# Transforma a String em Maiúsculo
nome.upper()

# Conta os caracteres específicos em uma String. É case sensitive
nome.count("o") # Saída: 2

# Separa a string colocando numa lista, toda vez que encontrar uma vírgula e um espaço! O padrão sem nada separa por espaço
nome.split(", ")

# Método que converte a primeira letra dos caracteres maiúscula
nome.capitalize()

#Faz o mesmo que o método acima, mas serve para nomes compostos ou várias palavras
nome.title()

# Centraliza uma string dentro de um espaço definido, preenchendo os espaços restantes com um caractere opcional.
print(nome.center(20, '!'))

# Retorna a posição de um caractere (-1 se não existir)
nome.find("o")

#  Verifica se todos os caracteres da string são letras (A-Z ou a-z) e não vazios
print("abc".isalpha())      # True
print("abc123".isalpha())   # False (contém números)

# Retorna a String completa substituindo uma parte (é case sensitive), nesse caso substitui Marcelo por Mamédio
nome_modificado = nome.replace("Marcelo","Mamédio")

# Retorna a String removendo os espaços em branco apenas do inicio e do fim
texto = "    EU SOU O MELHOR ATACANTE    "
print(texto.strip())

# Retorna uma string de um iterável separada por virgulas
numeros = ['1', '2', '3', '4']
resultado = ", ".join(numeros)

O .join() é um método da classe str em Python. O método .join(iterável) junta os elementos do iterável (como listas ou tuplas) em uma única string, usando a string que o chama como separador.


Métodos Especiais (Dunder methods)

Métodos especiais, dunder methods (double underscore methods) ou métodos mágicos, possuem dois underscores antes e depois do nome (por exemplo, __init__, __str__, __len__), eles são usados para definir comportamentos internos de classes e interagir com operações embutidas do Python. São chamados automaticamente pelo Python quando uma ação correspondente acontece. No entanto, alguns precisam ser explicitamente ativados por certas operações, por exemplo,__init__ que NÃO é chamado automaticamente em herança se a classe filha não chamar super().__init__().

Aqui estão alguns métodos que o Python chama automaticamente em diferentes situações:

Método Quando é chamado automaticamente?
__init__ Quando um objeto é instanciado (obj = Classe()).
__del__ Quando um objeto é destruído (pode ser quando sai do escopo ou manualmente com del).
__str__ Quando str(obj) ou print(obj) é chamado.
__repr__ Quando repr(obj) ou o objeto é exibido no terminal.
__len__ Quando len(obj) é chamado.
__getitem__ Quando obj[chave] é acessado.
__setitem__ Quando obj[chave] = valor é atribuído.
__delitem__ Quando del obj[chave] é chamado.
__call__ Quando o objeto é tratado como uma função (obj()).
__add__ Quando obj1 + obj2 é usado.
__sub__ Quando obj1 - obj2 é usado.
__mul__ Quando obj1 * obj2 é usado.
__eq__ Quando obj1 == obj2 é comparado.
__lt__ Quando obj1 < obj2 é comparado.
__gt__ Quando obj1 > obj2 é comparado.
__contains__ Quando valor in obj é usado.
metodos_especiais.py
# Exemplo get_item
class ListaPersonalizada:
    def __init__(self, elementos):
        self.elementos = elementos

    def __getitem__(self, index):
        return self.elementos[index]

lista = ListaPersonalizada([10, 20, 30])
print(lista[1])  # __getitem__ será chamado automaticamente


# Exemplo com len
class Grupo:
    def __init__(self, pessoas):
        self.pessoas = pessoas

    def __len__(self):
        return len(self.pessoas)

g = Grupo(["Alice", "Bob", "Carlos"])
print(len(g))  # __len__ será chamado automaticamente


# implementando o metodo iter em minha coleção
class MinhaColecao:
    def __init__(self):
        self.dados = [1, 2, 3]

    def __iter__(self): # sem esse metodo não seria um iteravel
        return iter(self.dados)  

colecao = MinhaColecao()
for item in colecao:
    print(item)  # Agora funciona!

Metaclasses

Uma classe também é um objeto em python.

Uma metaclasse é uma classe que cria outras classes.  O type é a metaclasse padrão em Python, é a fábrica de classes que cria as classes (por debaixo dos panos) do mesmo jeito que uma classe cria objetos.  Toda vez que criamos uma classe, ela é, na verdade, um objeto criado pela metaclasse type.

Obs.: o metodo type(obj) não é a metaclasse type. mas retorna quem criou o objeto.

🔹 Casos comuns para usar metaclasses: Adicionar automaticamente métodos ou atributos às classes, Controlar a criação de classes (ex: impedir que uma classe seja criada sem determinado atributo), Registrar automaticamente classes (ex: criar um registro automático de todas as classes que herdam de uma base), Forçar padrões de código (ex: garantir que todas as classes sigam um formato específico)

Não crie metaclasse se o problema pode ser resolvido com herança ou funções normais. 

classes_com_type.py
MinhaClasse = type("MinhaClasse", (), {"atributo": "Olá, mundo!"})

print(MinhaClasse)       # <class '__main__.MinhaClasse'>
print(MinhaClasse.atributo)  # Olá, mundo!

podemos usar type diretamente para criar uma classe. quando escrevemos class MinhaClasse: é equivalente a type("MinhaClasse", (), {}) nos bastidores.

metaclasse.py
class MinhaMetaClasse(type):  # Criamos uma metaclasse herdando de 'type'
    def __new__(cls, name, bases, dct):
        print(f"🔨 Criando a classe {name}")
        return super().__new__(cls, name, bases, dct)

# Agora usamos essa metaclasse para criar uma classe
class MinhaClasse(metaclass=MinhaMetaClasse):
    pass

podemos criar a nossa própria metaclasse para mudar o comportamento da criação das classes.