Programação Orientada a Objetos

Programação Orientada a Objetos

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 atributos

  • O '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

💡
É preferível não colocar traço baixo (_) em uma função e sim em uma variável.
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()