sábado, 12 de abril de 2014

Ordem de chamada dos métodos de inicialização

Quando você cria e instancia uma classe em Python, muita coisa acontece e acho que nem todos estão familiarizados com os passos.

Vamos dar uma olhada na sequência.

Criação da classe

Quando você cria uma classe com o statement class, a primeira coisa que acontece é o construtor (__new__) da metaclasse (por padrão type) ser chamado.

O construtor recebe como parâmetros a própria metaclasse, o nome da classe (str), uma tupla contendo as classes base da classe criada e um dicionário com métodos e atributos declarados no corpo da classe.

Caso você sobrescreva o construtor da metaclasse, ele precisa retornar o objeto instanciado, que você obtém do construtor da classe pai da metaclasse.

Em seguida o método de inicialização (__init__) da metaclasse é chamado, recebendo como parâmetros o retorno do construtor, o nome da classe, a tupla de classes base e o dicionário de métodos e atributos.

É recomendável não sobrescrever o construtor da metaclasse. Qualquer procedimento pode ser tranquilamente executado nos métodos e inicialização e de chamada.

Instanciação da classe

Quando você instancia a classe, o primeiro método a ser chamado é o método de chamada (__call__) da metaclasse. Ele recebe como parâmetros a classe e quaisquer parâmetros passados no comando de instanciação.

Em seguida é evocado o construtor da classe, recebendo a classe e quaisquer parâmetros passados no comando de instanciação.

É obrigatório que a instância criada pelo construtor de super seja retornada, caso o construtor seja sobrescrito.

Após o construtor, o método de inicialização da classe é chamado, recebendo como parâmetros o retorno do construtor e quaisquer parâmetros passados no comando de instanciação.

Chamando a instância

Caso o método de chamada seja implementado na classe, a instância é “chamável” (callable). Na chamada o método de chamada é executado recebendo a instância e quaisquer parâmetros passados na chamada da instância.

Quem é quem

Um pequeno código apenas com boilerplates que não executam nada, com o único objetivo de demonstrar onde fica cada método citado:
class Metaclasse(type):

    def __new__(meta, name, base, dict_):
        """ Este é o construtor da metaclasse """
        return type.__new__(meta, name, base, dict_)

    def __init__(cls, name, base, dict_):
        """ Este é o método de inicialização da metaclasse """
        type.__init__(cls, name, base, dict_)

    def __call__(cls, *args, **kwargs):
        """ Este é o método de chamada da metaclasse """
        return type.__call__(cls, *args, **kwargs)


class Classe(object):
    __metaclass__ = Metaclasse

    def __new__(cls, *args, **kwargs):
        """ Este é o construtor da classe """
        return super(Classe, cls).__new__(cls)

    def __init__(self, *args, **kwargs):
        """
        Este é o método de inicialização da classe.
        Como ela herda de object, não há necessidade de
        chamar super, mas quando houver, a forma é:
        super(Classe, self).__init__(*args, **kwargs)
        """

    def __call__(self, *args, **kwargs):
        """ Este é o método de chamada da classe """
        return None

[update 2015-04-18] Esta é a versão do código acima em Python 3:
class Metaclasse(type):

    def __new__(meta, name: str, base: tuple, dict_: dict) -> type:
        """ Este é o construtor da metaclasse """
        return type.__new__(meta, name, base, dict_)

    def __init__(cls, name: str, base: tuple, dict_: dict):
        """ Este é o método de inicialização da metaclasse """
        type.__init__(cls, name, base, dict_)

    def __call__(cls, *args, **kwargs) -> object:
        """ Este é o método de chamada da metaclasse """
        return type.__call__(cls, *args, **kwargs)


class Classe(object, metaclass=Metaclasse):

    def __new__(cls, *args, **kwargs) -> object:
        """ Este é o construtor da classe """
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        """
        Este é o método de inicialização da classe.
        Como ela herda de object, não há necessidade de
        chamar super, mas quando houver, a forma é:
        super().__init__(*args, **kwargs)
        """

    def __call__(self, *args, **kwargs) -> None:
        """ Este é o método de chamada da classe """
        return None
[/update]

Dois dedos de prosa sobre método destruidor

O método destruidor (__del__) não é chamado quando o objeto perde todas as referências ativas, mas sim quando é coletado pelo garbage collector (gc).

Como o gc não tem hora certa para rodar e seu comportamento varia muito de uma implementação de Python para outra, não é recomendável confiar nele para executar procedimentos críticos.

O que você pode fazer é usá-lo para garantir determinados estados que podem ter sido esquecidos ou perdidos depois que a instância ficou sem referências.

Observações finais

As assinaturas do construtor e do método de inicialização da classe devem ser rigorosamente iguais.

[update]
Um detalhe importante é que, se o método de inicialização for sobrescrito alterando sua assinatura, não há necessidade de sobrescrever o construtor, porém se o construtor for sobrescrito alterando sua assinatura, é obrigatório que o método de inicialização também seja sobrescrito.

Detalhes da linguagem…
[/update]

A chamada do método de chamada da classe pai da meta classe deve passar os parâmetros esperados pelos argumentos nas assinaturas do construtor e da inicialização da classe. No exemplo acima, esta chamada (precedida por return) é:
type.__call__(cls, *args, **kwargs)

[update]
Um detalhe que esqueci de mencionar é que é justamente nessa chamada que o construtor e o método de inicialização são evocados.
[/update]

Os parâmetros passados são os esperados nas assinaturas citadas.

[]’s
Cacilhας, La Batalema
blog comments powered by Disqus