GMGall’s blog

Programação, Linux e o que mais der na telha

Metaclasses em Python

Introdução

Li dois textos interessantes no Kodumaro recentemente: um sobre propriedades e acessores e outro sobre o design pattern singleton. 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. 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.

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 de um artigo do IBM 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 metaclasse) 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 de uma determinada classe:


  1. Se o dicionário que representa o namespace da classe contiver uma chave __metaclass__, o seu valor é usado.
  2. Senão, se existe pelo menos uma classe base, sua metaclasse é usada.
  3. Senão, se existe uma variável global __metaclass__, seu valor é usado.
  4. 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 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:


  • A modificação do dicionário que representa o namespace da classe deve ser feita em __new__, como no exemplo. Se na definição de ChattyType, dct[‘usa_metaclasse’]=True fosse executado em __init__, a alteração não teria efeito. Considere o uso de setattr nesses casos.
  • Sempre herde de type.
  • __new__ é o construtor e deve retornar uma nova instância.
  • Erros em metaclasses costumam aparecer durante imports e definições de classes que usem metaclasses diferentes de type.

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.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: