Orientação a objetos de outra forma: Métodos estáticos e de classes

Série Orientação a objetos de outra forma

Na postagem anterior foi apresentado o self, nessa postagem será discutido mais a respeito desse argumento, considerando opções para ele e suas aplicações.

Métodos estáticos

Nem todas as funções de uma classe precisam receber uma referência de um objeto para lê-lo ou alterá-lo, muitas vezes uma função pode fazer o seu papel apenas com os dados passados como argumento, por exemplo, receber um nome e validar se ele possui pelo menos três caracteres sem espaço. Dessa forma, essa função poderia ser colocada fora do escopo da classe, porém para facilitar sua chamada, e possíveis alterações (que será discutido em outra postagem), é possível colocar essa função dentro da classe e informar que ela não receberá o argumento self com o decorador @staticmethod:

class Pessoa:
    ...  # Demais funções

    @staticmethod
    def valida_nome(nome):
        return len(nome) >= 3 and ' ' not in nome

Dessa forma, essa função pode ser chamada diretamente de um objeto pessoa, ou até mesmo diretamente da classe, sem precisar criar um objeto primeiro:

# Chamando diretamente da classe
print(Pessoa.valida_nome('João'))

# Chamando através de um objeto do tipo Pessoa
p1 = Pessoa('João', 'da Silva', 20)
print(p1.valida_nome(p1.nome))

E essa função também pode ser utilizada dendro de outras funções, como validar o nome na criação de uma pessoa, de forma que caso o nome informado seja válido, será criado um objeto do tipo Pessoa, e caso o nome seja inválido, será lançado uma exceção:

class Pessoa:
    def __init__(self, nome, sobrenome, idade):
        if not self.valida_nome(nome):
            raise ValueError('Nome inválido')

        self.nome = nome
        self.sobrenome = sobrenome
        self.idade = idade

    ...  # Demais funções

    @staticmethod
    def valida_nome(nome):
        return len(nome) >= 3 and ' ' not in nome


p1 = Pessoa('João', 'da Silva', 20)  # Cria objeto
p2 = Pessoa('a', 'da Silva', 20)  # Lança ValueError: Nome inválido

Métodos da classe

Entretanto algumas funções podem precisar de um meio termo, necessitar acessar o contexto da classe, porém sem necessitar de um objeto. Isso é feito através do decorador @classmethod, onde a função decorada com ele, em vez de receber um objeto como primeiro argumento, recebe a própria classe.

Para demonstrar essa funcionalidade será implementado um id auto incremental para os objetos da classe Pessoa:

class Pessoa:
    total_de_pessoas = 0

    @classmethod
    def novo_id(cls):
        cls.total_de_pessoas += 1
        return cls.total_de_pessoas

    def __init__(self, nome, sobrenome, idade):
        self.id = self.novo_id()
        self.nome = nome
        self.sobrenome = sobrenome
        self.idade = idade

p1 = Pessoa('João', 'da Silva', 20)
print(p1.id)  # Imprime 1
p2 = Pessoa('Maria', 'dos Santos', 18)
print(p2.id)  # Imprime 2
print(Pessoa.total_de_pessoas)  # Imprime 2
print(p1.total_de_pessoas)  # Imprime 2
print(p2.total_de_pessoas)  # Imprime 2

Nesse código é criado uma variável total_de_pessoas dentro do escopo da classe Pessoas, e que é compartilhado tanto pela classe, como pelos objetos dessa classe, diferente de declará-la com self. dentro do __init__, onde esse valor pertenceria apenas ao objeto, e não é compartilhado com os demais objetos. Declarar variáveis dentro do contexto da classe é similar ao se declarar variáveis com static em outras linguagens, assim como o @classmethod é semelhante a declaração de funções com static.

As funções declaradas com @classmethod também podem ser chamadas sem a necessidade de se criar um objeto, como Pessoa.novo_id(), embora que para essa função específica isso não faça muito sentido, ou receber outros argumentos, tudo depende do que essa função fará.

Considerações

Embora possa parecer confuso identificar a diferença de uma função de um objeto (função sem decorador), função de uma classe (com decorador @classmethod) e função sem acesso a nenhum outro contexto (com decorador @staticmethod), essa diferença fica mais clara ao se analisar o primeiro argumento recebido por cada tipo de função. Podendo ser a referência a um objeto (self) e assim necessitando que um objeto seja criado anteriormente, ser uma classe (cls) e não necessitando receber um objeto, ou simplesmente não recebendo nenhum argumento especial, apenas os demais argumentos necessários para a função. Sendo diferenciados pelo uso dos decoradores.

Na orientação a objetos implementada pelo Python, algumas coisas podem ficar confusas quando se mistura com nomenclaturas de outras linguagens que possuem implementações diferentes. A linguagem Java, por exemplo, utiliza a palavra-chave static para definir os atributos e métodos de classe, enquanto no Python um método estático é aquele que não acessa nem um objeto, nem uma classe, devendo ser utilizado o escopo da classe e o decorador @classmethod para se criar atributos e métodos da classe.