Python para Iniciantes
Carregando, aguarde alguns segundos.

5 - Classes

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.

5.1 - Tipos de programação

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.

5.2 - Encapsulamento, Abstração e Modificadores de Acesso

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:

  • Acesso Controlado: O encapsulamento permite que você controle o acesso aos membros de uma classe. Atributos e métodos podem ser definidos como públicos, privados ou protegidos.
    • Públicos: Métodos e atributos públicos são acessíveis de fora da classe.
    • Privados: Métodos e atributos privados só podem ser acessados internamente pela própria classe.
    • Protegidos: Métodos e atributos protegidos são acessíveis na classe e em suas subclasses.
  • Ocultação de Informação: A ideia é esconder os detalhes de implementação dos objetos. Isso é alcançado ao definir certos métodos como públicos e ocultar a complexidade interna. Os usuários da classe podem interagir com o objeto usando essa interface pública, sem precisar saber como as operações são realizadas internamente.
  • Manutenção do Estado Consistente: O encapsulamento ajuda a manter a consistência do estado de um objeto, garantindo que os valores dos atributos estejam dentro de limites válidos e possam ser modificados apenas de maneira controlada, evitando ações que poderiam levar o objeto a um estado inválido.

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:

  • Atributos e métodos públicos: Nomes sem traço-baixo (ex: self.nome).
  • Atributos e métodos protegidos: Nomes com um traço-baixo no início (ex: self._idade).
  • Atributos e métodos privados: Nomes com dois traços-baixo no início (ex: self.__senha).

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)

5.3 - Herança

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)

5.4 - Polimorfismo

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.

Arduino
Coautor
Betobyte
Autor
Autores
||| Áreas ||| Estatística ||| Python ||| Projetos ||| Dicas & Truques ||| Quantum ||| Python para Iniciantes || Python para Iniciantes || Python Básico || Matplotlib || Numpy || Seaborn || Pandas || Django || Estatística para Cientistas de Dados || Python com ML Básico || Python com ML Básico || Aulas | Introdução (Instalação, variáveis e tipos de dados, operações, formatação e comentários.) | Coleções (Listas, Tuplas, Conjuntos e Dicionários.) | Funções e Módulos (Declaração, argumentos, retorno, recursão, funções lambda.) | Estruturas de Controle (Controle Condicional (if/elif/else, match) e Controle de repetição (for, while).) | Classes (Classes, Herança e Polimorfismo.) | Bibliotecas e Frameworks (Pacotes integrados, instalados e personalizados.) | Manipulação de Arquivos (Manipulação de Arquivos) | Bancos de Dados SQL (MySQL, SQLite e PostgreSQL.) | Capítulo 9 (APIs e Web Scraping) | Capítulo 10 (Projetos Práticos) | Considerações finais () |