Explorando metaprogramação em Python: django-supermodels

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!

Tags: , , , , ,

11 Responses to “Explorando metaprogramação em Python: django-supermodels”

  1. Olá Guilherme, muito interessante teu artigo. Não conhecia esses métodos de busca dinâmicos do rails. Deve ser um dos motivos do pessoal chamar ele de “mágico” :-) .

    Mas em relação ao projeto supermodels, eu deixaria os “find_by*” no manager mesmo (objects), já que o padrão usado no Django é esse mesmo: quem faz as buscas é o Manager (que por sua vez repassa pra QuerySet). E não o Model em si.

  2. Vanderson says:

    Muito legal implementar uma feature do Rails no Django.

    Outra coisa legal seria se o Django tivesse o has_many e belongs_to do Rails em relacionamentos 1- * ao invés de tascar uma declaração de foreignKey.

  3. Interessante o projeto!

    Concordo com o Igor… isso deve ficar no manager, pois ele serve justamente para fornecer esse tipo de funcionalidade.

    []s!

  4. Oi, Guilherme,

    Mais algumas dicas em relação a questão de Model e Manager: o objects é o models.Manager implementado por default em qualquer models.Model. Se você adicionar um outro models.Manager à tua classe, você vai “perder” o objects.

    A melhor solução pro teu caso acaba sendo por definir uma classe SupermodelManager, herdando de models.Manager, que implemente o método find_by, e vai adicionar uma instância do seu SuperModelManager nas classes de Model que você quer ter os finders. , então você vai mexer no __getattribute__ do Model, que vai implementar o method_missing e fazer chamada para o find_by do teu SuperManager.

    Outra coisa: não sei se você precisa usar o method_missing para ter os seus finders. Se você está implementando o __getattribute__, você sabe qual é o nome do método que está chamando. Ou seja, se você está implementando __getattribute__(self, name), basta fazer um if name.startswith(”find_by”), para descobrir se o que você quer é o acesso a um atributo ou se quer fazer uma chamada ao find_by. Usar method_missing, no caso, parece-me desnecessariamente custoso, é melhor lidar com comparação de string do que sempre ficar lidando a exceção NotImplementedError.

    • @Raphael Lullis

      Se vc olhar o source vai ver que tudo o que eu fiz é exatamente o que vc está falando :) O meu objetcs é uma subclasse do Manager do Django, exatamente isso.

      E eu usei esse exemplo com o nome “method_missing” aqui no blog só para facilitar o paralelo, mas se você for ver no código do django-supermodels está do jeito que vc está falando aí ;)

  5. Fala guilherme, acho a reescrita do método __getattr__ mais adequada, pois __getattribute__ é invocado para toda chamada de um attributo/método. Entretanto, __getattr__ só é chamado quando o attributo não existe.

    Qual foi o problema de usar algo parecido com isso no Model??? :

    from … import Model as DjangoModel

    class SuperModel(DjangoModel):
    def __getattr__(self, attr):
    if attr.startswith(’find_by_’):
    return busca no banco
    else:
    return DjangoModel,__getattr__(self,attr)

  6. Uma boa observação:
    __getattribute__ é sempre chamado quando algum atributo ou método é invocado, mesmo se existir!
    O método que é chamado se, e somente se, o atributo ou método não existir é o __getattr__.

    Conserta aí ;)

    • @Gustavo Rezende, @Hugo Lopes Tavaves

      Obrigado pela observação, vou consertar :)

      @Gustavo

      Sobre as dificuldades, você pode olhar o source para entender o problema. Tem um rascunho lá que você pode usar para testar se quiser.

  7. Henrique says:

    Qual a diferença com relação ao já existente Model.objects.filter(**kwargs)?

    Model.objects.filter(x=1, y=2)
    Model.objects.find_by_x_and_y(1, 2)

    Ou foi só pra deixar igual o Rails? Em todo caso, acho kwargs bem mais elegante que métodos diferentes recebendo parâmetros posicionais, conserva a mesma sintaxe para qualquer filtro.

    • Quando vc está estudando vc não precisa necessariamente fazer coisas que façam sentido. No meu caso minha única exigência é que só precisa ser divertido :) Eu aprendi um bocado de metaprogramação em Python fazendo isso e experimentando possiveis soluções, além de ter olhado e conhecido um bocado do código fonte dos models do Django. Faça isso vc tb, eu recomendo :)

      [ ]s, gc

Leave a Reply