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:
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.
Muito bom, adorei os dois últimos posts. Está de parabéns!
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,
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!
Post a Comment