Hoje o @RodrigoSetti e o @watinha precisaram fazer um código muito louco que envolvia decorators para alterar a funcionalidade de um método de uma classe da biblioteca unittest.
Como eles não pretendiam alterar o pacote original, pois isso implicaria em bagunças catastróficas, eles prosseguiram com tentativa e erro, raça e uma pequena ajuda do Stack Overflow, até que o código funcionou. Eu que me envolvi um pouco na solução fiquei pensando o motivo da solução ter funcionado e como, então lembrei que havia um termo específico para o que eles haviam feito, monkey patch, ao chegar em casa decidi pesquisar sobre o assunto, o que me motivou a escrever esse post.
A pesquisa me levou a essa resposta linda do Stack Overflow e baseado nela que vou montar minha solução, ou seja, parecerá bastante com uma tradução da resposta mais votada.
No Python há uma leve diferença entre função e método.
>>> class Foo:
... def bar(self):
... pass
...
>>> def baz():
... pass
...
>>> foo = Foo()
>>> foo.bar
<bound method Foo.bar of <__main__.Foo instance at 0x1004d29e0>>
>>> baz
<function baz at 0x1004b5f50>
Um método de uma classe é do tipo bound method, isto é, é uma função vinculada a uma instância de uma classe e consequentemente, por possuir esse vínculo, recebe o primeiro parâmetro self que é a instância da classe.
Callables que são propriedades de uma classe, ainda estão desvinculados, isso permite que você modifique a definição da classe como quiser.
>>> def life(self):
... return 42
...
>>> Foo.life = life
>>> new_foo = Foo()
>>> new_foo.life
<bound method Foo.life of <__main__.Foo instance at 0x1004d29e0>>
>>> new_foo.life()
42
Há um porém nessa solução, ela modifica todas as instâncias da classe.
>>> foo.life()
42
É importante salvar a função atual em uma variável para retornar a classe ao original após o uso.
Se você inocentemente tenta assinalar a função como atributo de uma instância:
>>> def leet(self):
... return 1337
...
>>> foo.leet = leet
>>> foo.leet()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: leet() takes exactly 1 argument (0 given)
Acontece que a função não é automaticamente vinculada a instância e portanto não recebe o self como esperado.
>>> foo.leet
<function leet at 0x1004b6f30>
Para vincular um método a uma única instância podemos utilizar o módulo new:
>>> import new
>>> foo.leet = new.instancemethod(leet, foo, Foo)
>>> foo.leet
<bound method Foo.leet of <__main__.Foo instance at 0x1004d29e0>>
>>> foo.leet()
1337
Desta vez outras instâncias não serão afetadas.
>>> new_foo.leet()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Foo instance has no attribute 'leet'
Esse é um verdadeiro monkey patch.
Vale lembrar que a partir da versão 2.6 do Python o módulo new está deprecated em favor do módulo types, então alteramos nossa solução para:
>>> def qux(self):
... return "quux"
...
>>> import types
>>> foo.qux = types.MethodType(qux, foo)
>>> foo.qux
<bound method ?.qux of <__main__.Foo instance at 0x1004d29e0>>
>>> foo.qux()
'quux'
Por fim, uma outra opção é utilizar types.UnboundMethodType, que faz sentido, agora que sabemos o que é um método vinculado (bound).
Por hoje é só, até a próxima pessoal.


