Metaclasses em Python

Postado em 21.02.2009 00:00

Introdução

Li dois textos interessantes no [Kodumaro](http://kodumaro.blogspot.com/) recentemente: um sobre [propriedades e acessores](http://kodumaro.blogspot.com/2009/01/propriedades.html) e outro sobre o [design pattern *singleton*](http://kodumaro.blogspot.com/2009/01/s-ingleton.html). Ambos citavam as metaclasses, um conceito novo para mim e, pelo que andei conversando, novo para muitos de meus colegas de faculdade e trabalho. O seguinte texto é resultado de minha tentativa de explicar o que são metaclasses de uma forma simples de ser assimilada por pessoas que começaram a estudar Python há pouco tempo como eu e portanto não pode ser considerado como um guia definitivo e sem erros sobre o assunto. Qualquer sugestão ou comentário é bem vindo.

Revisando Orientação a Objetos e definindo metaclasses

Em uma linguagem de programação orientada a objetos, você pode definir classes que combinam dados (atributos) e métodos (comportamentos) que atuam sobre esses dados. Pode-se afirmar que ao definir uma classe se está definindo um domínio.

As classes geralmente atuam como modelos para a criação de instâncias de classe, que apesar de compartilharem uma mesma "forma", possuem dados diferentes. Uma instância `real` e outra `peso` de uma mesma classe `Moeda` podem possuir um atributo `cotacao`, mas cada instância terá seu próprio valor para esse atributo.

Em Python, tudo é objeto e tudo tem um tipo. Não existe diferença real entre uma classe e um tipo, especialmente depois da unificação de classes e tipos iniciada no Python 2.2. Ao definir uma classe, portanto, o programador está criando um novo tipo de dados. Como tudo é objeto, as classes também o são e possuem uma classe, um tipo. A classe de uma classe é chamada de metaclasse. O tipo *default* de uma classe é `type`.

>>> class Moeda(object):
...    pass
...
>>> type(Moeda)
<type 'type'>
>>> Moeda
<class '__main__.Moeda'>

Ao chamar `type()` com apenas um argumento, obtém-se o tipo do objeto. Observe que o tipo da **classe** `Moeda` é `type`. Afirmar que o tipo de uma classe é `type` é o mesmo que afirmar que a sua metaclasse é `type`. O exemplo acima comprovou que as classes realmente são um objeto com um tipo.

Os conceitos apresentados até aqui são um tanto abstratos para quem aprendeu orientação a objetos em linguagens como Java e C# e para os novatos em Python. No tópico seguinte, apresento a implementação de uma classe e de uma metaclasse com dicionários, como se Python não tivesse suporte sintático para OO. Apesar de ninguém querer programar assim na prática o exemplo é extremamente útil para entender como os objetos funcionam em Python.

Implementando classes com dicionários

O código abaixo foi originalmente postado no blog de Pedro Werneck em um post que explicava o [porquê do `self` explícito](https://web.archive.org/web/20110812155307/http://diaspar.blogspot.com/2008/11/o-porqu-do-self-explcito-ser-que-agora.html). Apesar de explicar metaclasses não ser o objetivo principal do texto, achei bastante útil para entender como o suporte a orientação a objetos foi implementado em Python. Isso acaba levando ao entendimento de vários conceitos interessantes (entre eles metaclasses) da linguagem e a uma apreciação ainda maior de sua simplicidade e consistência.

No post original, o código evoluiu aos poucos até chegar ao ponto em que está aqui. Para um entendimento pleno, [leia o post original](https://web.archive.org/web/20110812155307/http://diaspar.blogspot.com/2008/11/o-porqu-do-self-explcito-ser-que-agora.html).

A primeira parte define uma função `newClass` que recebe um nome de classe e um dicionário com seus atributos e retorna um dicionário que faz o papel das "classes" desse programa.

# precisaremos usar isso logo adiante...
from functools import partial

### Agora já podemos pensar nela como a classe 'Class'
def newClass(nome, atributos):
    cls = {'nome':nome} # cria o dicionário para a classe somente com seu nome
    for k, v in atributos.items():
        # aqui se um método for o newNome, ele tem de receber a mesma
        # alteração e passar a receber 'cls' como primeiro argumento
        if k == 'new'+nome:
            v = partial(v, cls)
        # e atribuímos tudo normalmente
        cls[k] = v
    return cls

Em seguida, é definido uma "classe" `Pessoa` com os atributos `nome`, `nascimento` e um método `idade`.

### Classe Pessoa

# construtor, corresponde ao __new__ de Python
def newPessoa(cls, nome, nascimento): # agora a classe mesmo vem aqui
    inst = {} # a nova instância
    inst['classe'] = cls # a instância tem de saber de que classe é, e
                         # agora pode saber dinamicamente

    # Agora a instância vai criar os métodos embrulhando as chamadas
    # para as funções originais em uma nova, já se incluindo nela
    for k, v in cls.items():
        # Se for função...
        if callable(v):
            # é, então vamos embrulhar...
            metodo = partial(v, inst)
            inst[k] = metodo
    inst['init'+cls['nome']](nome, nascimento)
    return inst

# inicializador, corresponde ao __init__ de Python
def initPessoa(inst, nome, nascimento):
    inst['nome'] = nome
    inst['nascimento'] = nascimento
    # assim como fazemos normalmente no __init__, aqui não precisamos
    # nos preocupar

def idade(inst, hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    x = ha - na
    return x

Com as funções que se tornarão os métodos da classe `Pessoa` definidos, falta a última parte da definição da classe que é criar o objeto que representa a classe. Esse objeto é retornado pela função `newClass` definida no início do código.

# e agora initPessoa() tem de entrar aqui também
Pessoa = newClass('Pessoa', {'newPessoa':newPessoa,
                             'initPessoa':initPessoa,
                             'idade':idade})
### Fim da definição de classe

A seguir, cria-se duas instâncias de `Pessoa` para testar a implementação.

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))
print hank['idade']((6, 11, 2008))

fante = Pessoa['newPessoa']('John Fante', (8, 4, 1909))
print fante['idade']((6, 11, 2008))

Nesse exemplo, a função `newClass` faz o papel de uma metaclasse, pois é ela que é chamada para criar a classe. É interessante notar que não existe diferença real entre o funcionamento de `newClass` e `newPessoa`. Ambas são funções que retornam instâncias, as instâncias retornadas por `newClass` são "classes" e as instâncias retornadas por `newPessoa` são objetos do tipo `Pessoa`.

O autor termina o texto usando as funções de inicialização e cálculo de idade com a metaclasse padrão de Python, `type`. Ela recebe uma string que será o nome da nova classe, uma tupla de classes base e um dicionário representando o *namespace* da classe. Esse dicionário contém o que você normalmente define como o corpo de uma instrução `class`. A única coisa que tem que mudar é o acesso aos atributos, que antes estavam sendo feitos como chaves de dicionários e agora devem usar a sintaxe de atributos normais.

#-*- coding: utf-8 -*-
def initPessoa(inst, nome, nascimento):
        inst.nome = nome
        inst.nascimento = nascimento

def idade(inst, hoje):
        hd, hm, ha = hoje
        nd, nm, na = inst.nascimento
        x = ha - na
        return x

Pessoa = type('Pessoa', (), {'__init__':initPessoa,
                              'idade':idade})
print "Representação de Pessoa como string: ",Pessoa
print "Tipo de Pessoa (sua metaclasse): ",type(Pessoa)
print
print "Imprimindo a idade de uma instância de Pessoa para teste: "
hank = Pessoa('Hank Moody', (8, 11, 1967))
print hank.idade((6, 11, 2008))

Resumindo:

- Em Python tudo é objeto, inclusive classes e instâncias. Não existe diferença real entre elas.

- Criar uma classe é criar um novo tipo.

- A classe de uma classe é chamada de metaclasse. As instâncias de metaclasses são classes.

- A metaclasse *default* é `type`.

- Não existe diferença real entre métodos e funções. Os métodos são apenas funções com *namespaces* específicos (suas classes).

Criando e definindo metaclasses

Para definir sua própria metaclasse, basta criar uma classe que herda da metaclasse `type`. O método inicializador recebe os mesmos argumentos que o método inicializador de `type`: uma string que será o nome da classe, uma tupla de classes bases e um dicionário representando o *namespace* da classe.

Segue um [exemplo ligeiramente modificado](https://web.archive.org/web/20090215011047/http://www.ibm.com/developerworks/linux/library/l-pymeta.html) de um artigo do [IBM developerWorks](https://web.archive.org/web/20090221162632/http://www.ibm.com/developerworks/). Primeiramente, vamos definir uma nova metaclasse:

>>> class ChattyType(type):
...    def __new__(cls, name, bases, dct):
...       print "Alocando memória para classe", name
...       dct['usa_metaclasse']=True
...       return type.__new__(cls, name, bases, dct)
...    def __init__(cls, name, bases, dct):
...       print "Inicializando (configurando) classe", name
...       super(ChattyType, cls).__init__(name, bases, dct)
...

Essa metaclasse sobrescreve o método `__new__` que efetivamente é o construtor e retorna uma nova instância de classe (nesse exemplo específico, a instância retornada será uma classe por estarmos escrevendo uma **meta**classe) e o método `__init__` que é o inicializador da instância e não retorna nada. É tentador chamar o `__init__` de construtor, mas o trabalho de criação de instâncias é do `__new__`.

O código acima criou um novo tipo que imprime uma mensagem quando está sendo criado e outra quando está sendo inicializado. Definiu-se também que todas as instâncias do novo tipo terão um atributo `usa_metaclasse` com o valor inicial `True`. Essa mudança foi feita adicionando uma chave ao dicionário que representa o *namespace* da classe (`dct`).

Com a metaclasse definida, já é possível criar classes a partir dela chamando-a como chama-se `type`:

>>> X=ChattyType('X', (object,), {})
Alocando memória para classe X
Inicializando (configurando) classe X

Instanciar classes manualmente como feito acima, não é um procedimento muito comum. Segue como Python [determina a metaclasse](https://www.python.org/download/releases/2.2.3/descrintro/#metaclasses) de uma determinada classe:

1. Se o dicionário que representa o *namespace* da classe contiver uma chave `__metaclass__`, o seu valor é usado.

1. Senão, se existe pelo menos uma classe base, sua metaclasse é usada.

1. Senão, se existe uma variável global `__metaclass__`, seu valor é usado.

1. Senão, `types.ClassType` é usado.

A criação da classe `X` feita no exemplo acima comumente seria feita assim:

>>> class X(object):
...    __metaclass__=ChattyType
...
Alocando memória para classe X
Inicializando (configurando) classe X

A instanciação de `ChattyType`, feita explicitamente no primeiro exemplo, agora é feita pelo interpretador Python no final da instrução `class`. Note que a instrução `class` é simplesmente [açúcar sintático](https://en.wikipedia.org/wiki/Syntactic_sugar) para a instanciação de classes através de suas metaclasses. Os programadores que não fazem uso de metaclasses acabam não percebendo isso, já que simplesmente criam suas classes usando `class` e Python simplesmente as instancia chamando `type`.

Continuando o exemplo, podemos confirmar que o tipo da classe é a metaclasse:

>>> type(X)
<class '__main__.ChattyType'>
>>> y=X()
>>> type(y)
<class '__main__.X'>
>>> type(X) == type(type(y))
True

Para concluir, segue algumas dicas na hora de usar metaclasses:

Alguns exemplos

Metaclasses são usadas na maior parte do tempo para alterar o comportamento de criação e inicialização de classes. Na maioria dos casos, os usuários finais são outros programadores. Frameworks e ferramentas de mapeamento objeto-relacional são exemplos de softwares que usam metaclasses.

Alguns usos para as metaclasses:

O importante é que a classe só é realmente criada na chamada final a `type` na metaclasse. Você é livre para modificar o dicionário de atributos de acordo com as suas necessidades antes disso, portanto existem muito mais possibilidades de uso para as metaclasses do que as apresentadas aqui.