by Caio Mizerkowski
4 min read

Motivação

Por meio de um curso na Alura descobri que no JS existe o método bind, que permite conectar uma função a uma instância de uma classe. Isso sem a necessidade de criar a função como um método interno da classe.

O exemplo no curso é o seguinte:

class Pessoa {
    constructor(nome) {
        this.nome = nome;
    }
}

function exibeNome() {
    alert(this.nome);
}

let pessoa = new Pessoa('Fulano');
exibeNome = exibeNome.bind(pessoa);
exibeNome();

Em vista disso, me desafiei a conseguir o mesmo resultado através do Python. Precisava para tal criar uma classe com dois atributos e um método para ser comparado a função que será integrada a posteriore na instância.

class Foo:
    def __init__(self, nome, idade):
        self._nome = nome
        self._idade = idade

    def get_nome(self):
        return self._nome

Enquanto o objetivo principal é reproduzir o mesmo comportamento de um método, é importante notar que métodos e funções possuem tipos diferentes dentro do Python.

foo = Foo('Caio', 25)
print('foo:',foo)

print(foo.get_nome())
print(type(foo.get_nome))
print(foo.get_nome)
foo: <__main__.Foo object at 0x00000218FC8EE280>
Caio
<class 'method'>
<bound method Foo.get_nome of <__main__.Foo object at 0x00000218FC8EE280>>

Tendo estes pormenores em mente, comecei a fazer algumas experimentações com closures. Chegando a esse código, no qual a função inner da closure é salva para foo.get_idade, recebendo como parâmetro em sua criação a instância foo. Esse parâmetro é lembrado pela função e permite a chamada dos atributos da instância em seu interior.

def get_idade(self):
    def inner():
        return self._idade
    return inner

foo.get_idade = get_idade(foo)
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'function'>
<function get_idade.<locals>.inner at 0x00000218FC8DF8B0>

Stackoverflow

Após chegar a uma das soluções para o problema, procurei para ver se encontrava maiores discussões sobre isso e formas mais canonicas de realizar o mesmo processo. A principal página que consultei foi esta questão no stack overflow. As diversas respostas me permitiram ver soluções variadas para o problema e entender um pouco melhor o que acontecia.

Duas soluções se destacam entre as apresentadas, por funcionarem não somente conectando a função a classe como também alterando o seu tipo para bound method e, pelo menos numa primeira analise, tornando-as tais quais os métodos nativos da classe. Como a chamada direta dos dunder methods não é aconselhada, o uso da biblioteca types me parece a solução mais aconselhada para o problema.

def get_idade(self):
    return self._idade

foo.get_idade = get_idade.__get__(foo, Foo)
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'method'>
<bound method get_idade of <__main__.Foo object at 0x00000218FC8EE280>>
import types
foo.get_idade = types.MethodType(get_idade, foo)
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'method'>
<bound method get_idade of <__main__.Foo object at 0x00000218FC8EE280>>

Outras formas de se conseguir o mesmo efeito também estão presentes, como o uso da lambda function que nesse contexto não deixa de se comportar como uma closure.

def get_idade(self):
    return self._idade

foo.get_idade = lambda: get_idade(foo)
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'function'>
<function <lambda> at 0x00000218FC901310>

Uma forma mais sofisticada de usar funções aninhadas também está presente, o que me gerou o estalo para produzir o decorador que vou apresentar. Esse método permite com que diversos parâmetros sejam passados a função por esta se preocupar em trata-los.

def bind(instance, method):
    def binding_scope_fn(*args, **kwargs): 
        return method(instance, *args, **kwargs)
    return binding_scope_fn

foo.get_idade = bind(foo, get_idade)    
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'function'>
<function bind.<locals>.binding_scope_fn at 0x00000218FC9015E0>

Descobri também nas respostas a possibilidade de se usar uma partial function, algo que nunca usei em Python mas já possui a necessidade de usar enquanto fazia meus projetos de IC no Matlab.

from functools import partial
foo.get_idade = partial(get_idade, foo)
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'functools.partial'>
functools.partial(<function get_idade at 0x00000218FC901280>, <__main__.Foo object at 0x00000218FC8EE280>)

Transformando num decorador

Inspirado nas diversas respostas, resolvi montar um decorador para estes casos. Permitindo então aplicar o mesmo a qualquer função e a integrar em uma instância já em funcionamento.

def bind(self):
    def inner_bind(function):
        def inner(*args, **kwargs):
            return function(self, *args, **kwargs)
        return inner
    return inner_bind

@bind(foo)
def get_idade(self):
    return self._idade

foo.get_idade = get_idade
print(foo.get_idade())
print(type(foo.get_idade))
print(foo.get_idade)
25
<class 'function'>
<function bind.<locals>.inner_bind.<locals>.inner at 0x00000218FC9010D0>

Como se pode ver, embora se comporte como esperado, ainda não é visto pelo Python como um bound method da instância. Vamos arrumar isso usando a biblioteca types, conseguindo assim o efeito e o tipo pretendido para o agora método.

import types
def bind(self):
    def inner_bind(function):
        def inner(*args, **kwargs):
            return function(*args, **kwargs)
        return types.MethodType(inner, self)
    return inner_bind

E vamos testar o mesmo com outra variável, para vermos seu funcionamento no modo esperado.

foo._universidade = 'UFPR'

@bind(foo)
def get_universidade(self):
    return self._universidade

foo.get_universidade = get_universidade
print(foo.get_universidade())
print(type(foo.get_universidade))
print(foo.get_universidade)
UFPR
<class 'method'>
<bound method bind.<locals>.inner_bind.<locals>.inner of <__main__.Foo object at 0x00000218FC8EE280>>

Monkey Patching

Esse processo é bem similar ao monkey patching. A principal diferença é que no monkey patching se altera a própria classe, enquanto que nesse processo está sendo alterado a instância. Portando, caso se crie uma nova instância da classe estes métodos adicionados não estarão presentes. O erro a seguir mostra bem isso.

foo2 = Foo('Fulano',55)
foo2.get_idade()
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_7552/2613053042.py in <module>
      1 foo2 = Foo('Fulano',55)
----> 2 foo2.get_idade()


AttributeError: 'Foo' object has no attribute 'get_idade'