Métodos: instância, classe e estáticos

1. Introdução aos Métodos em Python

Em Python, métodos são funções definidas dentro do corpo de uma classe e que operam sobre os dados daquela classe. A principal diferença entre um método e uma função comum é que o método está associado a um objeto (instância) ou à própria classe, recebendo automaticamente uma referência implícita ao contexto onde foi chamado.

Python oferece três tipos distintos de métodos, cada um com seu propósito e comportamento específicos:

  • Métodos de instância: operam sobre uma instância específica da classe
  • Métodos de classe (@classmethod): operam sobre a classe em si
  • Métodos estáticos (@staticmethod): funcionam como funções utilitárias dentro do namespace da classe

Os decoradores @classmethod e @staticmethod são fundamentais para definir os dois últimos tipos, modificando o comportamento padrão de como o Python passa o primeiro argumento implícito.

2. Métodos de Instância

Os métodos de instância são o tipo mais comum e recebem self como primeiro parâmetro, que representa a instância específica do objeto.

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

    def depositar(self, valor):
        """Método de instância que modifica o estado do objeto."""
        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 exibir_saldo(self):
        """Método de instância que retorna informações do estado atual."""
        return f"Titular: {self.titular} | Saldo: R${self.saldo:.2f}"

# Chamada implícita (recomendada)
conta = ContaBancaria("Ana", 1000)
print(conta.depositar(500))  # Saída: Depósito de R$500.00 realizado. Novo saldo: R$1500.00

# Chamada explícita (equivalente)
print(ContaBancaria.exibir_saldo(conta))  # Saída: Titular: Ana | Saldo: R$1500.00

A chamada explícita Classe.metodo(instancia) demonstra que, internamente, o Python converte objeto.metodo() para Classe.metodo(objeto).

3. Métodos de Classe (@classmethod)

Métodos de classe recebem cls como primeiro parâmetro, que representa a classe em si (não a instância). São definidos com o decorador @classmethod.

class Pessoa:
    especies = "Homo sapiens"

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

    @classmethod
    def criar_anonimo(cls, idade):
        """Factory method: cria uma pessoa sem nome definido."""
        return cls("Anônimo", idade)

    @classmethod
    def obter_especie(cls):
        """Método de classe que acessa atributo da classe."""
        return f"Espécie: {cls.especies}"

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

# Usando o construtor alternativo
pessoa1 = Pessoa.criar_anonimo(25)
print(pessoa1.apresentar())  # Saída: Olá, sou Anônimo e tenho 25 anos.
print(Pessoa.obter_especie())  # Saída: Espécie: Homo sapiens

A diferença fundamental é que cls permite acessar atributos e métodos da classe, além de criar novas instâncias (como no factory method acima).

4. Métodos Estáticos (@staticmethod)

Métodos estáticos não recebem nenhum parâmetro implícito (self ou cls). São definidos com o decorador @staticmethod e funcionam como funções comuns, mas organizadas dentro do namespace da classe.

class Calculadora:
    @staticmethod
    def validar_numero(valor):
        """Método estático: função utilitária de validação."""
        return isinstance(valor, (int, float)) and valor > 0

    @staticmethod
    def converter_para_moeda(valor):
        """Método estático: formata valor como moeda."""
        return f"R${valor:.2f}"

    def __init__(self, valor_inicial):
        if not Calculadora.validar_numero(valor_inicial):
            raise ValueError("Valor inicial inválido")
        self.valor = valor_inicial

# Uso sem instanciar a classe
print(Calculadora.validar_numero(100))     # Saída: True
print(Calculadora.converter_para_moeda(1500.5))  # Saída: R$1500.50

calc = Calculadora(200)
print(Calculadora.converter_para_moeda(calc.valor))  # Saída: R$200.00

Métodos estáticos são ideais para lógicas que pertencem conceitualmente à classe, mas não precisam acessar seus dados.

5. Comparação Prática e Casos de Uso

Característica Método de Instância Método de Classe Método Estático
Parâmetro implícito self (instância) cls (classe) Nenhum
Acesso a atributos de instância Sim Não Não
Acesso a atributos de classe Sim (via self.__class__) Sim (via cls) Não
Pode ser chamado pela instância Sim Sim Sim
Pode ser chamado pela classe Sim (passando instância) Sim Sim

Quando escolher cada tipo:

  • Método de instância: operações que dependem ou modificam o estado do objeto específico.
  • Método de classe: operações que envolvem a classe como um todo (factory methods, acesso a atributos de classe).
  • Método estático: funções utilitárias relacionadas ao domínio da classe, mas sem dependência de estado.

Exemplo integrado:

class Pedido:
    taxa_entrega = 5.0  # Atributo de classe

    def __init__(self, cliente, itens):
        self.cliente = cliente
        self.itens = itens
        self.total = sum(itens.values())

    def aplicar_desconto(self, percentual):
        """Método de instância: modifica o estado do pedido."""
        desconto = self.total * (percentual / 100)
        self.total -= desconto
        return f"Desconto de {percentual}% aplicado. Novo total: R${self.total:.2f}"

    @classmethod
    def atualizar_taxa(cls, nova_taxa):
        """Método de classe: altera atributo compartilhado."""
        cls.taxa_entrega = nova_taxa
        return f"Taxa de entrega atualizada para R${nova_taxa:.2f}"

    @staticmethod
    def validar_cupom(codigo):
        """Método estático: validação simples."""
        return len(codigo) == 8 and codigo.isalnum()

pedido = Pedido("Carlos", {"pizza": 30.0, "refrigerante": 8.0})
print(pedido.aplicar_desconto(10))  # Saída: Desconto de 10% aplicado...
print(Pedido.atualizar_taxa(7.0))   # Saída: Taxa de entrega atualizada para R$7.00
print(Pedido.validar_cupom("DESC10"))  # Saída: False

6. Comportamento em Herança e Polimorfismo

class Animal:
    def __init__(self, nome):
        self.nome = nome

    def emitir_som(self):
        """Método de instância: será sobrescrito nas subclasses."""
        return "Som genérico de animal"

    @classmethod
    def criar_por_tipo(cls, tipo, nome):
        """Método de classe: factory method polimórfico."""
        if tipo == "cachorro":
            return Cachorro(nome)
        elif tipo == "gato":
            return Gato(nome)
        return cls(nome)

    @staticmethod
    def validar_idade(idade):
        """Método estático: herdado como função."""
        return 0 <= idade <= 30

class Cachorro(Animal):
    def emitir_som(self):
        return "Au au!"

    @classmethod
    def criar_por_tipo(cls, tipo, nome):
        # 'cls' aqui é Cachorro, não Animal
        return cls(nome)

class Gato(Animal):
    def emitir_som(self):
        return "Miau!"

# Testando polimorfismo com métodos de instância
animais = [Cachorro("Rex"), Gato("Mimi")]
for animal in animais:
    print(f"{animal.nome}: {animal.emitir_som()}")

# Método de classe respeita a subclasse
rex = Animal.criar_por_tipo("cachorro", "Rex")
print(type(rex).__name__)  # Saída: Cachorro (cls aponta para subclasse)

# Método estático é herdado sem polimorfismo
print(Cachorro.validar_idade(5))  # Saída: True

Note que cls em métodos de classe aponta para a subclasse que chamou o método, permitindo polimorfismo. Já métodos estáticos são herdados como funções comuns, sem comportamento polimórfico.

7. Boas Práticas e Armadilhas Comuns

  1. Não usar @staticmethod quando um método de instância é mais adequado: se o método precisa acessar self, use método de instância.

  2. Evitar @classmethod para funcionalidades que seriam melhores como funções modulares: se o método não usa cls para nada relacionado à classe, considere transformá-lo em função no módulo.

  3. Cuidado com herança e métodos estáticos: como não são polimórficos, podem causar confusão se você espera que subclasses os sobrescrevam com comportamento diferente.

  4. Documentação clara: sempre use docstrings para indicar a intenção do método:

class Produto:
    @staticmethod
    def calcular_imposto(valor):
        """
        Calcula o imposto sobre o valor do produto.

        Este método é estático pois não depende do estado
        de nenhuma instância ou classe específica.
        """
        return valor * 0.15
  1. Prefira métodos de classe para factory methods: eles são mais flexíveis que métodos estáticos para criar instâncias, especialmente em hierarquias de herança.

Referências