quinta-feira, 18 de agosto de 2011

Metaclasses em Python

Boa tarde pessoas! Faz algum tempo que eu não posto coisas de programação então resolvi fazê-lo. Como vocês sabem, eu gosto muito de Python. Já mostrei várias funcionalidades da linguagem e expliquei algumas coisas que, na época que eu escrevia os respectivos posts, nem eu entendia muito bem. Hoje, acontece a mesma coisa.

Uma destas coisas é o conceito de metaclasses. Na verdade, em Python, não existe realmente uma idéia de metaclasse. Ela surge da forma como o Python lida com a linguagem. Python utiliza alguns conceitos de OO herdados de uma linguagem chamada Smalltalk onde, nesta linguagem, tudo é objeto. Classes, tipos, métodos, tudo é representado sob a forma de objetos. E em Python, acontece a mesma coisa. Então vamos a explicação! Se segurem!

Metaclasses em Python

Na maioria das linguagens, classes são somente a definição de como criar um objeto. Isso também é verdadeiro em Python:

>>> class MinhaClasse(object):
...     pass
... 
>>> meuObjeto = MinhaClasse()
>>> print(meuObjeto)
<__main__.MinhaClasse object at 0x2772ad0>
>>>

Mas em Python, classes são um pouco mais do que isso. Classes também são objetos. Sim, objetos, não só definições...


Assim que você usa a palavra-chave class, o interpretador Python o executa e cria um Objeto. A instrução:

>>> class MinhaClasse(object):
...     pass

cria na memória um objeto com o nome MinhaClasse.

Este objeto (a classe) é ela mesma capaz de criar outros objetos (suas instâncias), e é por isso que ela é uma classe.

Mas, ela ainda é um objeto, então:

  • Você pode associar a classe a uma variável;
  • Você pode copiar a classe;
  • Você pode adicionar atributos à classe;
  • Você pode passar a classe como parâmetro de uma função/método.


Veja no exemplo:

>>> class MinhaClasse(object):
...     pass
... 
>>> def echo(classe):
...     print(classe)
... 
>>> echo(MinhaClasse)
<class '__main__.MinhaClasse'>
>>> print(hasattr(MinhaClasse, "atributo"))
False
>>> MinhaClasse.atributo = "Hail!"
>>> print(hasattr(MinhaClasse, "atributo"))
True
>>> print(MinhaClasse.atributo)
Hail!
>>> OutraClasse = MinhaClasse
>>> class MinhaClasse(object):
...     pass
... 
>>> def echo(classe):
...     print(classe)
... 
>>> echo(MinhaClasse)
<class '__main__.MinhaClasse'>
>>> print(hasattr(MinhaClasse, "atributo")
... )
False
>>> print(hasattr(MinhaClasse, "atributo"))
False
>>> MinhaClasse.atributo = "Hail!"
>>> print(hasattr(MinhaClasse, "atributo"))
True
>>> print(MinhaClasse.atributo)
Hail!
>>> OutraClasse = MinhaClasse
>>> print(OutraClasse.atributo)
Hail!
>>> print(OutraClasse())
<__main__.MinhaClasse object at 0xf1fb10>
>>> print(OutraClasse.atributo)
Hail!
>>> print(OutraClasse())
<__main__.MinhaClasse object at 0xf1fb10>

Este exemplo demonstra claramente o comportamento de objeto de uma classe.

Criando classes dinâmicamente

Já que classes são objetos, podemos criá-las à vontade, que nem criamos objetos:

>>> def escolha_classe(nome):
...     if nome == "foo":
...             class Foo(object):
...                     pass
...             return Foo #Retorna a classe, não uma instância dela
...     else:
...             class Bar(object):
...                     pass
...             return Bar
... 
>>> MinhaClasse = escolha_classe("foo")
>>> print(MinhaClasse) #A função retorna uma classe, não uma instância dela
<class '__main__.Foo'>
>>> print(MinhaClasse()) #Podemos criar uma instância à partir dessa classe
<__main__.Foo object at 0xd53ad0>

Na verdade, isso não é tão dinâmico assim, porque no final das contas tivemos que escrever o código da classe.

E já que classes são objetos, elas podem ser geradas por alguma outra coisa.

Quando usamos a palavra reservada class, o interpretador cria a nossa classe automaticamente. Mas, como a maioria das coisas em Python, há uma outra maneira, manual, de se criar uma classe.

Se você programa em Python há algum tempo, já deve ter utilizado algumas vezes uma função chamada type, para saber qual o tipo de um objeto, não é mesmo?

>>> print(type(1))
<type 'int'>
>>> print(type("string"))
<type 'str'>
>>> print(type(MinhaClasse))
<type 'type'>
>>> print(type(MinhaClasse()))
<class '__main__.MinhaClasse'>

Bem, o comando type têm uma outra habilidade completamente diferente, que é poder criar classes quando necessário. No caso, o comando type pega a descrição de uma classe sob a forma de parâmetros, e retorna uma classe.

A função type (para a criação de classes) recebe os seguintes parâmetros:

  1. O nome da classe;
  2. Uma lista com as classes-pai (para formação da herança);
  3. Um dicionário, contendo nomes de atributos/métodos e os respectivos valores.


Baseado nisso, a seguinte classe:

>>> class MinhaClasse(object):
...     pass

pode ser criada manualmente da seguinte forma:

>>> MinhaClasse = type("MinhaClasse", (), {})
>>> print(MinhaClasse)
<class '__main__.MinhaClasse'>
>>> print(MinhaClasse())
<__main__.MinhaClasse object at 0xd53bd0>

Você deve ter percebido que foi usado o nome "MinhaClasse" é tanto o nome da classe quanto da variável que detém a referência da mesma. Elas podem ser diferentes, mas não há razões para complicar.

Como você deve ter visto na definição, type aceita um dicionário que define os atributos de uma classe. Então, a classe:

>>> class MinhaClasse(object):
...     atributo = "Hail!"

pode ser criada com type da seguinte forma:

>>> MinhaClasse = type("MinhaClasse", (), {"atributo": "Hail!"})

e usada como uma classe normal:

>>> print(MinhaClasse)
<class '__main__.MinhaClasse'>
>>> print (MinhaClasse.atributo)
Hail!
>>> instancia = MinhaClasse()
>>> print(instancia)
<__main__.MinhaClasse object at 0xd53c90>
>>> print(instancia.atributo)
Hail!

e claro, você pode definir herança:

>>> class OutraClasse(MinhaClasse):
...     pass

criando a classe da seguinte forma:

>>> OutraClasse = type("OutraClasse", (MinhaClasse,), {"outroAtributo": "Neah?"})
>>> print(OutraClasse)
<class '__main__.OutraClasse'>
>>> print(OutraClasse.outroAtributo)
Neah?
>>> print(OutraClasse.atributo)
Hail!

Preste atenção que a herança foi estabelecida e que podemos acessar tanto o atributo da classe atual quanto da classe pai. Também podemos adicionar métodos à nossa classe. Como um método também é um objeto, podemos pegar uma classe:

>>> class MinhaClasse(object):
...     def echo():
...             print("Oi?")

e escrevê-la da seguinte forma:

>>> MinhaClasse = type("MinhaClasse", (), {"echo": echo})
>>> hasattr(MinhaClasse, "echo")
True
>>> instancia = MinhaClasse()
>>> instancia.echo()
Oi?

O que eu quis mostrar com tudo isso? Que classes são objetos como qualquer outra coisa em Python, e podem ser criadas dinâmicamente. Isto que vc viu é o que o Python faz por trás dos panos quando você usa a palavra reservada class.

O que são metaclasses?

Finalmente, depois dessa explicação toda, vamos falar de metaclasses em Python.

Primeiramente, Metaclasse é algo que cria classes.

Você define classes com o objetivo de criar objetos, certo? E em Python, uma classe TAMBÉM é um objeto. Então, metaclasses também criam objetos. Só que esses objetos são classes. Então, metaclasse é uma classe de classes. Confuso né? Vamos explicar melhor:

MinhaClasse = MetaClasse()
instancia = MinhaClasse()

Simples assim. E você viu anteriormente que uma classe pode ser criada da seguinte maneira:

MinhaClasse = type("MinhaClasse", (), {})

Isso acontece porque, de fato, o método type é uma metaclasse. O método type é a metaclasse que o Python usa para criar todas as classes por detrás da cortina.

E você deve estar se perguntando. Se type é uma metaclasse, e de acordo com a PEP 8, classes devem ser escritas com maiúsculas, por que o método type é escrito em caixa baixa?

Porque como você deve ter notado, estou chamando type de método por todo o texto. E é o que ela é. Da mesma forma você pode observar isso com outros métodos que criam objetos, como str(), int(), list(), dict(), entre outros. É uma forma de consistência de nomes. Você pode checar isso através do atributo __class__ presente em qualquer objeto do Python.

Tudo (e quando eu digo tudo, é TUDO mesmo) em Python é objeto. Isso inclui os próprios métodos que citamos acima. Todos são objetos. E todos são criados à partir de uma classe.

>>> numero = 29
>>> numero.__class__
<type 'int'>
>>> nome = "Eduardo"
>>> nome.__class__
<type 'str'>
>>> def metodo(): pass
>>> metodo.__class__
<type 'function'>
>>> class classe(object): pass
>>> classe.__class__
<type 'type'>
>>> instancia = classe()
>>> instancia.__class__
<class '__main__.classe'>

Agora, observe o seguinte:

>>> numero.__class__.__class__
<type 'type'>
>>> nome.__class__.__class__
<type 'type'>
>>> metodo.__class__.__class__
<type 'type'>
>>> classe.__class__.__class__
<type 'type'>
>>> instancia.__class__.__class__
<type 'type'>

Você pode ver que type é a classe fundamental de todas as outras. Podemos chamar ela de "Class Factory". type é o tipo built-in que o Python usa para criar classes mas, claro, você pode usá-la também para criar suas próprias metaclasses.

O atributo __metaclass__

Você deve estar imaginando a quantidade de coisas que pode fazer com isso. Existe até uma forma de "automatizar" isto. Eu coloquei entre aspas a palavra automatizar porque essa solução é traiçoeira se você não souber usar. É o atributo __metaclass__.

Você pode adicionar este atributo em uma classe que você esteja definindo:

class MinhaClasse(Object):
    __metaclass__ = type

Se você fizer isto, o interpretador irá usar a metaclasse informada para criar a classe MinhaClasse.

Isto é traiçoeiro por que? Porque você escreveu primeiro class MinhaClasse(object), mas a classe em si ainda não está criada na memória. O interpretador irá olhar para __metaclass__ na definição da classe. Se ele encontrar a classe referenciada, ele irá usá-la para criar o objeto classe MinhaClasse. Se ele não encontrar, ele irá usar type para criar, sem dar nenhum aviso de qual foi usada.

Leia isto várias vezes, caso não tenha entendido. Mesmo assim, vou explicar abaixo.

Quando você faz:

class MinhaClasse(OutraClasse):
    pass

o interpretador faz o seguinte:

  • Existe um atributo __metaclass__ em MinhaClasse?
  • Se existe, cria na memória um objeto classe (veja bem, eu disse um objeto classe), com o nome MinhaClasse usando o que está definido em __metaclass__.
  • Se o interpretador não encontrar __metaclass__, ele irá procurar por __metaclass__ em OutraClasse (a classe pai), e tentar fazer o mesmo.
  • Se o interpretador não encontrar __metaclass__ em nenhuma das classes pai, ele irá procurar __metaclass__ no nível de módulo, e tentará fazer o mesmo.
  • Se o interpretador não encontrar __metaclass__, ele irá usar type para criar o objeto classe.


Agora, a grande questão: O que colocar em __metaclass__? E a resposta é: Algo que possa criar classes.

Mas, o que pode criar uma classe? Se você fez essa pergunta nesse ponto do post, pode desistir e ir programar em Java! Brincadeira! Qualquer coisa que use ou herde de type (ou o próprio type) pode ser usado para isto.

Metaclasses customizadas

Bom, aprendemos que em Python tudo é objeto, aprendemos um pouco mais sobre como funciona a criação de classes, e ainda aprendemos como funciona uma metaclasse. Agora, qual seria o uso dessa técnica?

Então, o principal objetivo de uma metaclasse é permitir a alteração da classe, quando ela é criada.

Normalmente você faz isso para APIs, onde você precisa criar classes que correspondam ao contexto onde elas estão sendo criadas.

Imaginem o seguinte exemplo estúpido: Você decidiu que todas as classes do seu módulo devem ter seus atributos escritos em caixa alta. Há várias formas de resolver esse "inconveniente", e uma das formas é setando __metaclass__ no nível de módulo.

Para nossa sorte, __metaclass__ pode ser qualquer objeto que implementa __call__, não é necessário que definamos uma classe somente para isso (eu sei, algo com "class" no seu nome não precisa ser necessariamente uma classe, vai entender... mas é útil).

Então, vamos começar com um exemplo simples, usando um método (que implementa __call__) para fazer o que queremos:

# a metaclasse passa automaticamente o mesmo argumento que voce usualmente passaria com type
def caixaAltaAttr(nomeDaClasse, classesPai, atributosDaClasse):
    """
    Retorna um objeto do tipo classe, com a lista dos atributos transformados em
    caixa alta.
    """
    
    # pega qualquer atributo que nao inicie com __ 
    atributos = ((nome, valor) for nome, valor in atributosDaClasse.items() if not nome.startswith('__'))
    # transforma eles em caixa alta
    atributosCaixaAlta = dict((nome.upper(), valor) for nome, valor in atributos)
    
    # deixamos que "type" faca a criacao da classe
    return type(nomeDaClasse, classesPai, atributosCaixaAlta)

__metaclass__ = caixaAltaAttr # isto ira afetar TODAS as classes dentro do modulo

class MinhaClasse(object): 
    # nos poderiamos definir __metaclass__ aqui, de forma a afetar somente esta classe
    __metaclass__ = caixaAltaAttr
    atributo = 'Hail!'

print(hasattr(MinhaClasse, 'atributo'))
# Saida: False
print(hasattr(MinhaClasse, 'ATRIBUTO'))
# Saida: True

f = MinhaClasse()
print(f.ATRIBUTO)
# Saida: 'Hail!'

Agora vamos fazer exatamente o mesmo, mas usando classes:

# lembre-se que "type" eh atualmente uma classe como "str" e "int"
# então voce pode herdar dela
class MetaclasseCaixaAlta(type): 
    # __new__ eh o método que eh chamado antes de __init__.
    # Ele o metodo que cria o objeto e o retorna, enquanto __init__ simplesmente
    # inicializa o objeto passado como parametro.
    # Voce raramente usa __new__, exceto quando voce quer controlar como o objeto
    # é criado.
    # Aqui o objeto criado eh a classe, e queremos customiza-la, então nos
    # substituimos __new__ com nossa versao.
    # Voce tambem pode colocar alguma coisa e personalizar __init__, se você quiser.
    # Algum uso avancado pode pedir que se substitua __call__, mas nao eh o caso agora.
    def __new__(metaclasse, nomeDaClasse, classesPai, atributosDaClasse):
        atributos = ((nome, valor) for nome, valor in atributosDaClasse.items() if not nome.startswith('__'))
        atributosCaixaAlta = dict((nome.upper(), valor) for nome, valor in atributos)

        return type(nomeDaClasse, classesPai, atributosCaixaAlta)

Mas isto não é verdadeiramente Orientação à Objetos, já que nós chamamos type diretamente e nós não alteramos a chamada de call do objeto pai. Veja como ficaria, com estas correções:

class MetaclasseCaixaAlta(type): 
    def __new__(metaclasse, nomeDaClasse, classesPai, atributosDaClasse):
        atributos = ((nome, valor) for nome, valor in atributosDaClasse.items() if not nome.startswith('__'))
        atributosCaixaAlta = dict((nome.upper(), valor) for nome, valor in atributos)

        # Reusando o metodo type.__new__
        # Isso é OO basico, nao ha magicas aki
        return type.__new__(metaclasse, nomeDaClasse, classesPai, atributosCaixaAlta)

Você deve ter percebido o argumento metaclasse adicional. Não há nada de especial nele. Um método sempre recebe no primeiro parâmetro a instância atual do objeto ao qual ele está ligado. Funciona exatamente como o self, que você já deve estar acostumado a usar.

Claro, os nomes que eu usei aqui foram usados desta forma para tornar mais claro o entendimento do que está sendo feito mas, assim como o self, todos os argumentos que usamos têm nomes padronizados. Assim, uma metaclasse deveria se parecer com isso:

class MetaclasseCaixaAlta(type): 
    def __new__(cls, name, bases, dct):
        atributos = ((nome, valor) for nome, valor in dct.items() if not nome.startswith('__'))
        atributosCaixaAlta = dict((nome.upper(), valor) for nome, valor in atributos)

        return type.__new__(cls, name, bases, atributosCaixaAlta)

Podemos deixar esse código ainda mais claro usando o método super(), que facilita a herança (Sim, porque você pode ter metaclasses, que herdam de metaclasses, que herdam de metaclasses, que no final de tudo, herdam de type):

class MetaclasseCaixaAlta(type): 
    def __new__(cls, name, bases, dct):
        atributos = ((nome, valor) for nome, valor in dct.items() if not nome.startswith('__'))
        atributosCaixaAlta = dict((nome.upper(), valor) for nome, valor in atributos)

        return super(MetaclasseCaixaAlta, cls).__new__(cls, name, bases, atributosCaixaAlta)

Bem, é isso. Não há mais nada que falar sobre metaclasses.

A razão por trás da complexidade do código que usa metaclasses não é por causa delas mesmas, é porque normalmente você usa metaclasses para fazer coisas complicadas relacionadas ao funcionamento da própria classe, manipular herança ou atributos, como por exemplo __dict__, entre outros.

Então, metaclasses são especialmente úteis para fazer serviço sujo, e as vezes complicado. Mas, por elas mesmas, são relativamente simples:

  • Permitem a interceptação da criação da classe;
  • Modificam a classe;
  • Retornam a classe modificada.


Por que você usaria uma metaclasse em vez de usar um método?

Já que __metaclass__ aceita qualquer objeto que implemente __call__, por que usar uma classe para fazer o serviço, já que sua definição é obviamente mais complexa que a definição de um método para tal?

Há diversas razões para tal. Entre elas:
A intenção é clara. Quando você encontrar MetaclasseCaixaAlta(type), você sabe o que virá à seguir;
Você pode usar Orientação à Objetos. Uma metaclasse pode herdar de outra, e substituir os métodos da classe pai. Uma metaclasse pode ainda usar outras metaclasses;
Você pode estruturar seu código bem melhor. Você nunca usa uma metaclasse para fazer algo como o exemplo idiota acima. Isto é feito normalmente para coisas bem mais complexas. A habilidade de poder ter vários métodos e agrupá-los em uma única classe é bastante útil para tornar o código fácil de se ler;
Você pode alterar os métodos __new__, __init__ e __call__, cada um permitindo fazer coisas diferentes. Mesmo que você possa fazer tudo em __new__, algumas pessoas podem se sentir mais confortáveis usando __init__;
Elas não são chamadas metaCLASSES à toa! Isso deve significar algo, né?

Qual o uso real disso?

Agora a grande questão deste post todo. Por que eu usaria algo tão obscuro (eu diria arcano) e propenso à erros?

Bem, usualmente, você não precisa. Como diz o guru em Python, Tim Peters:

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).

Traduzindo:

Metaclasses são aquele tipo de magia obscura que 99% dos usuários jamais deveriam se preocupar. Se você se pergunta se você precisa delas, você não precisa (as pessoas que realmente precisam delas sabem com certeza que elas precisam, e não precisam de uma explicação sobre o porquê).

Sem mais explicações.

O uso principal de metaclasses é na criação de APIs (que não vou explicar aqui, né). Um exemplo típico disso é a ORM (Modelo Objeto Relacional) do Django.

Ela permite que você defina algo como isto:

class Pessoa(models.Model):
    nome = models.CharField(max=length=30)
    idade = models.IntegerField()

Mas, se você fizer isto:

eu = Pessoa(nome='Eduardo', idade='29')
print(eu.idade)

Ele não irá retornar um objeto IntegerField. Ele irá retornar um objeto int, pegando diretamente do banco de dados.

Isto é possível porque models.Model define __metaclass__ e utiliza a mesma para fazer a mágica que irá tornar a classe Pessoa que você definiu ali com simples linhas de código em uma complexa chamada a um campo de banco de dados.

Django faz algo complexo parecer simples pela exposição de uma API simples de usar e que, usando metaclasses, recria o código à partir da API para fazer o real trabalho, tudo por debaixo dos panos. De fato, se você olhar ModelBase do Django, você vai ver a mágica sendo feita. Já aviso, não é para pessoas facilmente impressionáveis.

Finalizando

Primeiro, agora você sabe que classes são, na verdade, objetos que podem criar objetos.

Bem, de fato, classes são elas mesmas instâncias. De metaclasses.

>>> class MinhaClasse(object):
...     pass
... 
>>> id(MinhaClasse)
41692336

Então, TUDO em Python são objetos. E eles são TODOS instâncias de classes ou instâncias de metaclasses.

Exceto para type.

type é atualmente sua própria metaclasse. Ela é algo que você não pode reproduzir em Python, e isto é feito trapaceando um bocado no nível de implementação.

Segundo, metaclasses são complicadas. Não duvide disso! Você pode não querer usá-las para simples alterações de classes. Na realidade, você pode fazer isso de maneira muito mais fácil utilizando outras técnicas:

Alteração manual;
Decoradores de classe.

Em 99,5% do tempo que que você precisar de alterar uma classe, usar uma das duas soluções acima é melhor.

Mas, em 99% do tempo você não precisa de alteração de classes!

Fontes:
A Primer on Python Metaclass Programming
Metaclasses Demystified
Unifying types and classes in Python 2.2
A conservative metaclass
Stupid Metaclass and Template Tricks
Metaclass programming in Python (ibm.com): Part 1 ibm.com
Metaclass programming in Python (ibm.com): Part 2 ibm.com
Metaclass programming in Python (ibm.com): Part 3 ibm.com