GMGall’s blog

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

Arquivos de tags: design patterns

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