Posts Tagged ‘Metaprogramação’

Explorando metaprogramação em Python: django-supermodels

Tuesday, March 10th, 2009

Esses dias trabalhando com Python comecei a me perguntar se seria possível fazer no Django alguma coisa parecida com os finders dinâmicos do Rails.

A maioria das pessoas que lêem esse blog devem saber do que se trata mas em todo caso funciona assim: se uma classe Person do model do Rails tem uma propriedade “name” e outra propriedade “country”, você ganha automaticamente uma série de métodos dinâmicos para buscas por objetos dessa classe:

Person.find_by_name("Guilherme")
Person.find_by_country("BR")
Person.find_by_name_and_country("Guilherme", "BR")
# e outros...

Tudo isso é possível graças à funcionalidade de method missing do Ruby. Quando um método que não existe é invocado em uma classe, uma exceção do tipo NoMethodError é lançada. Alternativamente você pode implementar na sua classe o método method_missing, que é automaticamente invocado quando são invocados métodos que não existem.

class Exemplo
  def method_missing(metodo, *args)
    puts "Chamou '#{metodo}' com os params. '#{args}'"
  end
end
Exemplo.new.um_metodo_qualquer("teste")
 
# retorna: 
# "Chamou 'um_metodo_qualquer' com os params. 'teste'"

Usando essa funcionalidade foi possível fazer o tratamento dessas chamadas find_by_* traduzindo para chamadas comuns ao método find(…) com parâmetros.

Na minha pesquisa para ver se isso era possível em Python, acabei criando o projeto django-supermodels (créditos ao Ramalho pelo nome infame), que é uma extensão dos models do Django para prover essa funcionalidade.

Para minha surpresa foi trivial implementar exatamente a mesma coisa em Python usando o método __getattr__, que também é invocado caso um método/atributo não exista:

class Exemplo(object):
  def __getattr__(self, metodo):
    def method_missing(*args):
      print "Chamou '%s' com os params. '%s'" % (metodo, args)
    return method_missing
Exemplo().um_metodo_qualquer("teste")
 
# retorna:
# "Chamou 'um_metodo_qualquer' com os params. '('teste',)'"

Foi um pouco mais difícil fazer isso funcionar dentro do Django, porque a classe django.db.models.Model é construída de uma forma muito estranha que dificulta demais que a minha classe simplesmente herde da classe do Django. No fim das contas acabei criando os finders dinâmicos no manager, que é o objeto que dá acesso às buscas de objetos no banco de dados. Ficou assim:

Person.objects.find_by_name('Guilherme Chapiewski') 
Person.objects.find_by_id(2)
Person.objects.find_by_name_and_id('Guilherme Chapiewski', 2)
Person.objects.find_by_id_and_name(2, 'Guilherme Chapiewski')
 
Person.objects.find_by_nonexistingfield('something')
# Cannot resolve keyword 'nonexistingfield' into field.
# Choices are: age, birth_date, country, id, name

Estou trabalhando para não precisar usar o “.objects” assim como é no Rails, mas isso está dando um pouco mais de trabalho porque envolve entender melhor a forma que o model é criado dentro do Django. Depois disso vou trabalhar também em algumas opções mais poderosas de finders para poder justificar de verdade seu uso.

Antes que alguém fale, não estou tentando transformar o Django em Rails, estou apenas criando um extensão do model que eu acho bem legal e útil, e que você pode usar ou não. :)

O código fonte está disponível no meu Github. Já é possível usar esse plugin no seu projeto Django bastando apenas instalar o egg do projeto. Para isso, baixe o código fonte e execute “sudo python setup.py install”. Para saber como usar, veja a aplicação Django “example” que está incluída no código fonte.

Como esse projeto nasceu de um código de estudo está um pouco desorganizado, mas pretendo resolver isso em breve.

Sugestões são bem-vindas!