Programação Orientada a Objetos (OOP) é um paradigma de programação 👨💻 que organiza o código em unidades reutilizáveis e auto contidas chamadas objetos. Esses objetos encapsulam dados e comportamentos, permitindo uma abordagem mais modular e estruturada para o desenvolvimento de software. Em Python, OOP é implementada por meio de classes. Voltando para os exemplos de Harry Potter e as Casa ao quais pertencem os magos 👇
def main():
nome = get_name()
casa = get_house()
print(f"{nome} de {casa}")
def get_name():
return input("Nome:")
def get_house():
return input("Casa:")
if __name__ == "__main__":
main()
Há varias formas de representar o algoritmo desse script, uma outra forma é algo assim como 👇:
def main():
nome, casa = get_student()
print(f"Nome: {nome} Casa:{casa}")
def get_student():
nome = input("Nome")
casa = input("Casa")
return (nome,casa) #retorna uma tupla
if __name__ == "__main__":
main()
def main():
student = get_student()
print(f"Nome: {student[0]} Casa:{student[1]}")
def get_student():
nome = input("Nome")
casa = input("Casa")
return [nome,casa] #retorna uma lista
if __name__ == "__main__":
main()
def main():
student = get_student()
print(f"Nome: {student["nome"]} Casa:{student["casa"]}")
def get_student():
estudante = {}
estudante["nome"] = input("Nome")
estudante["casa"] = input("Casa")
return estudante #retorna um dicionario
if __name__ == "__main__":
main()
Classes
Uma classe é um modelo para a criação de objetos. Ela define os atributos (dados) e métodos (funções) que os objetos da classe terão. Classes permitem você criar seus próprios dados, seus próprios objetos. Aqui está um exemplo simples de uma classe 👇:
class Estudante:
...
def get_student():
estudante = Estudante() #aqui cria-se o objeto / inicia a classe
estudante.nome = input("Nome:")
estudante.casa = input("Casa:")
return estudante
def main():
student = get_student()
print(f"{student.nome} de {student.casa}")
if __name__ == "__main__":
main()
Métodos
init
Classes vem com métodos, um dos principais é __init__(self)
que é um método que instância (inicializa).
class Estudante:
def __init__(self, nome, casa):
self.nome = nome
self.casa = casa
Quando você cria um objeto ou função algum lugar, e esta ainda não existe, é chamada de "Construtor call" e irá construir um objeto "Estudante"🎓 . Mas como será que é representado uma classe na memória do computador 💻?
A classe é o código, o template são os objetos e estes são armazenados na 🧠 memória do computador, ocupando alguns bytes.
class Estudante:
def __init__(self, nome, casa):
self.nome = nome
self.casa = casa
def get_student():
nome = input("Nome:")
casa = input("Casa:")
return Estudante(nome, casa)
estudante = get_student()
print(estudante)
<__main__.Estudante object at 0x7fc88a8bebc0>
Agora sabemos que os dados dessa classe foram salvos na memória, com esse endereço 👉 0x7fc88a8bebc0
Raise
Use o raise
quando algum erro acontece ❌, por exemplo, você não precisa fechar ou interromper o programa usando sys.exit()
ou similares. Então use o raise como um alerta 🚨 indicando ao programador que há um erro, e não simplesmente deixe-o com o aviso de forma genérica, explique o erro.
class Estudante:
def __init__(self,nome, casa):
if not nome:
raise ValueError("Faltando nome")
if casa not in ["Grifinória", "Cornival", "Sonserina"]:
raise ValueError("Casa não válida")
self.nome = nome
self.casa = casa
def get_student():
nome = input("Nome:").title()
casa = input("Casa:").title()
return Estudante(nome, casa)
def main():
student = get_student()
print(f"{student.nome} de {student.casa}")
if __name__ == "__main__":
main()
Str
Este é outro método do python que automaticamente chama a função, a qualquer momento. Se você quer ver seu objeto como string, geralmente usado para usuários ao invés de programadores.
class Estudante:
def __init__(self,nome, casa):
if not nome:
raise ValueError("Faltando nome")
if casa not in ["Grifinória", "Cornival", "Sonserina"]:
raise ValueError("Casa não válida")
self.nome = nome
self.casa = casa
def __str__(self):
return f"{self.nome} de {self.casa}"
def get_student():
nome = input("Nome:").title()
casa = input("Casa:").title()
return Estudante(nome, casa)
def main():
student = get_student()
print(student)
if __name__ == "__main__":
main()
Veja que as funções dentro da classe tem por parâmetro a palavra self
e isso é porque self é um método dentro de um classe, e a palavra é self por convenção e uma prática, ou seja, porque foi decidido usar essa palavra, mas não é obrigatório, simplesmente porque facilita a leitura por outros programadores, 🤷🏻♂️ ...bora agregar mais uma função na classe 👇.
class Estudante:
def __init__(self,nome, casa, patrono):
if not nome:
raise ValueError("Faltando nome")
if casa not in ["Grifinória", "Cornival", "Sonserina"]:
raise ValueError("Casa não válida")
self.nome = nome
self.casa = casa
self.patrono = patrono
def __str__(self):
return f"{self.nome} de {self.casa}"
def charm(self):
match self.patrono:
case "cervo":
return "🐴"
case "terrier":
return "🐶"
case _:
return "🪄"
def get_student():
nome = input("Nome:").title()
casa = input("Casa:").title()
patrono = input("Patrono:").lower()
return Estudante(nome, casa, patrono)
def main():
student = get_student()
print(student)
print("Expecto patronum")
print(student.charm()) # nesta linha se tem acesso a classe fora desta
if __name__ == "__main__":
main()
Nome:alvo
Casa:grifinória
Patrono:fenix
Alvo de Grifinória
Expecto patronum
🪄
Existe um "perigo" ☝️ ao acessarmos de fora da classe para modificar valores. Dentro da classe a casa é validada numa lista if casa not in ["Grifinória", "Cornival", "Sonserina"]
, mas ela pode ser acessada de fora da classe e ainda modificar a lista. Uma analogia seria: fazer um pix de 10R$ e modificar o código como se fosse 1 milhão R$💰 .
def main():
student = get_student()
student.casa = "Londrina"
print(student)
Nome:harry
Casa:grifinória
Harry de Londrina
Para evitar acessos de fora da classe usamos as propriedades da classe.
Propriedades
As propriedades em uma classe Python são atributos especiais que permitem controlar o acesso, a leitura e a gravação de valores. Elas são implementadas por meio de métodos conhecidos como "getters" e "setters". Um getter permite acessar o valor da propriedade, enquanto um setter permite modificar o valor. O uso de propriedades ajuda a encapsular a implementação interna da classe, permitindo uma interface mais controlada e facilitando a manutenção do código.
E para os getters ou setters utilizamos os decoradores (decorators) --é uma função que envolve outra função --, alterando ou estendendo seu comportamento. Pode ser aplicado a uma função usando o símbolo @
seguido pelo nome do decorator, posicionado acima da definição da função. Bora ver um exemplo 👇:
class Estudante:
def __init__(self,nome, casa):
if not nome:
raise ValueError("Faltando nome")
self.nome = nome
self.casa = casa
def __str__(self):
return f"{self.nome} de {self.casa}"
#getter
@property
def casa(self):
return self._casa #tecnicamente a instancia original , agora é chamada _casa
#setter
@casa.setter
def casa(self, casa):
# isso previne o acesso de fora e mudar valores da classe
if casa not in ["Grifinória", "Cornival", "Sonserina"]:
raise ValueError("Casa não válida")
self._casa = casa
def get_student():
nome = input("Nome:").title()
casa = input("Casa:").title()
return Estudante(nome, casa)
def main():
student = get_student()
#student.casa = "curitiba"
print(student)
if __name__ == "__main__":
main()
Isso previne acesso externo para modificar algum valor da classe. E simplesmente alerta com o raise ValueError("Casa não válida")
.
Se não ficou muito claro, vamos novamente 👩🏫 rever:
O 'getter' aponta para
@property
, é uma função que recebe atributosO 'setter' aponta a função que estabelece valores
@algo.setter
O getter e o setter trabalham em conjunto, o getter recebe e passa para o setter para que este 'dê' novos valores.
se o nome do getter é
fulano
, a propriedade do setter é@fulano.setter
uma variável não pode ter o mesmo nome que uma função, por isso o nome da variável passou a ser
_casa
class Estudante:
def __init__(self,nome, casa):
self.nome = nome
self.casa = casa
def __str__(self):
return f"{self.nome} de {self.casa}"
@property
def nome(self):
return self._nome
@nome.setter
def nome(self, nome):
if not nome:
raise ValueError("Faltando nome")
self._nome = nome
#getter
@property
def casa(self):
return self._casa #tecnicamente a instancia original , agora é chamada _casa
#setter
@casa.setter
def casa(self, casa):
# isso previne o acesso de fora e mudar valores da classe
if casa not in ["Grifinória", "Cornival", "Sonserina"]:
raise ValueError("Casa não válida")
self._casa = casa
def get_student():
nome = input("Nome:").title()
casa = input("Casa:").title()
return Estudante(nome, casa)
def main():
student = get_student() #inicia a classe (instância)
#student.casa = "curitiba"
print(student)
if __name__ == "__main__":
main()
Classmethod
É um decorador usado para definir métodos de classe. Um método de classe é associado à classe, em vez de uma instância especifica da classe. Ele recebe como primeiro parâmetro, tradicionalmente chamado de 'cls
' em vez do usual 'self
'. Este permite que o método seja chamada na classe, sem a necessidade de criar uma instância, bora ver um exemplo, seguindo essa mesma linha de exemplos de Harry Potter 🧙♂️👇
import random
class Chapeu:
casas = ["Grifinória", "Sonserina","Cornival", "Lufa-lufa"]
#casas é uma variavel da classe
@classmethod
def sort(cls,nome):
print(nome, "está em,", random.choice(cls.casas))
def main():
Chapeu.sort("Harry")
if __name__ == "__main__":
main()
> Harry está em, Grifinória
class Estudante:
def __init__(self, nome, casa):
self.nome = nome
self.casa = casa
def __str__(self):
return f"{self.nome} from {self.casa}"
@classmethod
def get(cls):
nome = input("Nome:")
casa = input("Casa:")
return cls(nome, casa)
def main():
student = Estudante.get()
print(student)
if __name__ == "__main__":
main()
Hierarquia
🌳 É um conceito que permite que uma classe (subclasse) herde características de outra classe (superclasse). Isso promove a reutilização ♻️ de código e a criação de uma hierarquia de classes, por exemplo entre alunos e professores em Harry Potter há atributos iguais, ambos tem "nomes próprios".
class Estudante:
def __init__(self, nome, casa):
if not nome:
raise ValueError("Faltando Nome")
self.nome = nome
self.casa = casa
class Professor:
def __init__(self, nome, materia):
if not nome:
raise ValueErro("Faltando Nome")
self.nome = nome
self.materia = materia
class Mago:
def __init__(self, nome):
if not nome:
raise ValueError("Faltando Nome")
self.nome = nome
Repare que na classe de Estudante
e Professor
há código que pode ser reusado. Para isso foi criado a classe Mago, então podemos dizer que a classe Estudante e Professor herdam atributos da classe Mago
que contém o código para reutilizar da seguinte forma 👇:
class Mago:
def __init__(self, nome):
if not nome:
raise ValueError("Faltando Nome")
self.nome = nome
class Estudante(Mago):
def __init__(self, nome, casa):
super().__init__(nome) #os atributos da outra classe
self.nome = nome
self.casa = casa
class Professor(Mago):
def __init__(self, nome, materia):
super().__init__(nome)
self.nome = nome
self.materia = materia
def main():
wizard = Mago("Alvo")
student = Estudante("Harry", "Grifinória")
professor = Professor("Severus", "Defesa")
print(wizard)
print(student)
print(professor)
if __name__ == "__main__":
main()
Overloading de Operadores
Refere-se à capacidade de uma classe definir ou redefinir o comportamento de operadores existentes ou criar novos comportamentos para 🔗 operadores. Bora ver uma exemplo com o Banco em Harry Potter 🪙👇:
class Gringotes:
def __init__(self, galeao, sicle, nuque):
self.galeao = galeao
self.sicle = sicle
self.nuque = nuque
def __str__(self):
return f"{self.galeao} Galeão, {self.sicle} Sicles, {self.nuque}Nuques"
def main():
potter = Gringotes(100, 52, 56)
weasley = Gringotes(120,25,30)
print(potter)
print(weasley)
if __name__ == "__main__":
main()
Suponha que quer somar os valores das duas famílias para ver o total 💰👇
def main():
potter = Gringotes(100, 52, 56)
weasley = Gringotes(120,25,30)
galeao = potter.galeao + weasley.galeao
sicle = potter.sicle + weasley.sicle
nuque = potter.nuque + weasley.nuque
total = Gringotes(galeao, sicle, nuque)
print(potter)
print(weasley)
print(total)
É aqui onde entram em cena 🎬 os "Overloading de Operadores" com o método __add__(self, other)
class Gringotes:
def __init__(self, galeao, sicle, nuque):
self.galeao = galeao
self.sicle = sicle
self.nuque = nuque
def __str__(self):
return f"{self.galeao} Galeão, {self.sicle} Sicles, {self.nuque}Nuques"
def __add__(self, other):
galeao = self.galeao + other.galeao
sicle = self.sicle + other.sicle
nuque = self.nuque + other.nuque
return Gringotes(galeao, sicle, nuque)
def main():
potter = Gringotes(100, 52, 56)
weasley = Gringotes(120,25,30)
total = potter + weasley # repare nessa linha
print(potter)
print(weasley)
print(total)
if __name__ == "__main__":
main()