Classes são modelos de estruturas para definir objetos com características e comportamentos próprios, através do encapsulamento dos atributos e métodos necessários ao modelo, usando a POO - Programação Orientada a Objetos (OOP - Object Oriented Programming).
A POO é um paradigma de programação que se baseia na ideia de objetos como entidades fundamentais, possuindo características (atributos) e comportamentos (métodos) próprios e permitindo a modelagem e organização de problemas complexos em estruturas mais gerenciáveis e reutilizáveis.
Essa é uma abordagem poderosa e amplamente utilizada na programação que nos permite modelar e resolver problemas complexos de forma mais intuitiva e eficiente.
Python é uma linguagem de programação que suporta vários paradigmas, incluindo a POO, a programação procedural e a programação funcional.
Embora seja capaz de suportar programação orientada a objetos, Python não é estritamente uma linguagem de programação exclusivamente orientada a objetos.
Ela oferece suporte a muitos conceitos de POO, como classes, objetos, herança, polimorfismo e encapsulamento, mas também permite o uso de paradigmas diferentes, dependendo das necessidades do desenvolvedor.
Em Python, você pode escrever código que não segue necessariamente a abordagem orientada a objetos.
No entanto, a linguagem fornece todas as ferramentas e recursos necessários para a criação e manipulação de objetos, permitindo a utilização de técnicas e princípios orientados a objetos de forma eficaz, se desejado.
Em Python, uma classe é definida usando a palavra-chave class, seguida pelo nome da classe e dois pontos.
O corpo da classe contém os atributos e métodos que descrevem as características e comportamentos dos objetos.
No Python temos variáveis e funções globais e locais.
Nas classes, é uma convenção as variáveis internas serem referidas como atributos e as funções internas como métodos.
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def saudar(self):
print(f"Olá, meu nome é {self.nome} e tenho {self.idade} anos.")
Neste exemplo, criamos a classe Pessoa com os atributos nome e idade, e o método saudar().
O método __init__() é um método especial chamado de método-construtor, executado automaticamente na instanciação do objeto da classe, inicializando os seus atributos.
Neste exemplo, o método __init__() recebe os argumentos nome e idade, inicializando os atributos self.nome e self.idade dos objetos individuais da classe Pessoa instanciados.
O argumento self é uma palavra-chave reservada que contém o endereço do objeto que chamou o método, ou seja, a própria instância de objeto com atributos próprios.
Nos objetos, os atributos são variáveis que pertencem a cada objeto individualmente, e os métodos são funções compartilhadas entre os objetos da classe, operando os atributos de cada objeto separadamente, em função do argumento self.
Para criar um objeto a partir de uma classe, chamamos a classe como se fosse uma função, processo chamado de instanciamento em que criamos um objeto a partir da classe.
pessoa1 = Pessoa("João", 30)
pessoa2 = Pessoa("Maria", 25)
Neste exemplo, instanciamos e atribuimos nas variáveis pessoa1 e pessoa2 dois objetos da classe Pessoa.
Os atributos de instância são variáveis que pertencem a cada objeto individualmente.
Cada objeto criado a partir da classe terá seus próprios valores para esses atributos.
Os métodos de instância são funções que operam nos atributos do objeto e são compartilhados entre os objetos da classe.
print(f"Dados de pessoa1: {pessoa1.nome}, {pessoa1.idade}")
print(f"Dados de pessoa2: {pessoa2.nome}, {pessoa2.idade}")
print()
pessoa1.saudar()
pessoa2.saudar()
Neste exemplo, acessamos os atributos nome e idade de cada objeto pessoa1 e pessoa2, e chamamos o método saudar() em cada um deles.
Existem diferentes tipos de programação, e o Python é uma linguagem de uso geral que suporta vários paradigmas, incluindo Programação Orientada a Objetos (POO), Programação Procedural e Programação Funcional.
A Programação Funcional baseia-se em funções, considerando-nas cidadãs de primeira classe, permitindo passá-las como argumentos e retorná-las de outras funções.
Ocorre a imutabilidade de Dados evitando-se a alteração de variáveis, com operações gerando novos dados.
A recursão é comum na programação funcional, e laços (loops) são evitados em favor da chamada de funções recursivas.
São exemplos de linguagens que suportam a programação funcional: Haskell, Lisp e Clojure.
No Python utilizamos as funções integradas sum, sub, mul e div para exemplificar programação com operações de soma, subtração, multiplicação e divisão usadas recursivamente.
def soma(a,b): # soma
return a + b
def subtrai(a,b): # subtração
return a - b
def multiplica(a,b): # multiplicação
return a * b
def divide(a,b): # divisão
return a / b
resultado = soma(divide(multiplica(subtrai(21, 7), divide(soma(35, 25),5)), 4), multiplica(10, 5))
print(resultado)
A Programação Procedural baseia-se na estruturação da programação com procedimentos e funções, permitindo melhor organização e otimização do código para realização de tarefas específicas.
Os procedimentos são rotinas que realizam tarefas específicas, recebendo dados de entrada como argumentos e não retornando dados de saída, enquanto as funções são procedimentos que retornam dados de saída como resultado.
O foco está na sequência de passos que definem o roteiro sequencial de instruções a serem executadas, cujo fluxo pode ser controlados pelas estruturas condicionais e de repetição ou chamadas de funções.
Não há ênfase em objetos, apenas em funções não pertencentes a classes.
No Python, procedimentos que não retornam dados de saída e são atribuidos a uma variável, implicam na atribuição de None a esta variável.
Alguns exemplos de linguagens que suportam a programação procedural mas não suportam POO são: C, Fortran, Cobol.
No exemplo abaixo, a função validar_cpf() verifica se o formato do CPF é valido.
import re
def validar_cpf(cpf: str) -> bool:
""" Efetua a validação do CPF, tanto formatação quando dígito verificadores.
Parâmetros:
cpf (str): CPF a ser validado
Retorno:
bool:
- Falso, quando o CPF não possuir o formato 999.999.999-99;
- Falso, quando o CPF não possuir 11 caracteres numéricos;
- Falso, quando os dígitos verificadores forem inválidos;
- Verdadeiro, caso contrário.
Exemplos:
>>> validar_cpf('529.982.247-25')
True
>>> validar_cpf('52998224725')
False
>>> validar_cpf('111.111.111-11')
False
"""
# Verifica a formatação do CPF
if not re.match(r'\d{3}\.\d{3}\.\d{3}-\d{2}', cpf):
return False
# Obtém apenas os números do CPF, ignorando pontuações
numeros = [int(digit) for digit in cpf if digit.isdigit()]
# Verifica se o CPF possui 11 números ou se todos são iguais:
if len(numeros) != 11 or len(set(numeros)) == 1:
return False
# Validação do primeiro dígito verificador:
soma_dos_produtos = sum(a*b for a, b in zip(numeros[0:9], range(10, 1, -1)))
digito_esperado = (soma_dos_produtos * 10 % 11) % 10
if numeros[9] != digito_esperado:
return False
# Validação do segundo dígito verificador:
soma_dos_produtos = sum(a*b for a, b in zip(numeros[0:10], range(11, 1, -1)))
digito_esperado = (soma_dos_produtos * 10 % 11) % 10
if numeros[10] != digito_esperado:
return False
return True
Testamos a função:
print(f"CPF 529.982.247-25: {validar_cpf('529.982.247-25')}")
print(f"CPF 52998224725: {validar_cpf('52998224725')}")
print(f"CPF 111.111.111-11: {validar_cpf('111.111.111-11')}")
Finalmente temos a Programação Orientada a Objetos (POO), centrada na criação de objetos encapsulando dados (atributos) e funcionalidades (métodos) próprios, escondem detalhes internos e expondo uma interface para interagir com eles.
A herança permite a criação de novas classes (subclasses) baseadas em classes existentes (superclasses), herdando seus atributos e métodos, e o polimorfismo permite que iferentes objetos possam responder de maneira distinta a uma mesma mensagem ou interface.
São exemplos de linguagens que suportam a POO: Python, Java e C++ são linguagens que suportam fortemente a POO.
A classe Animal a seguir contém o método-construtor __init__() e o método fazer_som(), que imprime 'som indefinido'.
class Animal:
def __init__(self, nome=None):
self.nome = nome
def fazer_som(self):
print("som indefinido")
Instanciamos na variável animal um objeto a classe Animal e chamamos o método fazer_som() da variável.
animal = Animal()
animal.fazer_som()
As classes Cachorro e Gato a seguir são descendentes da classe Animal, herdando os seus atributos e métodos, mas sobrescrevendo o método fazer_som() para o comportamento específico da classe descendente de animal.
class Cachorro(Animal):
def fazer_som(self):
print("au au")
class Gato(Animal):
def fazer_som(self):
print("miau")
O método fazer_som() de cada classe descendente é sobreposta (sobrescrita) permitindo aos cachorros fazerem "au au" e aos gatos fazerem "miau".
Instanciamos as variáveis cachorro e gato como objetos da classe Cachorro e Gato, respectivamente.
cachorro = Cachorro("Rex")
gato = Gato("Bolinha")
A seguir chamamos o método fazer_som() de cada objeto.
cachorro.fazer_som()
gato.fazer_som()
A classe Animal é ancestral das classes Cachorro e Gato.
A herança é um mecanismo que permite criar novas classes descendentes (subclasses) a partir de classes existentes (classes base ou superclasses).
A classe descendente herda todos os atributos e métodos da classe ancestral, podendo adicionar novos atributos e métodos ou modificar os existentes.
Neste exemplo, a classe Animal é a classe ancestral, e as classes Cachorro e Gato são classes descendentes da classe Animal, e que herdam os seus atributos e métodos, redefinindo o método fazer_som() para fornecer seu próprio comportamento específico.
Esses paradigmas de programação oferecem abordagens distintas para desenvolver software.
A escolha entre eles depende da linguagem, das necessidades do projeto, da preferência do programador e da adequação do paradigma à resolução do problema em questão.
Às vezes, é possível utilizar conceitos de diferentes paradigmas em um mesmo projeto, aproveitando as vantagens de cada um para resolver desafios específicos.
O encapsulamento é um dos princípios fundamentais da Programação Orientada a Objetos (POO) e refere-se à abstração de ideias em torno de dados (atributos) e comportamentos (métodos) definindo modelos padronizados e utilizados em diferentes unidades de objetos da classe.
A abstração é o processo de identificar as características e comportamentos essenciais de um objeto e criar uma classe que represente esses conceitos abstratos.
Além disso, o encapsulamento implica em esconder os detalhes internos de como os objetos operam e interagem, fornecendo uma interface para interagir com esses objetos.
Em termos simples, o encapsulamento em POO é a prática de abstrair e incorporar os detalhes internos de um objeto e permitir o acesso controlado a eles por meio de métodos públicos, ocultando ou protegendo os atributos e funcionalidades internas.
Principais aspectos do encapsulamento:
Os modificadores de acesso (público, protegido e privado) são convenções usadas para controlar a visibilidade dos atributos e métodos de uma classe.
As convenções para indicar a visibilidade de atributos e métodos são:
O encapsulamento é um dos princípios fundamentais da programação orientada a objetos (POO) que visa esconder os detalhes internos de uma classe e fornecer uma interface pública clara e consistente para interagir com os objetos dessa classe.
Isso é alcançado definindo atributos e métodos como públicos, protegidos ou privados.
Atributos e métodos públicos são acessíveis de fora da classe e geralmente têm nomes sem traço-baixo (ex: self.nome).
Atributos e métodos protegidos têm nomes com um traço-baixo no início (ex: self._idade). Eles ainda são acessíveis de fora da classe, mas convenciona-se que eles devem ser tratados como privados e não devem ser acessados diretamente.
Atributos e métodos privados têm nomes com dois traços-baixo no início (ex: self.__senha). Eles são apenas acessíveis dentro da própria classe e não podem ser acessados de fora.
class Classe1:
def __init__(self):
self.atributo1 = 'atributo1'
self._atributo2 = '_atributo2'
self.__atributo3 = '__atributo3'
def metodo1(self):
return self.atributo1
def metodo2(self):
return self._atributo2
def metodo3(self):
return self.__atributo3
A seguir instanciamos a classe Classe1 atribuindo na variável objeto1.
objeto1 = Classe1()
A função dir() é usada para mostrar os atributos e métodos de uma classe.
lista_dir = dir(objeto1)
print(lista_dir)
Os atributos e métodos do objeto objeto1 podem ser acessados com a compreensão de listas usando a lista retornada pela função dir().
Os atributos e métodos sem o traço-baixo (underscore) na primeira posição são públicos e podem ser acessados dentro e fora da classe.
publico = [publico for publico in lista_dir if '_' not in publico]
print(publico)
O atributo _atributo2 é protegido e pode ser acessado como se fosse público, apenas serve para indicar que o atributo é protegido e deve ser usado apenas dentro da classe e nas classes descendentes.
protegido = [protegido for protegido in lista_dir if '_atributo' == protegido[0:9]]
print(protegido)
O atributo __atributo3 é privado e não pode ser acessado de fora da classe, portanto não aparece com a função dir().
privado = [privado for privado in lista_dir if '__atributo' == protegido[0:10]]
print(privado)
Entretanto é criado automaticamente o atributo público _Classe1__atributo3, a partir do atributo __atributo3.
privado_publico = [privado_publico for privado_publico in lista_dir if '_Classe' in privado_publico]
print(privado_publico)
Os métodos metodo1(), metodo2() e metodo3() de objeto1 têm acesso público aos atributos e métodos declarados na classe, sejam públicos, protegidos ou privados.
Por padrão, os atributos e métodos declarados em uma mesma classe, seja no construtor __init__() ou fora dele, podem ser acessados nos métodos da própria classe.
print(objeto1.metodo1())
print(objeto1.metodo2())
print(objeto1.metodo3())
Apenas os atributos públicos e protegidos são acessíveis no objeto, os privados não são.
Público:
print(objeto1.atributo1)
Protegido:
print(objeto1._atributo2)
Privado:
print(objeto1.__atributo3)
Agora, criamos uma nova classe Classe2 herdando os atributos e métodos da classe Classe1.
A classe Classe2 a seguir contém o método-construtor __init__() e os métodos metodo1(), metodo2(), metodo3(), que imprimem os atributos e os métodos da classe Classe1.
O método-construtor __init__() é chamado quando uma instância da classe Classe2 é instanciada, executando o método-construtor ancestralda classe Classe1, com a instrução super().__init__().
Além disso, inicializa os atributos self.atributo4, self._atributo5 e self.__atributo6.
Os métodos ancestrais metodo11(), metodo12() e metodo13() são sobrepostos (sobrescritos) na classe Classe2.
class Classe2(Classe1):
def __init__(self):
super().__init__()
self.atributo4 = 'atributo4'
self._atributo5 = '_atributo5'
self.__atributo6 = '__atributo6'
def metodo4(self):
return self.atributo4
def metodo5(self):
return self._atributo5
def metodo6(self):
return self.__atributo6
A seguir instanciamos a classe Classe1 atribuindo na variável objeto1.
objeto2 = Classe2()
A variável lista_dir recebe o resultado da função dir(), com a lista de atributos e métodos do objeto da classe.
lista_dir = dir(objeto2)
Os atributos e métodos do objeto objeto2 podem ser acessados com a compreensão de listas usando a lista retornada pela função dir().
Os atributos e métodos sem o traço-baixo (underscore) na primeira posição são públicos e podem ser acessados dentro e fora da classe.
publico = [publico for publico in lista_dir if '_' not in publico]
print(publico)
O atributo _atributo2 é protegido e pode ser acessado como se fosse público, apenas serve para indicar que o atributo é protegido e deve ser usado apenas dentro da classe e nas classes descendentes.
protegido = [protegido for protegido in lista_dir if '_atributo' == protegido[0:9]]
print(protegido)
O atributo __atributo3 é privado e não pode ser acessado de fora da classe, portanto não aparece com a função dir().
privado = [privado for privado in lista_dir if '__atributo' == protegido[0:10]]
print(privado)
Entretanto é criado automaticamente o atributo público _Classe1__atributo3, a partir do atributo __atributo3.
privado_publico = [privado_publico for privado_publico in lista_dir if '_Classe' in privado_publico]
print(privado_publico)
Os métodos metodo1()ao metodo6() de objeto2 têm acesso público aos atributos e métodos declarados na classe, sejam públicos, protegidos ou privados, masntendo os modificadores de acesso herdados nas superclasses.
Por padrão, os atributos e métodos declarados em uma mesma classe, seja no construtor __init__() ou fora dele, podem ser acessados nos métodos da própria classe.
print(objeto2.metodo1())
print(objeto2.metodo2())
print(objeto2.metodo3())
print(objeto2.metodo4())
print(objeto2.metodo5())
print(objeto2.metodo6())
Apenas os atributos públicos e protegidos são acessíveis no objeto, os privados não são.
O atributo atributo1 é publico e pode ser acessado de fora da classe.
print(objeto2.atributo1)
O atributo atributo4 também é publico e pode ser acessado de fora da classe.
print(objeto2.atributo4)
O atributo _atributo2 é protegido e pode ser acessado como se fosse público.
print(objeto2._atributo2)
O atributo _atributo5 também é protegido e pode ser acessado como se fosse público.
O atributo __atributo3 é privado e não pode ser acessado de fora da classe.
print(objeto2.__atributo3)
O atributo __atributo6 também é privado e não pode ser acessado de fora da classe.
print(objeto2.__atributo6)
Agora, criamos uma nova classe Classe3 que herda os atributos e métodos da classe Classe2 e, portanto, os atributos e métodos da classe Classe1.
A classe Classe3 a seguir contém o método-construtor __init__() e os métodos metodo1(), metodo2(), metodo3(), que sobreescrevem os atributos e os métodos herdados da classe Classe2, que por sua vez herda os atributos e métodos da classe Classe1 .
O método-construtor __init__() é chamado quando uma instância da classe Classe2 é criada e chama o método-construtor da classe Classe1 com o comando super().__init__().
Além disso, inicializa os atributos self.atributo4, self._atributo5 e self.__atributo6 com os valores 'atributo21', '_atributo22' e '__atributo23'.
Os métodos ancestrais metodo11(), metodo12() e metodo13() são sobrepostos (sobrescritos) na classe Classe2.
class Classe3(Classe2):
def __init__(self):
super().__init__()
self.prefixo = '(alterado) '
def metodo1(self):
return self.prefixo + super().metodo1()
def metodo2(self):
return self.prefixo + super().metodo2()
def metodo3(self):
return self.prefixo + super().metodo3()
A seguir instanciamos a classe Classe3 atribuindo na variável objeto3.
objeto3 = Classe3()
A variável lista_dir recebe o resultado da função dir(), com a lista de atributos e métodos do objeto da classe.
lista_dir = dir(objeto3)
Os atributos e métodos do objeto objeto3 podem ser acessados com a compreensão de listas usando a lista retornada pela função dir().
Os atributos e métodos sem o traço-baixo (underscore) na primeira posição são públicos e podem ser acessados dentro e fora da classe.
publico = [publico for publico in lista_dir if '_' not in publico]
print(publico)
O atributo _atributo2 é protegido e pode ser acessado como se fosse público, apenas serve para indicar que o atributo é protegido e deve ser usado apenas dentro da classe e nas classes descendentes.
protegido = [protegido for protegido in lista_dir if '_atributo' == protegido[0:9]]
print(protegido)
O atributo __atributo3 é privado e não pode ser acessado de fora da classe, portanto não aparece com a função dir().
privado = [privado for privado in lista_dir if '__atributo' == protegido[0:10]]
print(privado)
Entretanto é criado automaticamente o atributo público _Classe1__atributo3, a partir do atributo __atributo3.
privado_publico = [privado_publico for privado_publico in lista_dir if '_Classe' in privado_publico]
print(privado_publico)
Os atributos e métodos do objeto objeto3 são os mesmos herdados da classe Classe2, entretanto ao imprimir os resultados dos métodos verificamos a alteração no compotamento dos métodos sobrescritos, caracterizando o morfismo no objeto.
print(objeto3.metodo1())
print(objeto3.metodo2())
print(objeto3.metodo3())
print(objeto3.metodo4())
print(objeto3.metodo5())
print(objeto3.metodo6())
O polimorfismo é caracterizado pelos diferentes comportamentos dos métodos com mesmo nome mas comportamento diferentes definidos pela sobrescrita de métodos entre classes ancestrais e descendentes.
O objeto objeto3 é da classe Classe3, que herda os atributos e métodos da classe Classe2 e sobrescreve os metodos metodo1(), metodo2() e metodo3() da classe Classe2, por sua vez herdados de Classe1.
A função abaixo imprime_metodo1() recebe um objeto da classe Classe1 ou de uma de suas classes descententes (Classe2 ou Classe3) e se imprime o valor retornado pelo atributo metodo1() do objeto passado como argumento.
def imprime_metodo1(objeto):
print(objeto.metodo1())
imprime_metodo1(objeto2)
imprime_metodo1(objeto3)
A classe do objeto do método metodo1(), utilizado como argumento na chamada da função imprime_metodo1(), é desconhecida dentro da função, mas é descentente de Classe1. Assim, invocando-se o método metodo1() será executado independente do objeto ser da classe Classe1, Classe2 ou Classe3, sendo que esta última tem comportamento diferente das anteriores, que têm compotamento igual.