sábado, 12 de abril de 2014

Modelo de dados em Python

Ao escrever uma classe em Python, é preciso ficar atento a algumas convenções a serem respeitadas.

Métodos especiais

Em Python, métodos começando e terminando com sublinha dobrado (__*__) são reservados e chamados especiais. Você não deve implementá-los se não souber o que está fazendo.

Métodos especiais podem fazer coisas maravilhosas ou criar comportamentos inesperados que zoarão com sua aplicação. São divididos em 12 tipos:
  1. Personalização básica;
  2. Acesso personalizado a atributos;
  3. Criação personalizada de classes;
  4. Verificações personalizadas de instâncias e subclasses;
  5. Emulação de chamada;
  6. Emulação de recipiente;
  7. Emulação de tipos sequenciais;
  8. Emulação de tipos numéricos;
  9. Regras de coerção;
  10. Gerenciamento de contexto;
  11. Pesquisa de classes old-style;
  12. Pesquisa de classes new-style.

Recomendo fortemente a leitura do documento Data model, é essencial ao bom programador.

Métodos privados

Métodos privados são iniciados com sublinha dobrado, mas não terminados da mesma forma (__*).
Um método privado não pode ser acessado de outro contexto a não ser da classe onde ele foi definido.

Isso não é de todo verdade… o que acontece de fato é que métodos privados são prefixados com um sublinha seguido do nome da classe, para evitar que sejam sobrescritos e prevenir acesso a partir de subclasses ou de fora do contexto da classe.

Por exemplo, no contexto da classe Person, todos os métodos iniciados com sublinha dobrado serão prefixados com _Person. Assim, __fix_data vira _Person__fix_data.

Isso permite que você faça herança múltipla de classes que possuem o mesmo nome de método privado sem conflitos.

Métodos protegidos

Há uma convenção em Python de que métodos com nome iniciado com um sublinha (_*) são protegidos, ou seja, só devem ser acessados no contexto da classe e de suas subclasses.

Nenhum tratamento é feito para evitar que sejam acessados de outros contextos, mas se espera que os programadores sigam a convenção.

A orientação em Python é que o programador não é nenhum bebezinho que precisa ser guiado e sabe o que está fazendo, portanto não aja como um bebê e respeite as convenções.

Atributos e propriedades

Tudo o que foi dito sobre métodos especiais, privados e protegidos também vale para atributos e propriedades.

[]’s
Cacilhας, La Batalema

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