Pular para o conteúdo principal

Aula 07: Classes e Objetos

Aula 07: Classes e objetos

Nessa aula vamos introduzir o conceito de classes e objetos e os fundamentos de programação orientada por objetos.

Esse é um assunto complexo e extenso e a idéia aqui é passar os fundamentos em Python. A idéia é entender a diferença entre método e função e como usar classes e objetos em Python mais do que criá-las você mesmo.

Esta aula está baseadano capítulo 10 do livro Introdução à Progamação com PYTHON de Nilo Ney Coutinho Menezes, 3a edição, editora Novatec.

Começamos o curso usando o Python como uma calculadora. Depois introduzimos variáveis, estruturas de dados e funções. Á medida que o curso avançou reutilizamos todas os conceitos anteriores.

De certa maneira, qualquer programa, por maior e mais complexo ele seja, é formado de elementos básicos como os que vimos até o momento. A dificuldade na programação é enxergar a floresta sem se perder com as árvores. Qualquer linha é facilmente compreensível se a linguagem de programação for minimamente conhecida.

Programação orientada por objetos (POO)

Entender o que um pedaço de código faz é bem mais complexo, assim como escrever o código. Essa dificuldade permeia toda a história da ciência da computação. Uma das "soluções" para este problema foi a orientação por objetos.

Neste paradigma, objetos representam o problema e um programa é feito através da comunicação entre os objetos através de métodos. Em algumas linguagens como Smalltalk, esse conceito é levado literalmente.

Não existe definição simples de OOP. Mas alguns conceitos são recorrentes:

  • Objetos
  • Classes
  • Encapsulamento
  • Herança

Veremos brevemente esses conceitos aplicados a Python

Objetos como representação do mundo real

Um objeto é uma representação de algo no mundo real, uma representação idealizada e simplificada do mundo real.

Imagine uma televisão. Ela tem algumas características:

  • Pode ser ligada ou desligada
  • Ela está sintonizada em um canal
  • Pode aumentar ou diminuir o som
  • Muitas outras coisas (numa tv moderna)

Então vamos usar uma classe Televisão para caracterizar uma televisão:

In [1]:
class Televisão:  # Definição de uma classe
    def __init__(self):  # Método especial - construtor
        self.ligada = False # atributo 
        self.canal = 2 # outro atributo

Agora podemos criar um objeto Televisão que pode representar uma TV:

In [2]:
tv = Televisão()  # Criar um objeto `Televisão`
tv.ligada
Out[2]:
False
In [3]:
tv.canal
Out[3]:
2
In [4]:
tv_sala = Televisão() # Outro objeto da classe televisão
tv_sala.ligada = True  # Estou ligando a televisão
tv_sala.canal = 4  # Mudei o canal
In [5]:
tv.ligada
Out[5]:
False
In [6]:
tv_sala.ligada
Out[6]:
True
In [7]:
tv.canal
Out[7]:
2
In [8]:
tv_sala.canal
Out[8]:
4

O método __init__ é um método especial usado na construção de objetos de uma classe. Ele é conhecido como construtor.

Exercício 1

Adicione atributos tamanho e marca à classe Televisão. Crie dois objetos Televisão e atribua tamanhos e marcas diferentes. Aí imprima os atribuitos para ver a diferença

In [ ]:
 

Vamos dar alguma funcionalidade à class Televisão:

In [9]:
class Televisão:
    def __init__(self):
        self.ligada = False
        self.canal = 2
        
    def muda_canal_para_baixo(self):
        self.canal -= 1
    def muda_canal_para_cima(self):
        self.canal += 1
In [10]:
tv = Televisão()
tv.canal
Out[10]:
2
In [11]:
tv.muda_canal_para_cima()
tv.muda_canal_para_cima()
tv.canal
Out[11]:
4
In [12]:
tv.muda_canal_para_baixo()
tv.canal
Out[12]:
3
In [ ]:
 

Passagem de parâmetros

Será que pode ter canal negativo? Ou o canal 8723245?

In [13]:
class Televisão:
    def __init__(self, min, max):
        self.ligada = False
        self.canal = 2
        self.cmin = min
        self.cmax = max
        
    def muda_canal_para_baixo(self):
        if self.canal -1 >= self.cmin:
            self.canal -= 1
    
    def muda_canal_para_cima(self):
        if self.canal +1 <= self.cmax:
            self.canal += 1
In [14]:
tv = Televisão(2,13)
In [15]:
for i in range(100):
    tv.muda_canal_para_cima()
print(tv.canal)
13
In [16]:
for i in range(100):
    tv.muda_canal_para_baixo()
print(tv.canal)
2

Exercício 2

Atualmente a classe Televisão inicializa o canal com 2. Modifique a classe de forma a receber o canal inicial em seu construtor.

In [ ]:
 

Exercício 3

Modifique a classe Televisão de modo que se pedirmos para mudar o canal para baixo, além do mínimo, ele vái para o máximo e vice e versa

In [ ]:
 

Exercício 4

Utilizando o que aprendemos com funções modifique o construtor de classe Telefisão de forma que min e max sejam parâmetros opcionais em que min vale 2 e max vale 13 caso outros valores não sejam passados

In [ ]:
 

Exercício 5

Utilizando a classe modificada no exercício anterior (4), crie duas instâncias (objetos) especificando min e max por nome.

In [ ]:
 

Exemplo de um Banco

Vamos tentar modelar contas correntes de um banco

In [17]:
class Cliente:
    def __init__(self, nome, telefone):
        self.nome = nome
        self.telefone = telefone
        
In [18]:
joão = Cliente("João da Silva", "555-0178")
bozo = Cliente("Bozo Palhaço", "236-0873")
In [19]:
joão.nome
Out[19]:
'João da Silva'
In [20]:
type(joão)
Out[20]:
__main__.Cliente
In [21]:
joão.telefone
Out[21]:
'555-0178'
In [22]:
bozo.nome
Out[22]:
'Bozo Palhaço'
In [23]:
bozo.telefone
Out[23]:
'236-0873'

Em geral vamos armazenar isso num módulo. Para isso criamos o arquivo clientes.py e importamos:

In [24]:
from clientes import Cliente
In [25]:
joão = Cliente("João da Silva", "555-0178")
bozo = Cliente("Bozo Palhaço", "236-0873")
In [26]:
type(joão)
Out[26]:
clientes.Cliente

Agora precisamos de um outro módulo para armazenar a class Conta que representa uma conta corrente.

In [27]:
# Código para recarregar automaticamente os módulos que 

%load_ext autoreload
%autoreload 2
In [28]:
from contas import Conta
In [29]:
conta = Conta(joão, "234.567-01", 1000)
In [30]:
conta.resumo()
CC Número: 234.567-01. Saldo:    1000.00
In [31]:
conta.saque(100)
conta.resumo()
CC Número: 234.567-01. Saldo:     900.00
In [32]:
conta.saque(358)
conta.resumo()
CC Número: 234.567-01. Saldo:     542.00
In [33]:
conta.deposito(674)
conta.resumo()
CC Número: 234.567-01. Saldo:    1216.00
In [34]:
conta.saque(2000)
conta.resumo()
CC Número: 234.567-01. Saldo:    1216.00
In [35]:
conta.saque(900)
conta.resumo()
CC Número: 234.567-01. Saldo:     316.00
In [36]:
conta.extrato()
Extrato CC No 234.567-01

DEPÓSITO      1000.00
SAQUE          100.00
SAQUE          358.00
DEPÓSITO       674.00
SAQUE          900.00

    Saldo:     316.00

Exercício 6

Altere o programa de forma que a mensagem saldo insuficiente seja exibida caso haja tentativa de sacar mais dinheiro que o saldo disponível

In [ ]:
 

Exercício 7

Modifique o método resumo da class Conta para exibir o nome e o telefone de cada cliente

In [37]:
from clientes import Cliente
from bancos import Banco
from contas import Conta
In [38]:
bozo = Cliente("Bozo Palhaço", "236-0873")
vovó = Cliente("Vovó Mafalda", "236-0873")
josé = Cliente("José Vargas", "3866-1234")
In [39]:
contaBV = Conta([bozo, vovó], 1, 100)
contaJ = Conta([josé], 2, 50)
In [40]:
tatu = Banco("Tatú")
tatu.abre_conta(contaBV)
tatu.abre_conta(contaJ)
In [41]:
tatu.lista_contas()
CC Número: 1. Saldo:     100.00
CC Número: 2. Saldo:      50.00
In [ ]:
 

Herança

Herança é um recurso da programação orientada por objetos que permite reutilizar código, modificando sua característica

In [42]:
from contas import ContaEspecial
In [43]:
bozo = Cliente("Bozo Palhaço", "236-0873")
vovó = Cliente("Vovó Mafalda", "236-0873")
In [44]:
conta1 = Conta(["bozo"], 1, 1000)
conta2 = ContaEspecial([vovó, bozo], 2, 500, 1000)
In [45]:
conta1.saque(50)
conta1.resumo()
CC Número: 1. Saldo:     950.00
In [46]:
conta2.deposito(300)
conta2.resumo()
CC Número: 2. Saldo:     800.00
In [47]:
conta1.saque(190)
conta1.resumo()
CC Número: 1. Saldo:     760.00
In [48]:
conta2.deposito(95.15)
conta2.resumo()
CC Número: 2. Saldo:     895.15
In [49]:
conta2.saque(1500)
conta2.resumo()
CC Número: 2. Saldo:    -604.85
In [50]:
conta1.extrato()
Extrato CC No 1

DEPÓSITO      1000.00
SAQUE           50.00
SAQUE          190.00

    Saldo:     760.00

In [51]:
conta2.extrato()
Extrato CC No 2

DEPÓSITO       500.00
DEPÓSITO       300.00
DEPÓSITO        95.15
SAQUE         1500.00

    Saldo:    -604.85

Exercício 8

Modofique as classes Conta e ContaEspecial para que a operação de saque retorne verdadeiro se o saque foi realizado e false caso contrário

In [ ]:
 

Exercício 9

Modifique a class ContaEspecial de forma que seu extrato exiba o limite e o total disponível para saque.

In [ ]:
 

Desenvolvendo uma classe para controlar listas

In [52]:
class ListaUnica:
    def __init__(self, elem_class):
        self.lista = []
        self.elem_class = elem_class
    def __len__(self):
        return len(self.lista)
    def __iter__(self):
        return iter(self.lista)
    def __getitem__(self, p):
        return self.lista[p]
    def indiceValido(self, i):
        return i >= 0 and i < len(self.lista)
    def adiciona(self, elem):
        if self.pesquisa(elem) == -1:
            self.lista.append(elem)
    def remove(self, elem):
        self.lista.remove(elem)
    def pesquisa(self, elem):
        self.verifica_tipo(elem)
        try:
            return self.lista.index(elem)
        except ValueError:
            return -1
    def verifica_tipo(self, elem):
        if not isinstance(elem, self.elem_class):
            raise TypeError("Tipo inválido")
    def ordena(self, chave=None):
        self.lista.sort(key=chave)
                         
    
    
In [53]:
lu = ListaUnica(int)
In [54]:
lu.adiciona(5)
In [55]:
lu.adiciona(3)
In [56]:
lu.adiciona(2.5)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_41173/78436172.py in <module>
----> 1 lu.adiciona(2.5)

/tmp/ipykernel_41173/263908826.py in adiciona(self, elem)
     12         return i >= 0 and i < len(self.lista)
     13     def adiciona(self, elem):
---> 14         if self.pesquisa(elem) == -1:
     15             self.lista.append(elem)
     16     def remove(self, elem):

/tmp/ipykernel_41173/263908826.py in pesquisa(self, elem)
     17         self.lista.remove(elem)
     18     def pesquisa(self, elem):
---> 19         self.verifica_tipo(elem)
     20         try:
     21             return self.lista.index(elem)

/tmp/ipykernel_41173/263908826.py in verifica_tipo(self, elem)
     24     def verifica_tipo(self, elem):
     25         if not isinstance(elem, self.elem_class):
---> 26             raise TypeError("Tipo inválido")
     27     def ordena(self, chave=None):
     28         self.lista.sort(key=chave)

TypeError: Tipo inválido
In [57]:
len(lu)
Out[57]:
2
In [58]:
for e in lu:
    print(e)
5
3
In [59]:
lu.adiciona(5)
In [60]:
len(lu)
Out[60]:
2
In [61]:
lu[0]
Out[61]:
5
In [62]:
lu[1]
Out[62]:
3
In [63]:
id(lu)
Out[63]:
139760455345056

Outro exemplo com métodos "mágicos"

In [64]:
class Nome:
    def __init__(self, nome):
        if nome is None or not nome.strip():
            raise ValueError("Nome não pode ser nulo nem um branco")
        self.nome = nome
        self.chave = nome.strip().lower()
    def __str__(self):
        return self.nome
    def __repr__(self):
        return f"<Classe {type(self).__name__} em 0x{id(self):x} Nome: {self.nome} Chave: {self.chave}>"
    def __eq__(self, outro):
        print("__eq__ Chamado")
        return self.nome == outro.nome
    def __lt__(self, outro):
        print("__lt__ Chamado")
        return self.nome < outro.nome
    
In [65]:
A = Nome("Nilo")
In [66]:
print(A)
Nilo
In [67]:
B = Nome("  ")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_41173/1734329681.py in <module>
----> 1 B = Nome("  ")

/tmp/ipykernel_41173/3332852593.py in __init__(self, nome)
      2     def __init__(self, nome):
      3         if nome is None or not nome.strip():
----> 4             raise ValueError("Nome não pode ser nulo nem um branco")
      5         self.nome = nome
      6         self.chave = nome.strip().lower()

ValueError: Nome não pode ser nulo nem um branco
In [68]:
C = Nome(None)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_41173/3476832770.py in <module>
----> 1 C = Nome(None)

/tmp/ipykernel_41173/3332852593.py in __init__(self, nome)
      2     def __init__(self, nome):
      3         if nome is None or not nome.strip():
----> 4             raise ValueError("Nome não pode ser nulo nem um branco")
      5         self.nome = nome
      6         self.chave = nome.strip().lower()

ValueError: Nome não pode ser nulo nem um branco
In [69]:
A == Nome("Nilo")
__eq__ Chamado
Out[69]:
True
In [70]:
A != Nome("Nilo")
__eq__ Chamado
Out[70]:
False
In [71]:
A < Nome("Nilo")
__lt__ Chamado
Out[71]:
False
In [72]:
A > Nome("Nilo")
__lt__ Chamado
Out[72]:
False
In [73]:
A >= Nome("Nilo")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_41173/816388600.py in <module>
----> 1 A >= Nome("Nilo")

TypeError: '>=' not supported between instances of 'Nome' and 'Nome'

Criando exceções

Podemos criar novos tipos de exceção para diferenciar os erros gerados nos nossos programas.

Para isso, é é necessário extender a class Exception

In [74]:
class NovaException(Exception):
    pass
In [75]:
def lançar_erro():
    raise NovaException
    
In [76]:
try:
    lançar_erro()
except NovaException as n:
    print("Uma excecão de tipo NovaExceção foi lançada")
Uma excecão de tipo NovaExceção foi lançada

As exceções podem ter informações ou atributos

In [77]:
class EstoqueException(Exception):
    def __init__(self, mensagem, codigo_de_erro):
        super().__init__(mensagem)
        self.codigo_de_erro = codigo_de_erro
In [78]:
def verifique_quantidade(quantidade):
    if quantidade < 0:
        raise EstoqueException("Quantidade negativa", codigo_de_erro=1)
In [79]:
try:
    verifique_quantidade(-10)
except EstoqueException as ee:
    print(f"Erro: {ee.codigo_de_erro} {ee}")
    
Erro: 1 Quantidade negativa
In [ ]:
 
In [ ]:
 

Comentários

Comments powered by Disqus