Livro de Python PDF – Capítulo 9: Fundamentos de Programação Orientada a Objetos (POO)

Compartilhe:

Para acabarmos os conteúdos referentes a Python,  discutiremos o paradigma da programação orientada a  objetos. A programação orientada a objetos (POO) é, como o  nome diz, baseada em objetos, que são correspondentes aos  da realidade, como: pessoa, carro, entre outros.  

Nesse modelo existem classes, que são como um  gabarito, um molde, para objetos com características  semelhantes. Essas características na POO são chamadas,  também, de atributos. Um exemplo de classe é a classe Livro,  que origina os objetos livro1, livro2, etc. Na classe  exemplificada abaixo, os atributos são: cor da capa, nome do  livro e o número de páginas.

**Os métodos são como funções, só que pertencentes às  classes. Seria como se tivéssemos a classe pessoa e ela  falasse. Para isso, criaríamos um método chamado falar.  Sempre que desenvolvemos uma classe, a primeira coisa  que devemos fazer é criar um construtor, que é um método  “especial” que tem como objetivo criar os futuros objetos.  Nota-se a estrutura de um construtor abaixo: 

def __init__ (self, atributo1, atributo2, …)
    self.atributo1 = atributo1 
    self.atributo2 = atributo2 
    …
#usa-se dois underlines seguidos antes e depois da 
#palavra init 

Podemos observar que, antes de definirmos o construtor,  utilizamos a palavra def, usada para declarar um método. Já a  palavra init é reservada ao construtor. Abaixo, temos um  exemplo real de uma classe pessoa que origina os objetos p1  e p2. 

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
    def falar(self)
        print('ola, tudo bem?'
p1 = Pessoa('Teo', 1)
p2 = Pessoa('Laura',10)
p1.falar()

No exemplo anterior, criamos, além de uma classe com  atributos, o método falar, que imprime na tela a frase ‘ola,  tudo bem?’.  

A palavra self serve para fazer referência à própria  instância que está chamando o método e deve ser utilizada  em todos os atributos que forem criados dentro de uma  classe.  

Temos ainda os métodos de classe, que são tipos  “especiais” e se referem à classe e não ao objeto. Logo, não  é requerida a criação de uma instância. Para implementarmos  o método de classe, utilizamos o decorador @classmethod e,  em seguida, criamos o método. Nesse tipo, ao invés de  utilizamos a palavra self para nos referirmos à instância de  uma classe, um objeto, utilizamos a palavra cls para fazer  referência à classe em si. Abaixo, temos um exemplo, no qual  o intuito é criarmos um objeto da classe pessoa. Ao invés de  inserirmos o nome dela e sua idade, como no exemplo  anterior, criaremos um objeto passando como parâmetro o  nome e o ano de nascimento do indivíduo. Para isso,  usaremos o método de classe. 

class Pessoa:
    ano_atual = 2020
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
    @classmethod
    def por_ano_nascimento(cls, nome, ano_nascimento):
        idade = cls.ano_atual - ano_nascimento
        return cls(nome, idade)
    def falar(self):
        print('ola, tudo bem?')
p1 = Pessoa.por_ano_nascimento('Teo', 2019)

Agora abordaremos os métodos estáticos, que são  aqueles que não dependem nem da classe, nem da  instância. Eles são como funções comuns, porém são  colocados dentro de uma classe para facilitar a organização  do código. Como por exemplo, se quisermos uma função que  gera um id, ou seja, um número aleatório entre 1 e 1000,  podemos torná-la em um método estático que faz parte da  classe Pessoa. 

from random import randint
class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
    @staticmethod
    def gera_id():
        rand = randint(1, 1000)
        return rand
print(Pessoa.gera_id())
#OU
p1 = Pessoa('Teo', 1)
print(p1.gera_id())

Notamos ainda que dentro do método estático gera_id(),  utilizamos a função randint(), que gera números aleatórios.  Os atributos de classe são aqueles que se referem à  classe e não aos objetos dela. Eles podem ser acessados  pelos objetos, mas não por eles alterados. Observa-se isso  no exemplo abaixo: 

class A:
    atributoDeClasse=10 
a1 = A()
a2 = A() 
print(A.atributoDeClasse)
print(a2.atributoDeClasse)
print(a1.atributoDeClasse)

O Python, com relação ao encapsulamento, é diferente da  maioria das outras linguagens de programação OO  (orientadas a objeto). Nas outras, temos uma parte da classe  privada ou protegida e uma parte pública. A parte privada ou  protegida não pode ser acessada diretamente de fora do  objeto, somente por getters e setters. Como em Python não  existe esse tipo de divisão, foi convencionado que se um  atributo ou método tiver um underline o precedendo, então é  como se fosse privado. Porém, embora não seja  recomendado, isso não nos impede de modificá-lo  diretamente. Agora, caso esse atributo seja precedido por  dois underlines, não poderemos acessá-lo diretamente, de  modo algum. Para resolver esse problema, usamos a  seguinte sintaxe:  

nomeDoObjeto._nomeDaClass__nomeDoAtributo

Um exemplo disso é caso tivéssemos como intuito ter uma  classe pessoa com o atributo nome “privado”, como pode ser  visto abaixo: 

class Pessoa:
    def __init__(self, nome, idade):
        self.__nome = nome
        self.idade = idade
p1 = Pessoa('Teo', 1)
print(p1._Pessoa__nome) 

Podemos, também, tanto acessar quanto modificar esse  atributo nome através de Getters e Setters. Os Getters servem  para acessar um atributo “privado”; ou seja, são métodos que  retornam um atributo. Já os Setters são métodos cuja função é receber um valor e, em seguida, a e ele igualar um atributo.  Logo abaixo, temos o mesmo exemplo que utilizamos acima,  mas resolvido de outra forma, com Geters e Setters: 

class Teste:
    def __init__(self):
        self.__x = None
    @property
    def x(self):;
        return self.__x
    @x.setter
    def x(self, value):
        self.__x = value
teste1 = Teste() #estamos criando o objeto teste1
teste1.x = 5 #setter
print(teste1.x) #getter

Podemos notar que, antes de definirmos os métodos  Getter e Setter, utilizamos os decoradores @property para o  get e @nomeDoAtributo.setter e, por fim, nas duas últimas  linhas, invocamos primeiro o método set e, em seguida, o  get.  

Em um programa, podemos ter diversas classes e elas  podem se relacionar entre si de 3 maneiras: associação,  agregação e composição.  

A associação é quando uma classe utiliza a outra, mas não  existe uma relação de interdependência entre elas. Um  exemplo disso é um programa em que uma classe escritor  usa uma classe caneta, como pode ser visto a seguir: 

class Escritor:
    def __init__(self, nome):
        self.nome = nome
        self.ferramenta = None
class Caneta:
    def __init__(self, marca):
        self.marca = marca
    def escrever(self):
        print('a caneta escreve')
escritor = Escritor(‘Teo')
caneta = Caneta('Bic')
escritor.ferramneta = caneta
escritor.ferramneta.escrever()

O segundo tipo é a agregação, na qual uma classe existe  sem a outra, mas há uma relação de dependência entre elas.  Como, por exemplo, uma classe carrinho de compras e uma  

classe compra. Uma existe independente da outra, mas o  carrinho de compras não faria sentido sem compras. Abaixo,  temos a representação, em código, desse exemplo: 

class CarrinhoDeCompras:
    def __init__(self):
        self.produtos = []
    def inserir_produto(self, produto):
        self.produtos.append(produto)
class Produto:
    def __init__(self, nome, valor):
        self.nome = nome
        self.valor = valor
p1 = Produto('caneta', 5)
carrinho = CarrinhoDeCompras()
carrinho.inserir_produto(p1)

Ainda temos a relação por composição, que é a mais forte.  Nela, uma classe é “dona” da outra, isso quer dizer que, se a  classe principal for apagada, todos os objetos que ela utilizou  serão apagados também. Como, por exemplo, no código  abaixo, no qual temos uma loja com inúmeras sedes e  queremos armazenar os endereços em uma lista, utilizando  uma classe endereço:  

class class Loja:
    def __init__(self, nome):
        self.nome = nome
        self.enderencos = []
    def insere_enderecos (self, cidade, estado):
        self.enderencos.append(Endereco(cidade, estado))
class Endereco:
    def __init__(self, cidade, estado):
        self.cidade = cidade
        self.estado = estado
p1 = Loja('Renner')
p1.insere_enderecos('Curitiba', ‘PR')
p1.insere_enderecos('Guaratuba', 'PR')
print(p1.enderencos[0].cidade)
print(p1.enderencos[0].estado)

Pode-se notar no exemplo acima, que a classe loja contém  todos os objetos endereços, ou seja, criaremos um novo  objeto endereço sempre que invocarmos a classe Endereco. 

Por fim, temos a herança, que serve, principalmente, para  reaproveitarmos código. Vamos supor que temos uma classe  pessoa que possui dois atributos, o atributo nome e o idade.  Temos ainda uma classe estudante que, além do nome e da  idade, possui o atributo ano escolar. Nesse caso, poderíamos  herdar o nome e idade da classe pessoa, da seguinte forma:  

class Pessoa:
    def __init__(self, nome, idade): 
        self.nome=nome
        self.idade=idade 
class Aluno(Pessoa):
    def __init__(self, nome, idade, anoEscolar):  Pessoa.__init__(self, nome, idade)
        self.anoEscolar = anoEscolar 
a1 = Aluno('Teo', 10, 'terceiro ano')

🔗 Para acessar todos os capítulos, visite a página: Livro de Lógica de Programação em Python.

Gabriel Selow
Gabriel Selow
Artigos: 21