Tabela de Conteudos

Programação Orientada a Objetos Básica


A programação orientada a objetos (POO em português e OOP em ingles) é um paradigma de programação que consiste na divisão do código em blocos independentes entre si.

Esses blocos são chamados de objetos e esses objetos contém propriedades (Atributos) e metodos, as propriedades consistem em valores (variaveis) pertinentes ao objeto principal, já os metodos consistem em funções que performam ações no objeto principal. Os metodos podem ou não usar as propriedades em sua execução.

Por exemplo, se fossemos representar uma pessoa, ela teria propriedades como:

  • nome
  • idade
  • residencia

e teria metodos como:

  • andar
  • correr
  • falar

para facilitar o entendimento, iremos fazer um simples estudante.

pense no seguinte exemplo:

nós temos um estudante com um nome, uma profissão (estudante) e que contem 5 notas e queremos calcular a média delas.

nós poderiamos representar este estudante como um dicionário, e usar sum() / len().

Nota: A Orientação a Objetos é um tópico muito complexo e confuso por si só, até mesmo as diferenciações de vocabulos basicos costumam confundir, tentarei simplificar o máximo sem perder conteudo.

1estudante = {
2    "Nome": "Mirai",
3    "Notas": [8.6, 9.3, 7.5, 8.8, 7],
4    "Profissao": "Estudante",
5}
6
7media = sum(estudante["Notas"]) / len(estudante["Notas"])
8print(f"O {estudante['Profissao']} {estudante['Nome']} tem média {media:.2f}")
9

Saida


O Estudante Mirai tem média 8.24

nós poderiamos representar este mesmo estudante usando uma classe.

A sintaxe básica de uma classe é a seguinte:

1class {Nome}:
2    def __init__(self):
3        {codigo}
4

class -> é a keword de declaração de uma classe

{Nome} -> o nome da classe

def __init__(self): -> um método especial, essencialmente, é executado toda vez que a classe é instanciada, é usado para guardar as propriedades dessa instancia.

self -> um parametro especial, se refere a classe em si

{codigo} -> o codigo para ser executado

uma classe pode ter n funções, basta declara-las normalmente.

vamos representar o nosso estudante como uma classe:

1class Estudante:
2    profissao = "Estudante"  # esse é um Atributo de classe
3
4    def __init__(self, nome, notas):
5        self.notas = notas  # esse é um Atributo de Instancia
6        self.nome = nome  # esse é um Atributo de Instancia
7
8    def calcular_media(self):  # esse é um metodo
9        self.media = sum(self.notas) / len(self.notas)
10
11
12estudante_mirai = Estudante(
13    "Mirai", [8.6, 9.3, 7.5, 8.8, 7]
14)  # estudante_mirai é um objeto instanciado da classe Estudante
15estudante_joaquim = Estudante(
16    "Joaquim", [5.5, 3.8, 7.3, 4.5, 2.5]
17)  # estudante_joaquim é um objeto instanciado da classe Estudante
18
19estudante_mirai.calcular_media()
20print(
21    f"O {estudante_mirai.profissao} {estudante_mirai.nome} tem a media {estudante_mirai.media}"
22)
23

Saida


O Estudante Mirai tem a media 8.24

Classe, Objetos e Instancias

em palavras simples: Um Objeto é uma Instancia de uma Classe.

agora vamos as explicações.

Classes

De acordo com a Wikipédia:

Em programação e na orientação a objetos, uma classe é um Tipo abstrato de Dados (TAD); ou seja, uma descrição que abstrai um conjunto de objetos com características similares (um projeto do objeto), é um código da linguagem de programação orientada a objetos que define e implementa um novo tipo de objeto, que terão características (atributos) que guardaram valores e, também funções específicas para manipular estes.

Essencialmente, uma classe é um "Template" de objeto, definindo os atributos e metodos.

Objetos

De acordo com a Wikipédia:

Em programação orientada a objetos, a palavra objeto refere-se a um "molde"/classe, que passa a existir a partir de uma instância da classe. A classe define o comportamento do objeto, usando atributos (propriedades) e métodos (ações).

Essencialmente, um objeto é uma manifestação independente de uma classe, tendo seus proprios valores para os atributos das classes.

Instancias

De acordo com a Wikipédia:

Em programação orientada a objetos, chama-se instância de uma classe, um objeto cujo comportamento e estado são definidos pela classe.

Essencialmente, As instancias são um conjunto de objetos com metodos e definições de atributos em comum.

Um resumo de tudo de acordo com a Wikipédia:

As instâncias de uma classe compartilham o mesmo conjunto de atributos, embora sejam diferentes quanto ao conteúdo desses atributos. Por exemplo, a classe "Empregado" descreve os atributos comuns a todas as instâncias da classe "Empregado". Os objetos dessa classe podem ser semelhantes, mas variam em atributos tais como "nome" e "salário".

Nomeação de parametros

no seguinte bloco de código:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5

eu irei explicar a diferença entre o self.notas e o notas e consequentemente a diferença entre self.nome e nome.

o self se refere a classe Estudante em si, ou seja, quando fazemos self.notas, nós estamos criando um atributo chamado notas dentro da classe Estudante, e nós o associamos ao parametro notas da função especial __init__()

Metodos Mágicos (Magic Methods/Dunder Methods)

MeetTheDunders

Esses metodos também são chamados de "Metodos Especiais (Special Methods)" e "Dunder Methods"

Os Dunder Methods começam com dois _ ()underscore) e terminam com 2 também, ex: __init__

aqui uma lista dos mais comumns:

  • __init__() -> Executado assim que um novo objeto é instanciado.
  • __new__() -> Executado para criar uma nova instancia de uma classe.
  • __call__() -> Executado quando uma instancia é chamada como função.
  • __name__() -> Retorna o nome da classe cujo o objeto foi instanciado.
  • __repr__() -> Retorna uma string de representação da classe.

irei ressaltar todos exceto o __new__(), deixarei ele para quando aprendermos sobre super()

__init__()

o __init__() é executado quando um objeto é instanciado, ele essencialmente serve para definir os atributos das instancias.

Exemplo:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6
7estd = Estudante("Mirai", [1, 2, 3])
8
9print(estd.nome)
10

Saida


Mirai

__call__()

Essencialmente esse metodo é executado quando uma classe é executada como função.

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6    def __call__(self):
7        print(f"o estudante {self.nome} tem {self.notas} notas")
8
9
10Estd_1 = Estudante("Mirai", [1, 2, 3])
11
12Estd_1()  # __call__ é executado
13

Saida


o estudante Mirai tem [1, 2, 3] notas

__name__

O __name__ é usado quando queremos saber o nome da classe na qual o objeto foi instanciado, ou nome do módulo (mais a frente explicarei sobre).

exemplificarei o uso para classes.

Usando o __name__ nativo das classes:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6
7print(Estudante)
8print(Estudante.__name__)
9

Saida


<class '__main__.Estudante'> Estudante

Veja que nós não precisamos inicializar um objeto, pois nós usamos o __name__ nativo embutido em cada classe do Python.

Definindo a função __name__:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6    def __name__(self):
7        return self.__name__
8
9
10Estd_2 = Estudante("Mirai", [1, 2, 3])
11
12print(Estd_2.__name__)
13

Saida


<bound method Estudante.__name__ of <__main__.Estudante object at 0x7f6c086d3ca0>>

Eu sei, é um pouco confuso mesmo, mas essencialmente ele está dizendo o que o __main__ é:

<metodo associado Estudante.__name__ de <Objeto __main__.Estudante em {endereço na memória}>>

em outras palavras self.__name__ diz o que é o __main__ dele, usando a representação (__repr__) do objeto

Sendo sincero, nunca cheguei a ter que definir o __main__ diretamente, sempre usei o __main__ nativo

__repr__

O __repr__ retorna uma string de representação do objeto quando usamos a função repr().

quando omitimos a função ele usa um equivalente de tipo 'method-wrapper'

NOTA: '<method-wrapper>' é usado no código fonte do Python para implementar funções escritas em C como um '<unbound method>', as explicações pra isso vão além do escopo desse capitulo, então não me aprofundarei.

Usando o repr() nativo das classes:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6
7Estd_2 = Estudante("Mirai", [1, 2, 3])
8
9print(repr(Estd_2))
10

Saida


<__main__.Estudante object at 0x7f6c084bdba0>

O __repr__ retorna o endereço na memória de um objeto.

Definindo o __repr__:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6    def __repr__(self):
7        return f"O Estudante {self.nome} é um objeto"
8
9
10Estd_2 = Estudante("Mirai", [1, 2, 3])
11
12print(repr(Estd_2))
13

Saida


O Estudante Mirai é um objeto

Herança (básica)

Finalmente chegamos a um importantissimo tópico em orientação a objetos, Herança (Inheritance).

Irei me ater aos básicos aqui, de forma resumida, herança é a passagem/compartilhação de estados e metodos entre classes de forma hierarquica.

Nota: 'estado' se refere as variaveis como um todo, conforme formos progredindo os vocábulos começarão a ficar robustos e eu passarei a usar termos mais 'tecnicos'.

Para nos referirmos a hierarquia e herança, usamos uma relação familiar (pai, filho, irmão, etc...)

a sintaxe é bem similar a vista anteriormente:

1class {nome}({classe pai}):
2    def __init__(self, {variaveis da classe pai}):
3        super().__init__({variaveis da classe pai})
4

class {nome}({classe pai}): -> A unica diferença é que especificamos a classe pai como argumento.

def __init__(self, {variaveis da classe pai}): -> além do self incluimos os parametros da função __init__ da classe pai (se eles existirem)

super().__init__({variaveis da classe pai}) -> passamos os parametros da função __init__ do filho a função __init__ do pai usando super()

para exemplificar criaremos uma classe Pessoa e faremos nossa classe estudante filha dela.

1class Pessoa:  # Classe Pai
2    def __init__(self, nome, idade):
3        self.nome = nome
4        self.idade = idade
5
6    def apresentar(self):
7        return f"Olá eu me chamo {self.nome} e tenho {self.idade} anos"
8
9
10class Estudante(Pessoa):  # Classe Filha
11    def __init__(self, nome, idade, medias):
12        super().__init__(nome, idade)
13        self.medias = medias
14
15    def calcular_medias(self):
16        return sum(self.medias) / len(self.medias)
17
18
19EstudanteA = Estudante("Mirai", 17, [8, 9, 7, 6, 8])  # EstudanteA é irmão de EstudanteB
20EstudanteB = Estudante(
21    "Joaquim", 16, [5, 7, 3, 6, 9]
22)  # EstudanteB é irmão de EstudanteA
23
24print(EstudanteA.apresentar())  # Herdou da classe 'Pessoa'
25print(EstudanteA.calcular_medias())  # Definida na classe 'Estudante'
26
27print(EstudanteB.apresentar())  # Herdou da classe 'Pessoa'
28print(EstudanteB.calcular_medias())  # Definida na classe 'Estudante'
29

Saida


Olá eu me chamo Mirai e tenho 17 anos 7.6 Olá eu me chamo Joaquim e tenho 16 anos 6.0

Veja que não precisamos definir self.nome = nome e self.idade = idade na classe Estudante pois essas propriedades são herdadas da classe Pessoa, juntamente com o método apresentar

a relação desses objetos é a seguinte:

OOP_Relations

Todas as instancias da classe Estudante são irmãs entre si (pois tem a mesma classe pai).

A classe Estudante é filha da classe Pessoa.

A classe Pessoa é pai da classe Estudante.

NOTA: essas relações se extendem, ou seja, podemos ter classes netas, avós, tias, primas, etc...

super()

De acordo com a documentação do Python (tradução livre):

A função super() retorna um proxy de objeto que delega os metodos para uma classe pai ou irmã

basicamente, nos dá acesso aos metodos e atributos da classe pai.

ele também pode ser usado para instanciar um novo objeto, através do __new__

__new__

O __new__ é usado para criar um novo objeto, veja um exemplo:

1class Exemplo(object):  # Herdamos a Classe base da hierarquia <object>
2    def __new__(self):
3        print("Criando Instancia (Instanciando)")
4        return super(Exemplo, self).__new__(
5            self
6        )  # Usamos a classe base para criar um novo objeto
7
8    def __init__(self):
9        print("Inicializando Instancia")
10
11
12Exemplo()
13

Saida


Criando Instancia (Instanciando) Inicializando Instancia

Saida


<__main__.Exemplo at 0x7f6c08513430>

NOTA: Novamente, eu nunca cheguei a precisar usar o __new__ pessoalmente

Introdução básica a Decoradores

um decorador é uma funcionalidade da linguagem que nos permite alterar o funcionamento de funções e classes.

sua sintaxe é simples:

1@nome_do_decorador
2

e ele deve ser posto acima da função que deseja alterar.

NOTA: Não irei me aprofundar muito, pois existe uma seção especifica para eles mais a frente.

Existem varios decoradores, mas irei ressaltar 3

  • @property
  • @classmethod
  • @staticmethod

@property

O @property serve para converter um metodo em atributo.

ele só funciona se o metodo tem apenas self como parametro e retorna alguma operação simples

voltemos ao nosso exemplo do estudante e calculo de notas:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6    def media(self):
7        return sum(self.notas) / len(self.notas)
8
9
10EstudanteExemplo = Estudante("Mirai", [1, 2, 3, 4, 5, 6, 7, 8, 9])
11
12print(f"Media através do método: {EstudanteExemplo.media()}")
13

Saida


Media através do método: 5.0

Veja que devemos executar o método para termos acesso a média, usando o @property nós convertemos esse metodo para um atributo, veja:

1class Estudante:
2    def __init__(self, nome, notas):
3        self.notas = notas
4        self.nome = nome
5
6    @property
7    def media(self):
8        return sum(self.notas) / len(self.notas)
9
10
11EstudanteExemplo = Estudante("Mirai", [1, 2, 3, 4, 5, 6, 7, 8, 9])
12
13print(
14    f"Media através do atributo: {EstudanteExemplo.media}"
15)  # Omitimos a chamada da função
16

Saida


Media através do atributo: 5.0

Porque isso? Bom, se você lembra, metodos devem ser usados para representar ações do objeto, e não para adicionar atributos, exemplificando:

A média não é uma ação que nosso estudante faz, é um valor que ele possui, logo deve ser uma propriedade.

Decoradores de metodos @classmethod e @staticmethod

os decoradores de metodos alteram os metodos para pegar parametros em especifico, até então estavamos usando os instancemethod através do self, esse metodo precisa de uma instancia para performar sua operação.

aqui nós temos a classe Estudante:

1class Estudante:
2    def __init__(self, notas):
3        self.notas = notas
4
5    def media(self):
6        return sum(self.notas) / len(self.notas)
7

basicamente quando fazemos:

1EstudanteExemplo = Estudante([1, 2, 3, 4, 5, 6, 7, 8, 9])
2EstudanteExemplo.media()
3

estamos essencialmente fazendo:

1EstudanteExemplo = Estudante([1, 2, 3, 4, 5, 6, 7, 8, 9])
2Estudante.media(EstudanteExemplo.notas)
3

O metodo media usa a o objeto instanciado EstudanteExemplo para sua execução, ou seja, é um instancemethod

@classmethod

o @classmethod altera a função para usar a classe ao invés do objeto instanciado, veja o exemplo:

1class Foo:
2    @classmethod
3    def hi(cls):
4        print(cls.__name__)
5
6
7objeto = Foo()
8
9objeto.hi()
10

Saida


Foo

@staticmethod

o @staticmethod faz com que os metodos não precise de nenhum argumento, veja o exemplo:

1class Bar:
2    @staticmethod
3    def apresentar():
4        print("eu sou um metodo com @staticmethod")
5
6
7objeto_bar = Bar()
8
9objeto_bar.apresentar()
10

Saida


eu sou um metodo com @staticmethod