Classes e Objetos

O que são classes e objetos?

Classe: Um projeto para criar objetos. Encapsula dados (atributos) e comportamento (métodos).

Objeto: Uma instância de uma classe. Os objetos contêm valores específicos para os atributos definidos pela classe.

A partir de uma classe, é possivel criar múltiplos objetos todos do mesmo tipo, com as mesmas funcionalidades e atributos, embora com dados eventualmente diferentes. Diz-se que um objeto é uma instância de uma classe.

Para criarmos um objeto temos, em primeiro lugar, de criar uma classe com as funcionalidades (métodos) e atributos (propriedades) que esse objeto irá ter.

Criação de classes

Criamos uma classe em Python com a palavra reservada class:

class Tubarao:
    #método nadar()
    def nadar(self):
        print("Tubarão nada")
        
    #método atacar()
    def atacar(self):
        print("Tubarão ataca")

Atributos

Atributos são variáveis que servem para guardar dados que são as propriedades dos objetos ou, por outras palavras, o seu estado.

Os atributos em Python podem ser classificados como atributos de instância ou atributos de classe, dependendo de como e onde eles são definidos. A diferença fundamental está em como eles são armazenados e compartilhados entre os objetos criados a partir de uma classe.

Atributos de instância

São atributos que pertencem a cada objeto individualmente, ou seja, cada instância da classe tem sua própria cópia desses atributos. São exclusivos para cada instância. Alterar o valor de um atributo em uma instância não afeta outras instâncias.

Os atributos de instância são declarados, geralmente, dentro do método __init__ (o construtor) usando self:

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome  # Atributo de instância
        self.idade = idade  # Atributo de instância

# Criar objetos (instâncias)
pessoa1 = Pessoa("João", 25)
pessoa2 = Pessoa("Maria", 30)

# Cada objeto tem seus próprios atributos
print(pessoa1.nome)  # Saída: João
print(pessoa2.nome)  # Saída: Maria

# Alterar o atributo de uma instância não afeta a outra
pessoa1.nome = "Carlos"
print(pessoa1.nome)  # Saída: Carlos
print(pessoa2.nome)  # Saída: Maria

NOTA: ao conjunto dos valores dos atributos de uma instância ou objeto chamamos estado desse objeto.

Atributos de classe

Os atributos de classe são atributos que pertencem à própria classe, e não às suas instâncias. Eles são compartilhados por todas as instâncias da classe.

São comuns a todas as instâncias. Alterar o valor do atributo na classe afeta todas as instâncias que compartilham esse atributo.

Os atributos de classe são definidos diretamente no corpo da classe, fora de qualquer método:

class Pessoa:
    especie = "Humana"  # Atributo de classe

    def __init__(self, nome):
        self.nome = nome  # Atributo de instância

# Criando objetos (instâncias)
pessoa1 = Pessoa("João")
pessoa2 = Pessoa("Maria")

# O atributo de classe é compartilhado por todas as instâncias
print(pessoa1.especie)  # Saída: Humana
print(pessoa2.especie)  # Saída: Humana

# Alterando o atributo de classe
Pessoa.especie = "Homo Sapiens"
print(pessoa1.especie)  # Saída: Homo Sapiens
print(pessoa2.especie)  # Saída: Homo Sapiens

Construtor

Um construtor de um objeto é um método especial, definido na classe, que é executado quando um objeto é instanciado. Normalmente, é usado para inicializar variáveis de atributos de instância ou executar algum procedimento aquando da instanciação do objeto.

No Python, o construtor de uma classe é o método __init__(self).

class Tubarao:
    #construtor:
    def __init__(self, especie):
        self.especie = especie
        print("Construtor: objeto instanciado")

    #método nadar()
    def nadar(self):
        print("Tubarão nada")

    #método atacar()
    def atacar(self):
        print("Tubarão ataca")

def main():
    #criamos um objeto 'tub' do tipo Tubarao:
    tub = Tubarao("Tubarão branco")

Métodos

Métodos de instância

Métodos de instância em Python são funções definidas dentro de uma classe que operam no nível da instância, ou seja, trabalham com os atributos e comportamentos específicos de cada objeto criado a partir da classe. Eles são os métodos mais comuns em Python e são usados para manipular ou aceder o estado de uma instância.

Características dos métodos de instância

  • Acedem os atributos e métodos da instância: Eles podem aceder e modificar os atributos específicos de cada objeto.
  • Usam o parâmetro self: O primeiro parâmetro do método é sempre a própria instância, convencionalmente chamado de self.
  • Dependem de uma instância: Para chamar um método de instância, é necessário criar um objeto a partir da classe.

Exemplo 1: Aceder a atributos

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    # Método de instância:
    def apresentar(self):
        print(f"Olá, meu nome é {self.nome} e tenho {self.idade} anos.")

# Criando uma instância:
pessoa1 = Pessoa("João", 30)
pessoa1.apresentar()  # Saída: Olá, meu nome é João e tenho 30 anos.

Exemplo 2: Atualizar atributos

class ContaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.saldo = saldo

    def depositar(self, valor):
        self.saldo += valor
        print(f"Depósito de {valor:.2f}€ realizado. Saldo atual: {self.saldo:.2f}€")

    def levantar(self, valor):
        if valor <= self.saldo:
            self.saldo -= valor
            print(f"Levantamento de {valor:.2f}€ realizado. Saldo atual: {self.saldo:.2f}€")
        else:
            print("Saldo insuficiente.")

# Criando uma conta bancária
conta1 = ContaBancaria("Maria", 1000)
conta1.depositar(200)  # Saída: Depósito de 200.00€ realizado. Saldo atual: 1200.00€
conta1.levantar(300)      # Saída: Levantamento de 300.00€ realizado. Saldo atual: 900.00€

Métodos de classe

Métodos de classe em Python são funções definidas dentro de uma classe que operam no nível da classe, em vez de no nível da instância. Eles são úteis para aceder ou modificar atributos da classe ou realizar operações que não dependem de uma instância específica. Para defini-los, utiliza-se o decorador @classmethod e o primeiro parâmetro do método é sempre a própria classe, convencionalmente chamada de cls.

class MinhaClasse:
    atributo_classe = "valor inicial"

    @classmethod
    def meu_metodo_de_classe(cls, novo_valor):
        cls.atributo_classe = novo_valor
        print(f"Atributo da classe atualizado para: {cls.atributo_classe}")

Características dos Métodos de Classe

  • Acedem a classe, não a instância: Eles operam sobre atributos e métodos da classe, mas não podem aceder diretamente atributos específicos de uma instância.
  • Usam o decorador @classmethod: Para declarar um método de classe, utiliza-se o decorador @classmethod antes da definição do método
  • Usam o parâmetro cls: Esse parâmetro representa a própria classe e é usado para aceder ou modificar variáveis de classe.
  • Podem ser chamados diretamente pela classe: Não é necessário criar uma instância para utilizá-los.

Exemplo 1: Modificar um atributo de classe

class Escola:
    nome = "Escola Padrão"

    @classmethod
    def alterar_nome(cls, novo_nome):
        cls.nome = novo_nome

# Chamando o método diretamente pela classe
Escola.alterar_nome("Escola Python")
print(Escola.nome)  # Saída: Escola Python

Exemplo 2: Factory method

Os métodos de classe também são frequentemente usados como factory methods, que criam novas instâncias da classe com base em dados diferentes.

from datetime import date

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    @classmethod
    def from_ano_nascimento(cls, nome, ano_nascimento):
        idade = date.today().year - ano_nascimento
        return cls(nome, idade)

# Criando uma instância usando o método fábrica (factory method)
pessoa1 = Pessoa.from_ano_nascimento("João", 1990)
print(f"{pessoa1.nome} tem {pessoa1.idade} anos.")  # Saída: João tem 35 anos.

Membros public, private e protected

Membros (atributos e métodos) public, private e protected têm a ver com o acesso a esses membros.

Por defeito, os membros são públicos e isso significa que podem ser acedidos fora da classe.

Membros private não podem ser acedidos fora da classe onde estão definidos. Os membros private são definidos pondo dois underscores (__) antes do nome.

Membros protected só podem ser acedidos dentro da classe onde foram definidos ou a partir das suas subclasses. No entanto, tal é definido por convenção, uma vez que nada impede que se possam aceder a partir de uma outra classe que não é subclasse daquela onde foram definidos. Os membros protected são definidos pondo um underscore (_) antes do nome do membro.

class Estudante:

    def __init__(self, nr, nome, morada):
        # atributo public:
        self.nr = nr
        # atributo private:
        self.__nome = nome
        # atributo protected:
        self._morada = morada

    # método nadar (private)()
    def __nadar(self):
        print("Tubarão nada")


    # método atacar()
    def atacar(self):
        print("Tubarão ataca")