Em Python, os métodos definidos dentro de uma classe podem ser classificados em três categorias principais: métodos de instância, métodos de classe e métodos estáticos. Cada um possui comportamentos e finalidades distintas, e a escolha adequada impacta a clareza e a manutenibilidade do código. Nesta aula, vamos explorar cada tipo, entender suas assinaturas e decoradores, e discutir cenários práticos para sua utilização.

Compreender essas diferenças é fundamental para projetar classes coesas e seguir boas práticas de programação orientada a objetos. Vamos começar com o tipo mais comum: os métodos de instância.

Métodos de instância

Métodos de instância são funções definidas dentro de uma classe que operam sobre uma instância específica (objeto) da classe. Eles recebem o parâmetro self como primeiro argumento, que se refere à própria instância. Através de self, o método pode acessar e modificar atributos da instância e chamar outros métodos da mesma instância.

A maioria dos métodos que você escreve em classes Python são métodos de instância. Eles são chamados diretamente no objeto, como objeto.metodo(). Exemplo:

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

    def apresentar(self):
        return f"Olá, meu nome é {self.nome} e tenho {self.idade} anos."

p = Pessoa("Ana", 30)
print(p.apresentar())  # Olá, meu nome é Ana e tenho 30 anos.

No exemplo, apresentar é um método de instância que usa self.nome e self.idade. Cada instância possui seus próprios valores, e o método opera sobre eles.

@classmethod

Métodos de classe são definidos com o decorador @classmethod e recebem o parâmetro cls (referência à classe, não à instância) como primeiro argumento. Eles podem ser chamados tanto na classe quanto em uma instância, mas operam sobre a classe em si, não sobre instâncias específicas.

Métodos de classe são úteis para criar métodos de fábrica (factory methods) que retornam novas instâncias da classe, ou para acessar/modificar atributos da classe (variáveis de classe). Exemplo:

class Pessoa:
    especie = "Humano"

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

    @classmethod
    def criar_anonimo(cls):
        return cls("Anônimo", 0)

    @classmethod
    def mudar_especie(cls, nova_especie):
        cls.especie = nova_especie

p = Pessoa.criar_anonimo()
print(p.nome)  # Anônimo
print(Pessoa.especie)  # Humano
Pessoa.mudar_especie("Homo sapiens")
print(Pessoa.especie)  # Homo sapiens

Note que criar_anonimo usa cls para instanciar a classe (cls(...)), o que permite que subclasses herdem corretamente o método.

@staticmethod

Métodos estáticos são definidos com o decorador @staticmethod e não recebem nem self nem cls automaticamente. Eles são funções comuns dentro do namespace da classe, sem acesso a atributos da instância ou da classe (a menos que sejam passados como argumentos).

Métodos estáticos são usados quando uma função está relacionada à classe mas não depende de seu estado. Eles ajudam na organização do código, agrupando funções utilitárias junto à classe. Exemplo:

class Matematica:
    @staticmethod
    def somar(a, b):
        return a + b

    @staticmethod
    def pi():
        return 3.14159

print(Matematica.somar(2, 3))  # 5
print(Matematica.pi())         # 3.14159

Métodos estáticos podem ser chamados na classe ou em instâncias, mas não têm acesso a self ou cls. São equivalentes a funções definidas fora da classe, mas colocadas dentro por questões de organização.

Quando usar cada um

A escolha entre os três tipos depende do que o método precisa acessar:

  • Métodos de instância: Use quando o método precisa acessar ou modificar o estado da instância (atributos de instância). É o tipo mais comum.
  • Métodos de classe (@classmethod): Use quando o método precisa acessar ou modificar atributos da classe, ou quando você precisa de um método de fábrica que retorne uma instância da classe (ou de uma subclasse). Exemplos: construtores alternativos, manipulação de variáveis de classe.
  • Métodos estáticos (@staticmethod): Use quando o método não precisa acessar nem a instância nem a classe, mas está logicamente relacionado à classe. Exemplos: funções auxiliares, validações que não dependem de estado.

Boa prática: se você não usa self ou cls no método, considere se ele deveria ser um método estático ou uma função independente. Se a função for usada apenas dentro da classe, um método estático pode ser apropriado. Caso contrário, talvez seja melhor defini-la fora.

Boas práticas e observações finais

- Métodos de classe são frequentemente usados como construtores alternativos. Por exemplo, datetime.datetime.fromtimestamp() é um método de classe que cria um objeto datetime a partir de um timestamp.

- Métodos estáticos podem ser substituídos por funções modulares, mas mantê-los dentro da classe pode melhorar a coesão do código.

- Lembre-se de que métodos de classe e estáticos podem ser chamados tanto na classe quanto em instâncias, mas chamar um método estático em uma instância pode ser confuso; prefira chamá-los pela classe.

Referências

Exercícios

  1. Implemente uma classe ContaBancaria com métodos de instância depositar, sacar e exibir_saldo. Use atributos de instância titular e saldo.
  2. ✓ Resposta:
    class ContaBancaria:
        def __init__(self, titular, saldo=0):
            self.titular = titular
            self.saldo = saldo
    
        def depositar(self, valor):
            if valor > 0:
                self.saldo += valor
                return f"Depósito de R$ {valor:.2f} realizado. Novo saldo: R$ {self.saldo:.2f}"
            return "Valor inválido para depósito."
    
        def sacar(self, valor):
            if 0 < valor <= self.saldo:
                self.saldo -= valor
                return f"Saque de R$ {valor:.2f} realizado. Novo saldo: R$ {self.saldo:.2f}"
            return "Saldo insuficiente ou valor inválido."
    
        def exibir_saldo(self):
            return f"Saldo atual: R$ {self.saldo:.2f}"
  3. Crie um método de classe criar_conta_poupanca na classe ContaBancaria que retorna uma nova conta com saldo inicial de R$ 50,00.
  4. ✓ Resposta:
    @classmethod
    def criar_conta_poupanca(cls, titular):
        return cls(titular, saldo=50.0)
  5. Adicione um método estático validar_cpf que recebe uma string e retorna True se o CPF tiver 11 dígitos (apenas simulação).
  6. ✓ Resposta:
    @staticmethod
    def validar_cpf(cpf):
        # Simulação: verifica se tem 11 dígitos
        return len(cpf) == 11 and cpf.isdigit()
  7. Explique por que o método criar_conta_poupanca deve ser um método de classe e não um método estático.
  8. ✓ Resposta:Ele deve ser um método de classe porque precisa criar uma nova instância da classe (cls). Se fosse estático, não teria acesso à classe para instanciar, e teria que referenciar ContaBancaria diretamente, o que quebraria a herança. Usando @classmethod, subclasses podem herdar o método e criar instâncias delas mesmas.
  9. Crie uma classe Calculadora com um método estático soma e um método de classe versao que retorna uma string com a versão da calculadora (ex.: "Calculadora v1.0").
  10. ✓ Resposta:
    class Calculadora:
        versao_app = "1.0"
    
        @staticmethod
        def soma(a, b):
            return a + b
    
        @classmethod
        def versao(cls):
            return f"Calculadora v{cls.versao_app}"