Skip to main content

Orientação a Objetos



Orientação a Objetos


A Orientação a Objetos é uma forma de organizar o código. Em vez de só usar variáveis e funções soltas, você agrupa dados e ações dentro de algo chamado classe. A classe é um molde. Graças a esse esse molde (classe) nós podemos criar objetos.


Cada vez que você usa uma classe para criar algo, o Python gera um novo objeto com base naquele molde, ou seja, uma nova instância. Essa instância é independente das outras: ela segue a estrutura definida pela classe, mas armazena seus próprios dados.


Uma classe define a estrutura e o comportamento de um tipo de objeto. Isso inclui atributos (variáveis associadas a cada instância) e métodos (funções que operam sobre esses atributos). Por exemplo, ao definir uma classe Carro, você especifica o que cada objeto do tipo Carro terá, como marca, modelo e um método para acelerar.

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

def acelerar(self):
print(f"O {self.marca} {self.modelo} está acelerando.")

Esse código define a classe Carro com dois atributos e um método. A função __init__ é um construtor, chamado automaticamente ao criar um objeto da classe. A palavra-chave self representa a instância atual. Quando criamos um objeto com meu_carro = Carro('Ford', 'Fusion'), estamos instanciando a classe e atribuindo valores aos atributos.



Classes


Uma Class em Python é uma estrutura que serve como molde para criar objetos. Elas definem o que um objeto vai ter (os dados) e o que ele pode fazer (os comportamentos). Você pode imaginar uma classe como um plano de construção, e os objetos como casas feitas a partir desse plano. Cada objeto tem as mesmas partes definidas pela classe, mas com valores próprios.


Para criar uma classe em Python, você usa a palavra-chave class:

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

def falar(self):
print(f"{self.nome} está falando.")

Nesse exemplo, Pessoa é a classe. O método __init__ é especial, ele é chamado automaticamente sempre que um novo objeto é criado. Ele recebe self (que representa o próprio objeto) e nome, que será guardado dentro do objeto como self.nome.


Você pode criar objetos assim:

p1 = Pessoa("João")
p2 = Pessoa("Maria")

Tudo o que está definido dentro da classe pode ser usado pelos objetos criados com ela. Se você quiser que um objeto tenha um novo atributo depois de criado, pode fazer isso também, mas isso não afeta os outros objetos.


As classes são fundamentais na programação orientada a objetos. Elas permitem que você modele entidades do mundo real (como uma Pessoa, um Carro, uma ContaBancaria) dentro do código, agrupando dados e comportamentos em um só lugar. Isso deixa o código mais organizado, reutilizável e fácil de manter.



Encapsulamento


Encapsulamento é uma forma de organizar e proteger os dados dentro de um objeto. A ideia é simples, nem tudo dentro de um objeto precisa ou deve ser acessado diretamente por quem está usando ele. Algumas partes são internas, servem para o funcionamento da classe, e não deveriam ser modificadas ou vistas de fora. O encapsulamento define esse limite.


Em linguagens como Java ou C++, você usa palavras-chave para dizer o que é público e o que é privado. Já no Python, isso é feito por convenção, ou seja, o programador sinaliza o que é interno usando um underscore antes do nome da variável ou método. Por exemplo:

class ContaBancaria:
def __init__(self, saldo):
self._saldo = saldo # sinaliza que isso é interno

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

def mostrar_saldo(self):
print(f"Saldo atual: R$ {self._saldo}")

Nesse exemplo, _saldo pode até ser acessado de fora com conta._saldo, mas o underline avisa, "isso é uma variável interna, não mexa se não souber o que está fazendo".


Se quiser deixar ainda mais claro que o atributo não deve ser acessado, o Python permite usar dois underlines, como em __saldo. Isso ativa uma técnica chamada name mangling, que muda o nome interno da variável e dificulta o acesso direto:

class ContaSegura:
def __init__(self, saldo):
self.__saldo = saldo

Agora, se tentar conta.__saldo, vai dar erro. Só dá pra acessar com um nome interno gerado automaticamente (_ContaSegura__saldo), o que força mais ainda o encapsulamento.


O principal objetivo é proteger os dados do objeto e deixar claro quais partes são públicas (feitas para serem usadas de fora) e quais são privadas (detalhes da implementação que podem mudar e não devem ser tocados diretamente). Isso ajuda a manter o código mais confiável e organizado.


Quando você define uma variável dentro de uma função (ou método), ela existe só dentro daquela função. Assim que a função termina, essa variável desaparece. Isso acontece porque ela é uma variável local, usada apenas naquele momento.


Já quando você usa self.alguma_coisa, essa variável fica armazenada no objeto, e outras funções (métodos) dentro da mesma classe podem acessá-la. Isso é essencial na orientação a objetos, os dados do objeto vivem enquanto o objeto existir, não apenas durante a execução de uma função.


Ao usar self, você está dizendo que aquela informação faz parte do objeto. Isso não é o mesmo que dizer que ela deve ser acessível de fora. O encapsulamento entra aí, você controla o acesso. Mesmo sendo parte do objeto, nem toda variável precisa ou deve ser vista ou modificada por quem está usando aquele objeto.


Por exemplo, o saldo de uma conta bancária pode ser uma variável com self._saldo. Mas você não quer que alguém vá lá e mude diretamente com conta._saldo = 100000. Você quer que a pessoa use o método depositar() ou sacar(), que fazem validações, protegem regras, garantem integridade.


Então, o uso de underline apenas avisa ao programador que essa variável é interna e que não deve ser alterada diretamente de fora da classe. Mas dentro da classe ela é livre para ser usada, alterada, deletada, como quiser.



Protected


Quando você usa um único underscore no início do nome de um atributo ou método (como _saldo), está seguindo a convenção de que aquele elemento é protegido. Isso quer dizer que ele não deveria ser acessado fora da classe nem de suas subclasses, embora tecnicamente seja possível.


O uso de _ é uma forma de dizer: "isso é um detalhe interno da implementação, mexa por sua conta e risco".


Por exemplo:

class Conta:
def __init__(self, saldo):
self._saldo = saldo

def _atualizar_saldo(self, valor):
self._saldo += valor

Subclasses podem acessar _saldo ou _atualizar_saldo(), mas o programador deve saber que está lidando com algo que não faz parte da interface pública e que pode mudar a qualquer momento.


Tanto atributos protegidos (_) quanto privados (__) podem ser acessados, modificados, ou sobrescritos livremente dentro da própria classe onde foram definidos.



Private


Já quando você usa dois underscores antes do nome (__saldo), o Python aplica um mecanismo chamado name mangling, que transforma internamente o nome do atributo para evitar acessos diretos por engano ou descuido. Por exemplo:

class ContaSegura:
def __init__(self, saldo):
self.__saldo = saldo

Nesse caso, a variável não pode ser acessada diretamente por conta.__saldo, pois internamente ela foi renomeada para _ContaSegura__saldo. Isso não torna o atributo totalmente inacessível (nada em Python é 100% privado), mas torna o acesso externo bem mais difícil, o que ajuda a reforçar o encapsulamento e a proteger o estado interno do objeto.


Isso é útil quando você quer deixar claro que determinado dado não deve ser acessado nem mesmo por subclasses. Por exemplo, a tentativa abaixo vai falhar:

conta = ContaSegura(500)
print(conta.__saldo) # AttributeError

Mas ainda é possível acessá-lo com:

print(conta._ContaSegura__saldo)  # Funciona, mas é um atalho "feio"

Esse comportamento não é para segurança, e sim para proteger a integridade do código ao evitar conflitos de nomes e acessos indevidos por acidente.


Tanto atributos protegidos (_) quanto privados (__) podem ser acessados, modificados, ou sobrescritos livremente dentro da própria classe onde foram definidos.



Herança


A Herança permite que uma classe herde atributos e métodos de outra. Isso promove reuso de código. Por exemplo, podemos ter uma classe Veiculo com métodos genéricos e classes Carro e Moto que herdam de Veiculo, acrescentando ou sobrescrevendo funcionalidades específicas.

class Veiculo:
def mover(self):
print("O veículo está se movendo.")

class Moto(Veiculo):
def empinar(self):
print("A moto está empinando.")

Ao instanciar Moto, ela herda o método mover da classe Veiculo, além de ter seu próprio método empinar.


O Polimorfismo é a capacidade de diferentes classes implementarem métodos com o mesmo nome, mas comportamentos distintos. Isso permite escrever código que pode trabalhar com objetos de diferentes classes de forma intercambiável. Por exemplo, se Carro e Bicicleta tiverem ambos um método mover, podemos chamá-lo em qualquer um dos objetos sem nos preocupar com o tipo:

def iniciar_movimento(veiculo):
veiculo.mover()

Essa função pode receber qualquer objeto que implemente mover, independentemente da classe. Isso é polimorfismo em ação.


Python permite também a criação de classes com atributos de classe (compartilhados por todas as instâncias) e atributos de instância (específicos para cada objeto). Métodos estáticos (@staticmethod) e métodos de classe (@classmethod) fornecem outras formas de organizar o comportamento dependendo se precisam ou não de acesso à instância ou à classe.



atributos


Quando você cria uma classe, pode definir atributos que ficam armazenados em dois lugares diferentes: na própria classe ou em cada objeto. Esses dois tipos se comportam de forma distinta.


Um atributo de instância é criado com self.algo dentro de um método, normalmente dentro do __init__. Ele é exclusivo de cada objeto. Cada instância da classe pode ter um valor diferente para esse atributo.

class Pessoa:
def __init__(self, nome):
self.nome = nome # atributo de instância

p1 = Pessoa("João")
p2 = Pessoa("Maria")

print(p1.nome) # João
print(p2.nome) # Maria

Já um atributo de classe é definido diretamente dentro do corpo da classe, fora dos métodos. Ele é compartilhado entre todos os objetos daquela classe. Se você alterar o valor no nível da classe, ele muda para todos que ainda não tenham sobrescrito o valor individualmente.

class Pessoa:
especie = "Humano" # atributo de classe

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

print(Pessoa.especie) # Humano

p = Pessoa("João")
print(p.especie) # Humano

Pessoa.especie = "Alienígena"
print(p.especie) # Alienígena

Se você fizer p.especie = "Outro", aí o Python cria um novo atributo só naquele objeto, e o valor da classe não é mais usado para ele.



Métodos


Existem três tipos de métodos:

  1. O método comum, que usa self, acessa os dados do objeto específico (instância).
  2. O @classmethod, que usa cls, acessa a classe como um todo, podendo modificar atributos de classe.
  3. O @staticmethod, que não recebe nem self nem cls. É como uma função qualquer, mas colocada dentro da classe por organização.

Exemplo:

class Pessoa:
especie = "Humano"

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

def apresentar(self): # método comum
print(f"Olá, sou {self.nome}")

@classmethod
def mudar_especie(cls, nova):
cls.especie = nova # muda na classe inteira

@staticmethod
def boas_vindas():
print("Bem-vindo ao programa!")

O método comum (apresentar) precisa do objeto para funcionar, pois usa self.nome. O classmethod usa cls para alterar algo na definição da classe. O staticmethod não depende de nada, apenas está ali por organização, e pode ser chamado diretamente com Pessoa.boas_vindas().


# Podemos chamar um 'staticmethod' assim:
Pessoa.boas_vindas() # Funciona

p = Pessoa()
p.boas_vindas() # Também funciona


classmethod

Quando você usa @classmethod, está dizendo que aquele método não vai trabalhar com um objeto específico, mas sim com a classe inteira. O cls que aparece como primeiro parâmetro é a própria classe, assim como self representa o objeto.


Tomemos o código abaixo como exemplo:

class Pessoa:
especie = "Humano"

@classmethod
def mudar_especie(cls, nova):
cls.especie = nova

Aqui, especie é um atributo da classe, ou seja, todos os objetos usam esse valor. Se você quiser mudar esse valor para todos os objetos de uma vez só, você faz isso com um método de classe.


Você pode chamar assim:

Pessoa.mudar_especie("Alienígena")

Isso vai mudar Pessoa.especie, e qualquer objeto novo (ou antigo, que não tenha sobrescrito esse valor) vai refletir essa mudança:

p1 = Pessoa()
print(p1.especie) # Alienígena

Agora, se você usasse um método comum com self, ele só poderia alterar a cópia daquele objeto específico. Já com @classmethod, você acessa a classe em si e muda o valor original para todos.


Resumindo: @classmethod é útil quando você precisa alterar ou consultar coisas da própria classe, e não de um objeto só.



__name__ e __main__


O __name__ é uma variável especial que o Python cria automaticamente em todo arquivo .py. O valor dela muda dependendo de como o arquivo está sendo executado. Se você executar o arquivo diretamente, por exemplo com:

python meu_script.py

Então, dentro desse arquivo, o valor de __name__ será:

"__main__"

Agora, se você importa esse arquivo de outro, assim:

import meu_script

Então, dentro do arquivo meu_script.py, o valor de __name__ será:

"meu_script"

Ou seja, __name__ guarda o nome do módulo atual. E "__main__" é apenas uma string, que o Python usa como sinal de que aquele código está sendo executado como programa principal.


Uma expressão muito usada é if __name__ == "__main__":, ela é usada em arquivos Python para dizer: só execute esse bloco se esse arquivo estiver sendo executado diretamente. Se o arquivo for apenas importado por outro, esse bloco não será executado.


Exemplo:

class Pessoa:
especie = "Humano"

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

def apresentar(self): # método comum
print(f"Olá, sou {self.nome}")

@classmethod
def mudar_especie(cls, nova):
cls.especie = nova # muda na classe inteira

@staticmethod
def boas_vindas():
print("Bem-vindo ao programa!")


if __name__ == "__main__":
p = Pessoa("Fulano")

Se você executar o arquivo .py diretamente, a classe será instanciada, já que a variável __name__ terá o valor __main__. Mas se esse arquivo for importado, a classe não vai ser instanciada, na verdade, nada irá acontecer.



Self


O termo self representa a própria instância de uma classe, permitindo acessar atributos e métodos associados ao objeto criado. Quando definimos métodos em uma classe, o primeiro parâmetro que esses métodos recebem é convencionalmente chamado de self. Este parâmetro permite que você referencie o próprio objeto, acessando seus atributos e métodos internos.


Veja um exemplo prático:

class Pessoa:
def __init__(self, nome, idade):
self.nome = nome # atributo da instância
self.idade = idade # outro atributo da instância

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

# Cria um objeto (instância) da classe Pessoa
pessoa1 = Pessoa("Fulano", 27)

# Chama o método cumprimentar
pessoa1.cumprimentar()

Saída:

Olá, meu nome é Fulano e tenho 27 anos.

Se você não passar o parâmetro self para um método cumprimentar, ocorrerá um erro quando tentar chamá-lo. Isso ocorre porque quando você invoca um método como pessoa1.cumprimentar(), o Python implicitamente passa a instância (pessoa1) como argumento para o método cumprimentar.


Como o método foi definido sem aceitar nenhum parâmetro, ele gera um erro dizendo que recebeu 1 argumento, mas não espera ter nenhum.


Quando usado self.nome = Fulano, isso significa que você está criando (ou definindo) um atributo de instância chamado nome. O self aqui é a referência para o objeto atual.



Property


O @property é um decorador que permite que você utilize métodos como se fossem atributos comuns. Ele fornece uma maneira elegante e eficiente de encapsular atributos privados, permitindo controle sobre seu acesso e modificação.


Em resumo, com property, você pode:

  • Criar getters e setters personalizados.
  • Validar valores antes de atribuir a um atributo.
  • Executar algum comportamento toda vez que um atributo for acessado ou modificado.

Considere um exemplo de uma classe Pessoa, em que você quer controlar o acesso e a modificação da idade:

class Pessoa:
def __init__(self, idade):
self._idade = idade # Atributo "privado" por convenção

@property
def idade(self):
print("Acessando idade")
return self._idade

@idade.setter
def idade(self, valor):
if valor < 0:
raise ValueError("Idade não pode ser negativa.")
print("Alterando idade")
self._idade = valor

Para usar basta fazer:

p = Pessoa(27)

# Acessando (getter) como se fosse um atributo
print(p.idade)

# Alterando (setter)
p.idade = 28

print(p.idade)

# Tentando definir valor inválido
p.idade = -5 # Isso gera erro

Resultado:

Acessando idade
27
Alterando idade
Acessando idade
28
ValueError: Idade não pode ser negativa.

O decorador @property transforma um método que você criou em um getter, permitindo acessá-lo como se fosse um atributo normal. Se você quiser modificar o valor, precisa definir um método setter, usando a sintaxe especial:

  • Getter com @property:
@property
def nome_atributo(self):
return self._nome_atributo

  • Setter com @nome_atributo.setter:
@nome_atributo.setter
def nome_atributo(self, valor):
# Faça validação ou lógica aqui
self._nome_atributo = valor

Também é possível definir um deleter, mas isso é mais raro:

  • Deleter com @nome_atributo.deleter:
@nome_atributo.deleter
def nome_atributo(self):
del self._nome_atributo


Associação, Agregação e Composição


A associação, agregação e composição são conceitos da programação orientada a objetos que ajudam a representar relações entre objetos. Eles explicam como uma classe usa ou depende de outra, e cada uma dessas relações tem um grau diferente de acoplamento (ou seja, o quanto uma depende da outra para existir). Você usa isso para organizar melhor seu código, representando de forma fiel as relações do mundo real entre as entidades do seu sistema.



Associação


É a relação mais genérica entre duas classes. Significa apenas que uma classe conhece a outra e pode usar seus métodos ou atributos.


class Pessoa:
def cumprimentar(self):
print("Olá!")

class Empresa:
def __init__(self, funcionario):
self.funcionario = funcionario

def apresentar_funcionario(self):
self.funcionario.cumprimentar()

Aqui a Empresa usa uma Pessoa, mas nenhuma das duas depende da outra para existir. Você pode criar objetos das duas classes separadamente. É só uma relação de uso.



Agregação


É um tipo de associação mais forte, onde um objeto faz parte de outro, mas ainda pode existir sozinho. A ideia é que o objeto "agregado" não é criado dentro da outra classe, ele é passado de fora.


class Motor:
def ligar(self):
print("Motor ligado.")

class Carro:
def __init__(self, motor):
self.motor = motor # Seria como instanciar a classe Motor(), mas não é exatamente isso.

def ligar(self):
self.motor.ligar()

O Motor é parte do carro, mas também pode existir fora dele. Você pode ter um motor sem carro, trocar o motor de um carro por outro, etc. A vida útil do motor é independente do carro.



Composição


É a relação mais forte entre classes. Aqui, um objeto é parte fundamental de outro e não faz sentido existir sem ele. O objeto composto é criado dentro da classe e morre com ela.


class Motor:
def __init__(self):
print("Motor criado.")

class Carro:
def __init__(self):
self.motor = Motor() # Literalmente instanciando a classe dentro da classe Carro()

def destruir(self):
print("Carro destruído e motor também.")
del self.motor

O Motor faz parte do carro e depende dele. Quando o Carro for destruído, o Motor será também. Aqui existe forte acoplamento. A composição mostra que um objeto é dono absoluto do outro.



__call__


O método especial __call__ transforma um objeto em algo "chamável", ou seja, permite que você use instâncias de classes como se fossem funções. Quando você define o método __call__ dentro de uma classe, está dizendo: "Se alguém tentar usar essa instância com parênteses, execute esse método."

class Saudacao:
def __call__(self, nome):
print(f'Olá, {nome}!')

Agora, você pode fazer isso:

s = Saudacao()
s('Fulano') # Isso vai chamar __call__ automaticamente

Saída:

Olá, Fulano!

Mesmo sem ser uma função, o objeto s pode ser "chamado" como uma função porque a classe Saudacao define o método __call__. Para que serve isso na prática?

  • Simular comportamento de função com objetos que mantêm estado.
  • Simplificar chamadas em classes que encapsulam lógica complexa.
  • Usar objetos como callbacks (em frameworks, decorators, middlewares).
  • Criar APIs elegantes e reutilizáveis.

Outro exemplo para facilitar o entendimento:

class Acumulador:
def __init__(self):
self.total = 0

def __call__(self, valor):
self.total += valor
return self.total

Uso:

a = Acumulador()
a(10) # 10
a(5) # 15
a(3) # 18

Mesmo parecendo uma função, a é um objeto com estado (self.total) e com comportamento definido no __call__.



Dataclass


A dataclass é uma forma prática e automática de criar classes que servem principalmente para armazenar dados. Ela foi introduzida no Python 3.7 (pelo módulo dataclasses) e elimina a necessidade de escrever código repetitivo, como o __init__, __repr__, __eq__, entre outros.


A dataclass serve para definir objetos que só armazenam valores, como estruturas de dados. Ela cria automaticamente métodos úteis baseados nos atributos da classe, como:

  • __init__ para inicializar os dados
  • __repr__ para representação em string
  • __eq__ para comparação
  • E mais, se necessário

from dataclasses import dataclass

@dataclass
class Pessoa:
nome: str
idade: int

Essa simples definição já cria uma classe com __init__, __repr__, __eq__, e outros:


A execução poderia ser assim:

p1 = Pessoa('Fulano', 27)
p2 = Pessoa('Fulano', 27)

print(p1) # Pessoa(nome='Fulano', idade=27)
print(p1 == p2) # True (compara os dados)

Sem @dataclass, você teria que escrever tudo isso na mão.


Você pode configurar o comportamento da dataclass com parâmetros:

@dataclass(order=True, frozen=True)
class Produto:
id: int
nome: str
preco: float

  • order=True: cria métodos de ordenação (<, <=, etc.).
  • frozen=True: torna o objeto imutável (os atributos não podem ser alterados depois de criado).


Valor padrão


@dataclass
class Livro:
titulo: str
paginas: int = 100

Se você não passar o número de páginas, ele será 100:

l = Livro("Python para todos")
print(l.paginas) # 100


Campo com controle especial


Para mais controle, use o field():

from dataclasses import field

@dataclass
class Conta:
nome: str
saldo: float = field(default=0.0, repr=False)
  • default=...: valor padrão.
  • repr=False: exclui do __repr__.