Tuesday, March 24, 2009

Metaprogramação 2: o caso do method_missing

Depois de um rigoroso regime de Java ou Delphi, as pessoas ficam maravilhadas ao encontrar na classe Object da linguagem Ruby o método method_missing.

Como fazer isto em Python? Bom, uma busca rápida no Google revela uma solução feita por um programador Ruby que recentemente adotou Python. Ele complicou demais a questão. Vou dar a minha solução mais pragmática.

Antes de mais nada, o que faz o method_missing?

Quando o interpretador Ruby encontra uma invocação de um método que não existe, digamos x.spam, ele faz uma segunda tentativa invocando x.method_missing(:spam) . Se a classe de x não implementa este method_missing, ela herda a implementação da classe Object (veja method_missing na documentação do Ruby), que apenas reporta um erro.

A graça é que você pode implementar method_missing nas suas classes, e fazer o que quiser. Uma aplicação comum é implementar métodos dinâmicos que fazem coisas diferentes dependendo do nome que foi invocado. Em Rails você vê coisas como Cliente.find_by_cpf, onde cpf é um atributo, e find_by_cpf é um método que não existe mas a invocação é tratada por um method_missing que faz uma busca por CPF.

Origem da idéia

Não sei se este mecanismo existia antes do Smalltalk, mas ele fazia parte do Smalltalk-80, lançado há quase 30 anos. Em Smalltalk o método se chamava doesNotUnderstand: e tem um artigo co-escrito pelo Ralph Johnson (da Gangue dos Quatro) que discute o uso dele em metaprogramação.

E em Python?

Python é diferente de Ruby porque a interface pública dos objetos em Python é formada por apenas por atributos, e um método é simplesmente um atributo invocável (callable). Em Ruby a interface pública dos objetos é formada só por métodos. Então o mecanismo análogo em Python lida com atributos não encontrados, e não com métodos. O nome poderia ser attribute_missing mas na verdade é __getattr__. Para entender o __getattr__, tem um exemplo simples no meu texto anterior.

O exemplo de method_missing documentação do Ruby pode ser implementando em Python de forma muito simples usando apenas __getattr__:


# coding: utf-8

'''
Instâncias de Romano têm atributos dinâmicos que devolvem o valor
inteiro correspondente ao numeral romano acessado::

>>> r = Romano()
>>> r.III
3
>>> r.mmix # caixa-baixa também funciona
2009

'''
# Inspirado pela documentação do método Object#method_missing da linguagem Ruby
# e aproveitando o módulo roman.py de Mark Pilgrim, publicado no livro
# Dive into Python: http://diveintopython.org/

from roman import fromRoman, InvalidRomanNumeralError

class Romano(object):
def __getattr__(self, s):
try:
return fromRoman(s.upper())
except InvalidRomanNumeralError:
raise AttributeError('Numeral romano invalido: %r' % s)

if __name__=='__main__':
import doctest
doctest.testmod()



Note que na solução pythônica, acessar r.x significa acessar um atributo cujo valor é calculado dinamicamente, e não acessar um método. Se o objetivo é acessar um método mesmo, basta que o __getattr__ retorne um objeto invocável. No próximo post, a solução para isso.

4 comments:

Unknown said...

No caso do Ruby, são métodos porque Ruby é leniente com relação a chamar os métodos com ou sem parênteses - então implementando o truque do method_missing, independente se o programador espera um método ou atributo, ele recebe um resultado.

Particularmente não gosto dessa e outras ambiguidades de Ruby, e gostei da solução proposta em Python pois é consistente com o uso de atributos.

Danilo Cabello said...

Muito bom, adorei os dois últimos posts. Está de parabéns!

Willian said...

o que acho interessante da metaprogramação de Ruby é poder adicionar métodos e outros comportamentos à classes já existentes.

como fazer isso em Python?
como adicionar um método à uma classe já existente e Python?


abraços,

Dirceu Pereira Tiegs said...

Oi Luciano,

Parabéns pelos dois posts sobre metaprogramação.

Apesar de preferir a sintaxe de Ruby em alguns casos (como na criação de DSLs), realmente a consistência da sintaxe de Python ajuda muito na legibilidade.

Muito legal você ter voltado a blogar, abraço!