Programação orientada a objetos - Parte 1#

Na evolução das linguagens de programação, a necessidade de se representar e manipular informações complexas resultou no conceito de classes e objetos, onde classes servem como abstrações (representação) e objetos seriam instâncias de classes que mantém e permitem a manipulação da informação.

image.png

Em Python, toda informação que usamos é representada na forma de um objeto. Assim, o número 6 é um objeto da classe int, o número 3.14 é um objeto da classe float, e assim por diante.

print(type(6))  # type mostra o tipo do objeto passado como argumento
print(id(6))    # mostra o local na memória (endereço) onde o objeto está armazenado
i = 6
j = 6
print(id(i))    # note que i e j correspondem ao mesmo objeto 6
print(id(j))

Em computação, criar um objeto significa criar uma instância de uma classe.

Linguagens orientadas a objetos permitem a definição de novas classes.

Uma classe é uma abstração de alguma “coisa”, que possui um estado e comportamento.

Um estado é definido por um conjunto de variáveis chamadas de atributos.

Esses estados podem ser alterados por meio de “ações” sobre o objeto, que definem seu comportamento.

Essas ações são funções chamadas de “métodos”.

image.png

Alt text

image.png

Alt text

image.png

Por exemplo, para representar um carro podemos definir:

  • uma classe Carro:

  • com os atributos: ano, modelo, cor, e vel

  • e os métodos:

    • acelera(vel): acelera até vel

    • pare(): vel = 0

Alt text

class Carro:
    pass

# teste da classe Carro
fusca    = Carro()   # cria um objeto referenciado pela variavel fusca
brasilia = Carro()

fusca.ano = 1968
fusca.modelo = 'Fusca'
fusca.cor = 'preto'

brasilia.ano = 1981
brasilia.modelo = 'Brasilia'
brasilia.cor = 'amarela'
novo_fusca = fusca

print("Fusca igual a brasilia? > ", fusca == brasilia)
print("Fusca igual a novo_fusca? > ", fusca == novo_fusca)

fusca.ano += 10

print("fusca.ano = ", fusca.ano)
print("novo_fusca.ano = ", novo_fusca.ano)

O método especial init (construtor)#

Como sabemos que todos os carros que queremos representar possuem as propriedades “cor”, “ano”, e “modelo”, podemos deixar essas propriedades como atributos da classe e inicializá-las quando um objeto é instanciado (criado).

Para isso, vamos utilizar o método especial init, conhecido como construtor da classe.

class Carro:
    def __init__(self, m, a, c):
        self.modelo = m
        self.ano    = a
        self.cor    = c

brasilia = Carro('Brasilia', 1968, 'amarela')

class Fusca(Carro):
    pass
   
fusca_padrao = Fusca('Fusca', 1981, 'preto')

novo_fusca = Fusca('Novo_fusca',1981,'azul')

novo_fusca.ano += 10   # observe que podemos acessar atributos de fusca

print(fusca_padrao.ano)

print(novo_fusca.ano)
1981
1991

Mas o que é esse self?#

Todo método de uma classe recebe como primeiro parâmetro uma referência à instância que chama o método, permitindo assim que o objeto acesse os seus próprios atributos e métodos.

Por convenção, chamamos esse primeiro parâmetro de self, mas qualquer outro nome poderia ser utilizado.

O método especial init é chamado automaticamente após a criação de um objeto, e no caso de um Carro, na nova instância são criados os atributos ‘modelo’, ‘ano’ e ‘cor’ com os valores dados como argumentos do construtor (no caso Carro(“Brasilia”, 1968, “amarela”)).

Assim, no corpo de qualquer método, um atributo pode ser criado, acessado ou modificado usando self.nome_do_atributo.

Para exemplificar a criação de outros métodos e entender melhor o papel do self vamos criar os métodos imprima, acelere e pare. Por ser um exemplo mais elaborado, vamos também colocar os testes dentro da função main.

def main():
    brasilia = Carro('brasilia', 1968, 'amarela', 80)
    fuscao = Carro('fuscao', 1981, 'preto', 95)

#Na função main, brasilia e fuscao são instâncias de Carro. Para chamar o método acelere de Carro, usamos a notação:
 #   Carro.acelere(brasilia, 40)    
 #    Carro.acelere(fuscao, 50)
 #   Carro.acelere(brasilia, 80)
    
 #   Carro.pare(brasilia)
    Carro.acelere(fuscao, 100)

    
class Carro:
    def __init__(self, m, a, c, vm):
        self.modelo = m
        self.ano    = a
        self.cor    = c
        self.vel    = 0
        self.maxV   = vm  # velocidade maxima

    def imprima(self):
        if self.vel == 0: # parado da para ver o ano
            print( "%s %s %d"%(self.modelo, self.cor, self.ano)     )
        elif self.vel < self.maxV:
            print( "%s %s indo a %d Km/h"%(self.modelo, self.cor, self.vel) )
        else:
            print( "%s %s indo muito raaaaaapiiiiiiiidoooooo!"%(self.modelo, self.cor))

    def acelere(self, v):
        self.vel = v
        if self.vel > self.maxV:
            self.vel = self.maxV
        Carro.imprima(self)

    def pare(self):
        self.vel = 0
        Carro.imprima(self)

main()
fuscao preto indo muito raaaaaapiiiiiiiidoooooo!

Essa notação explicita a passagem dos objetos como primeiro parâmetro em cada método, mas é redundante visto que todo objeto sabe a que classe ele pertence. Uma notação mais comum e enxuta é chamar os métodos usando a notação com . de forma semelhante aos atributos e, como o primeiro argumento é sempre ele mesmo, ele pode ser evitado. Assim a chamada:

Carro.acelere.(brasilia, 40)

pode ser escrita como:

brasilia.acelere(40)

def main():
    brasilia = Carro('brasilia', 1968, 'amarela', 80)
    fuscao = Carro('fuscao', 1981, 'preto', 95)

    brasilia.acelere(40)
    fuscao.acelere(50)
    brasilia.acelere(80)
    brasilia.pare()
    fuscao.acelere(100)

class Carro:
    def __init__(self, m, a, c, vm):
        self.modelo = m
        self.ano    = a
        self.cor    = c
        self.vel    = 0
        self.maxV   = vm  # velocidade maxima

    def imprima(self):
        if self.vel == 0: # parado da para ver o ano
            print( "%s %s %d"%(self.modelo, self.cor, self.ano)     )
        elif self.vel < self.maxV:
            print( "%s %s indo a %d Km/h"%(self.modelo, self.cor, self.vel) )
        else:
            print( "%s %s indo muito raaaaaapiiiiiiiidoooooo!"%(self.modelo, self.cor))

    def acelere(self, v):
        self.vel = v
        if self.vel > self.maxV:
            self.vel = self.maxV
        self.imprima()

    def pare(self):
        self.vel = 0
        self.imprima()

main()
brasilia amarela indo a 40 Km/h
fuscao preto indo a 50 Km/h
brasilia amarela indo muito raaaaaapiiiiiiiidoooooo!
brasilia amarela 1968
fuscao preto indo muito raaaaaapiiiiiiiidoooooo!

O método especial str#

O método imprima é muito útil para qualquer tipo de objeto mas não é a forma mais comum utilizando objetos. Em muitos casos, é desejável simplesmente utilizar a função print para imprimir uma “representação textual” do objeto.

Na verdade, quando executamos o comando print(6), como o 6 é um objeto da classe int, a função print converte o inteiro 6 no string 6 antes de ser impresso, algo como print( str(6) ). Lembre-se que, dentre as funções para conversão de tipos (lembra do int em int(input(“Digite um número: ”))?), a função str converte o inteiro 6 para o string 6.

Para exibir então o conteúdo de um objeto usando print, devemos definir o método especial str, e devolver um string com a representação textual do objeto. A função str também chama e retorna o valor desse método (se existir).

O código abaixo é basicamente o mesmo que o anterior, mas substituimos o método imprima pelo método especial str. No corpo desse método, ao invés de chamar a função print, carrega-se um string apropriado em s, que é retornado pelo método para a função print ou str. Veja que nos métodos acelere e pare, basta chamar print(self).

def main():
    brasilia = Carro('brasilia', 1968, 'amarela', 80)
    fuscao = Carro('fuscao', 1981, 'preto', 95)

    print(brasilia)
    print(fuscao)

    brasilia.acelere(40)
    fuscao.acelere(50)
    brasilia.acelere(80)
    brasilia.pare()
    fuscao.acelere(100)

class Carro:
    def __init__(self, m, a, c, vm):
        self.modelo = m
        self.ano    = a
        self.cor    = c
        self.vel    = 0
        self.maxV   = vm  # velocidade maxima

    def __str__(self):
        if self.vel == 0: # parado da para ver o ano
            s = "%s %s %d"%(self.modelo, self.cor, self.ano)
        elif self.vel < self.maxV:
            s = "%s %s indo a %d Km/h"%(self.modelo, self.cor, self.vel)
        else:
            s = "%s %s indo muito raaaaaapiiiiiiiidoooooo!"%(self.modelo, self.cor)
        return s

    def acelere(self, v):
        self.vel = v
        if self.vel > self.maxV:
            self.vel = self.maxV
        print(self)

    def pare(self):
        self.vel = 0
        print(self)

main()
brasilia amarela 1968
fuscao preto 1981
brasilia amarela indo a 40 Km/h
fuscao preto indo a 50 Km/h
brasilia amarela indo muito raaaaaapiiiiiiiidoooooo!
brasilia amarela 1968
fuscao preto indo muito raaaaaapiiiiiiiidoooooo!
class Carro:
    def __init__(self):
        self.cor = "Preto"
        self.quatidade_de_lugares = 7
        self.velocidade_maxima = 200
        self.ligado = False
        self.marcha = 1
        self.velocidade = 0

    def Ligar(self):
        self.ligado = True

    def Acelerar(self):
        self.velocidade += 10

    def Freiar(self):
        self.velocidade -= 10

    def Trocar_Marcha(self, nova_marcha):
        self.marcha = nova_marcha

    def Desligar(self):
        self.ligado = False


class Celta(Carro):
    def __init__(self):
        Carro.__init__(self)
        self.cor = "Prata"
        self.quantidade_de_lugares = 5
        self.ar_condicionado_ligado = False

    def Ligar_ar_condicionado(self):
        self.ar_condicionado_ligado = True

    def Ligar(self):
        self.ligado = True
        self.marcha = 6


celta_1=Celta()
print(celta_1.cor)

celta_1.Ligar_ar_condicionado()
print(celta_1.ar_condicionado_ligado)
Prata
True
celta_1=Celta()
print(celta_1.cor)

celta_1.Ligar_ar_condicionado()
print(celta_1.ar_condicionado_ligado)
Prata
True

Importação de classes#

Salvar classes em arquivos separados e importá-las em outros programas é uma prática comum em programação. Essa abordagem é usada para organizar o código, facilitar a reutilização e melhorar a manutenção. A seguir, explica-se como isso funciona, por que é vantajoso e como implementá-lo, usando Python como exemplo.


O que significa salvar classes em arquivos separados?#

Em vez de definir todas as classes e funcionalidades de um programa em um único arquivo, você pode dividir seu código em vários arquivos, cada um contendo classes ou funções relacionadas. Esses arquivos são chamados de módulos e podem ser importados em outros programas.


Por que usar essa abordagem?#

  1. Organização: O código fica mais limpo e fácil de entender quando funções e classes relacionadas são separadas em arquivos diferentes.

  2. Reutilização: Classes e funções podem ser reaproveitadas em outros projetos ou partes do mesmo programa, sem a necessidade de reescrever o código.

  3. Manutenção: É mais fácil encontrar e corrigir bugs ou adicionar funcionalidades quando o código está dividido em partes lógicas.

  4. Colaboração: Em projetos maiores, diferentes membros da equipe podem trabalhar em arquivos diferentes sem conflitos.

  5. Modularidade: Facilita a substituição ou atualização de partes específicas do código sem impactar o restante.


Como salvar e importar classes?#

  1. Criar um arquivo com a classe
    Salve sua classe em um arquivo separado. Por exemplo, em um diretório chamado módulos, vamos salvar:

    Carro.py:

    class Carro:
     def __init__(self):
         self.cor = "Preto"
         self.quatidade_de_lugares = 7
         self.velocidade_maxima = 200
         self.ligado = False
         self.marcha = 1
         self.velocidade = 0
    
     def Ligar(self):
         self.ligado = True
    
     def Acelerar(self):
         self.velocidade += 10
    
     def Freiar(self):
         self.velocidade -= 10
    
     def Trocar_Marcha(self, nova_marcha):
         self.marcha = nova_marcha
    
     def Desligar(self):
         self.ligado = False
         
    class Celta(Carro):
     def __init__(self):
         Carro.__init__(self)
         self.cor = "Prata"
         self.quantidade_de_lugares = 5
         self.ar_condicionado_ligado = False
    
     def Ligar_ar_condicionado(self):
         self.ar_condicionado_ligado = True
    
     def Ligar(self):
         self.ligado = True
         self.marcha = 6
    
  2. Importar a classe em outro arquivo
    Em outro arquivo, importe a classe e use-a:

    main.py:

    from modulos.Carro import Celta
    
    celta_1=Celta()
    
    print(celta_1.cor)
    
    celta_1.Ligar_ar_condicionado()
    
    print(celta_1.ar_condicionado_ligado))
    
  3. Executar o programa principal
    Execute o arquivo main.py. Ele usará a classe definida em Carro.py.


Outras formas de importação#

  • Importar várias classes de um arquivo:

    from meu_modulo import Classe1, Classe2
    
  • Importar o módulo inteiro:

    import meu_modulo
    
    obj = meu_modulo.Classe1()
    
  • Usar apelidos para simplificar:

    import meu_modulo as mm
    
    obj = mm.Classe1()
    

Por que é uma boa prática?#

  • Evita duplicação de código: Você não precisa recriar a mesma classe em diferentes partes do projeto.

  • Melhora a legibilidade: Programas grandes tornam-se mais fáceis de entender.

  • Facilita a manutenção: Alterações em uma classe não exigem edições em múltiplos arquivos.

  • Promove testes unitários: Cada classe ou módulo pode ser testado separadamente.


Essa abordagem modular é um dos princípios fundamentais da programação orientada a objetos e do desenvolvimento de software escalável.

No exemplo do bloco a seguir a sub-classe Celta é importada da classe Carro.py salva no diretório modulos.

from modulos.Carro import Celta

celta_1=Celta()
print(celta_1.cor)

celta_1.Ligar_ar_condicionado()
print(celta_1.ar_condicionado_ligado)
Prata
True