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:
- O método comum, que usa
self
, acessa os dados do objeto específico (instância). - O
@classmethod
, que usacls
, acessa a classe como um todo, podendo modificar atributos de classe. - O
@staticmethod
, que não recebe nemself
nemcls
. É 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__
.