O porquê do self explícito em Python

Qua 14 maio 2014

Este texto foi baseado em uma conversa com alguns colegas Python Brasil e do canal #python-brasil da Freenode, e foi originalmente publicado em Novembro de 2008 no meu antigo blog. A intenção original era ilustrar informalmente a necessidade do self explícito em Python, mas acabou tornando-se uma introdução ao modelo de dados da linguagem, incluindo o papel de descriptors, metaclasses, entre outras coisas. É também um bom exemplo de como programar de forma orientada a objetos em uma linguagem sem suporte à programação orientada a objetos.

Python é uma linguagem onde há uma preocupação muito maior com a consistência interna e com a própria filosofia do que à obediência cega a padrões e normas. Python não pode simplesmente "ser como Ruby" como alguns desejariam, pois as duas linguagens usam conceitos diametralmente opostos para implementar o suporte simultâneo a programação funcional e orientada a objeto.

Em Ruby os elementos primários são classes, e funções são na verdade métodos de uma classe oculta. Em Python os elementos primários são funções, e métodos são criados com objetos especiais que conseguem saber a quem são atribuídos, os descriptors. Podemos dizer que Ruby é uma linguagem orientada a objetos com suporte a programação funcional, e Python é uma linguagem funcional com suporte a programação orientada a objetos.

Em Python tudo é um objeto, e não há nenhuma diferença prática entre classes e instâncias. Classes são instâncias de metaclasses, e o interpretador faz o trabalho de instanciá-las porque é algo onde o teu esforço não é necessário 99.9% do tempo.

Imagine que Python é uma linguagem puramente funcional, mas você quer programar OO, usando dicionários como estruturas de dados. Vamos primeiro à definição de classe com um exemplo trivial, uma classe Pessoa que vai armazenar nome e data de nascimento da pessoa.

### Classe Pessoa
Pessoa = {}
# construtor, corresponde ao __new__ de Python
def newPessoa(nome, nascimento):
    new = {} # a nova instância
    new['nome'] = nome
    new['nascimento'] = nascimento
    return new

Pessoa['new'] = newPessoa
### Fim da definição de classe

# Ou seja, agora se queremos criar uma nova instância, fazemos:
hank = Pessoa['new']('Hank Moody', (8, 11, 2007))

# Tudo como esperado, temos os dados no dicionário
hank['nome']
hank['nascimento']

Aqui programamos seguindo os princípios de OO sem precisar do suporte sintático. Podemos até mesmo colocar o código que cria a classe em uma função própria para isso:

### Uma função que facilita a criação de nossas "classes"
def newClass(nome, atributos):
    cls = {} # cria o dicionário vazio para a classe
    for k, v in atributos.items(): # atribui os atributos (métodos e
        # atributos de classe)
        cls[k] = v
    return cls

### Classe Pessoa, agora usando a função

# construtor, corresponde ao __new__ de Python
def newPessoa(nome, nascimento):
    inst = {} # a nova instância
    inst['nome'] = nome
    inst['nascimento'] = nascimento
    return inst

Pessoa = newClass('Pessoa', {'newPessoa':newPessoa})
### Fim da definição de classe

# E a classe Pessoa continua funcionando da mesma maneira na hora de
# criar uma instância
hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 2007))

A função criada para gerar a "classe" faz o papel de uma metaclasse, mas note como a rigor não há diferença nenhuma entre ela e a função que representa o método construtor da classe Pessoa. Elas apenas criam uma "instância", não importa do que.

As coisas começam a ficar complicadas e surge a necessidade do self quando resolvemos colocar alguns métodos. Resolvi agora que a 'classe' Pessoa precisa ter um método que vai receber uma tupla com a data atual e me devolver a idade da pessoa. Parece simples, mas...

### Classe Pessoa
# construtor, corresponde ao __new__ de Python
def newPessoa(nome, nascimento):
    inst = {} # a nova instância
    inst['nome'] = nome
    inst['nascimento'] = nascimento
    return inst

def idade(hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    idade = ha - na
    return idade

# agora o método idade tem de entrar aqui também
Pessoa = newClass('Pessoa', {'newPessoa':newPessoa, 'idade':idade})
### Fim da definição de classe

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))

# Lembre que o método pertence à classe, não à instância. Não existe a
# chave 'idade' no dicionário da instância, tenho de usar o dicionário
# que corresponde à classe.

Pessoa['idade']((6, 11, 2008))

Na hora de tentar usá-lo teremos um problema. Métodos são definidos e pertencem à classe, estão no dicionário que a define. De onde virá a variável 'inst' ali no método 'idade' que está na classe? A função está simplesmente errada e resulta em um NameError:

Traceback (most recent call last):
File "", line 38, in <module>
Pessoa['idade']((6, 11, 2008))
File "", line 24, in idade
nd, nm, na = inst['nascimento']
NameError: global name 'inst' is not defined

A única maneira de consertar isso é colocar o 'inst' na lista de parámetros para que o método saiba com que instância está lidando, e então passá-lo explicitamente ao chamar a função. O 'inst' usado aqui é o equivalente ao 'self', e as coisas começam a clarear.

def idade(inst, hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    idade = ha - na
    return idade

# agora o método idade tem de entrar aqui também
Pessoa = newClass('Pessoa', {'newPessoa':newPessoa, 'idade':idade})
### Fim da definição de classe

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))

# Claro que aqui posso usar apenas a função idade() diretamente, mas a
# intenção ao programar OO não é essa.

idade(hank, (6, 11, 2008))

# A função é um método, que pertence a classe e deve ser acessível por
# ela, então se quero usar o método Pessoa.idade com a instância que
# criei, tenho que usar o dicionário que corresponde à classe:

Pessoa['idade'](hank, (6, 11, 2008))

Porém ainda temos de saber a que classe a instância pertence, pois o correto é que a instância saiba e tenha acesso ao método. O construtor precisa armazenar no dicionário uma referência para a sua classe:

### Classe Pessoa

# construtor, corresponde ao __new__ de Python
def newPessoa(nome, nascimento):
    inst = {} # a nova instância
    inst['classe'] = Pessoa # a instância tem de saber a que classe pertence
    inst['nome'] = nome
    inst['nascimento'] = nascimento
    return inst

def idade(inst, hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    idade = ha - na
    return idade

Pessoa = newClass('Pessoa', {'newPessoa':newPessoa, 'idade':idade})
### Fim da definição de classe

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))

# Como agora a instância sabe sua classe, podemos encontrar o
# método sem precisar saber a classe:

hank['classe']['idade'](hank, (6, 11, 2008))

A coisa está melhorando. O inconveniente maior aqui é termos de repetir a instância duas vezes: uma para encontrar a classe que tem a função que estamos usando como método, e outra para passar a instância como parâmetro na chamada do método. O ideal seria que, de alguma forma, quando usei a instância para encontrar a classe e o método ele já viesse sabendo quem está pedindo por ele. Um inconveniente menor é ter de chegar a ela por dois passos, tendo de primeiro consultar a classe. O ideal seria a instância já ter acesso direto.

Se você não entendeu algo da explicação até aqui, releia e tenha certeza que entendeu tudo, pois esse é o ponto crítico.

O problema crucial é como passar a instância para a função que está sendo usada como um método. A solução mais simples que temos é fazer com que o construtor consulte todos os métodos que a classe tem e crie no dicionário da própria instância uma outra função "embrulhando" aquela original, mas que já inclua a instância. Ela será um atalho para encontrar o método diretamente através da instância, sem necessidade de consultar a classe, ou de passar explicitamente a instância, pois já sabe a quem pertence:

# precisaremos usar isso logo adiante...
from functools import partial

# a função que facilita a criação de nossas "classes"
def newClass(nome, atributos):
    cls = {} # cria o dicionário vazio para a classe
    for k, v in atributos.items(): # atribui os atributos (métodos e
    # atributos de classe)
    cls[k] = v
    return cls

### Classe Pessoa

# construtor, corresponde ao __new__ de Python
def newPessoa(nome, nascimento):
    inst = {} # a nova instância
    inst['classe'] = Pessoa # a instância tem de saber a que classe pertence

    # Agora a instância vai criar os métodos embrulhando as chamadas
    # para as funções originais em uma nova, já se incluindo nela
    for k, v in Pessoa.items():
        # Se for função...
        if callable(v):
            # é, então vamos embrulhar...
            metodo = partial(v, inst)
            # a linha acima equivale a: lambda *a, **k: v(inst, *a, **k)
            # mas não podemos usar isso pois as variáveis nos loops
            # são passadas simbolicamente e acabaríamos somente com a
            # última chamada.
            inst[k] = metodo

    inst['nome'] = nome
    inst['nascimento'] = nascimento
    return inst

def idade(inst, hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    x = ha - na
    return x

Pessoa = newClass('Pessoa', {'newPessoa':newPessoa, 'idade':idade})
### Fim da definição de classe

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))

# Como agora temos o metodo idade no dicionário instância e ele sabe a
# quem pertence, podemos fazer a chamada direto

print hank['idade']((6, 11, 2008))

# Para garantir que está funcionando mesmo, vamos criar uma nova
# instância com valores diferentes

fante = Pessoa['newPessoa']('John Fante', (8, 4, 1909))
print fante['idade']((6, 11, 2008))

Aqui, finalmente, temos o comportamento esperado. Podemos criar classes, instâncias, métodos e conseguir acessar tudo quase como faríamos com o suporte a OO. Se entendeu tudo até aqui, provavelmente já entendeu porque o self explícito existe e porque está tão entranhado em Python e faz parte da natureza da linguagem. Como poderíamos retirar aquele 'inst' das funções que são métodos?

Note que apesar da natureza bem estranha desse código para alguém habituado com Python, não há nada de errado com ele. Se há intenção de escrever código OO em uma linguagem sem suporte, essa é uma maneira perfeitamente válida.

Uma alternativa já foi debatida antes e resultou no hack do link abaixo, uma brincadeira que foi parar no Pythonbrasil. A cada chamada do objeto método - no caso do exemplo aqui seria aquele partial() que é criado em newPessoa() - a função é recriada inserindo a instância no namespace.

http://wiki.python.org.br/NoSelf

Outra alternativa aparentemente mais simples, mas que é aquela cujos problemas esse exemplo busca ilustrar melhor, seria alterar o compilador para distinguir entre funções criadas dentro do namespace de classes e fora deles, ou seja, ver métodos e funções como coisas diferentes. Acredito que a essa altura esteja óbvio como isso não seria uma solução em si, mas seria simplesmente recriar os fundamentos da linguagem, eliminando completamente essa simplicidade que Python tem e permite tanta liberdade.

Continuando, talvez já tenha notado que a função construtora, newPessoa(), é na verdade algo bem genérico. A única coisa específica à instância que ela faz nessa altura, é armazenar os valores de 'nome' e 'nascimento' no dicionário. Podemos também torná-la uma função genérica que pode ser usada para todas as classes, semelhante à newClass(), e tornar a parte que cria os atributos aquela que realmente interessa, como acontece em Python onde só __init__ é usado com frequência. Até por vício alguns chamam de construtor, mas o construtor de fato é o raramente usado __new__.

### Classe Pessoa
# construtor, corresponde ao __new__ de Python
def newPessoa(nome, nascimento):
    inst = {} # a nova instância
    inst['classe'] = Pessoa # a instância tem de saber a que classe pertence

    # Agora a instância vai criar os métodos embrulhando as chamadas
    # para as funções originais em uma nova, já se incluindo nela
    for k, v in Pessoa.items():
        # Se for função...
        if callable(v):
            # é, então vamos embrulhar...
            metodo = partial(v, inst)
            # a linha acima equivale a: lambda *a, **k: v(inst, *a, **k
            # mas não podemos usar isso pois as variáveis nos loops
            # são passadas simbolicamente e acabaríamos somente com a
            # última chamada.
            inst[k] = metodo

    # a própria newPessoa vai chamar initPessoa()
    inst['initPessoa'](nome, nascimento)
    return inst

# inicializador, corresponde ao __init__ de Python
def initPessoa(inst, nome, nascimento):
    inst['nome'] = nome
    inst['nascimento'] = nascimento
    # assim como fazemos normalmente no __init__, aqui não precisamos
    # nos preocupar

def idade(inst, hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    x = ha - na
    return x

# e agora initPessoa() tem de entrar aqui também
Pessoa = newClass('Pessoa', {'newPessoa':newPessoa,
                             'initPessoa':initPessoa,
                             'idade':idade})
### Fim da definição de classe

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))

# Como agora temos o metodo idade no dicionário instância e ele sabe a
# quem pertence, podemos fazer a chamada direto

print hank['idade']((6, 11, 2008))

# Para garantir que está funcionando mesmo, vamos criar uma nova
# instância com valores diferentes

fante = Pessoa['newPessoa']('John Fante', (8, 4, 1909))
print fante['idade']((6, 11, 2008))

E tudo continua funcionando exatamente como antes, mas agora todo o código que cria a instância está devidamente isolado, deixando a parte que interessa somente em initPessoa(). Apesar disso, a função newPessoa() ainda não está genérica como deveria, pois ela ainda usa a global Pessoa diretamente para informar a instância.

Isso parece exatamente com a situação que aconteceu lá em cima, quando criamos o método idade() e precisávamos saber a instância e resolvemos passá-la como primeiro parâmetro, não parece? A diferença é que aqui a instância é a classe, ou seja, newClass está para Pessoa como newPessoa está para inst. Pessoa é então uma instância da Class abstrata que existe aí, mas que não havíamos pensado até então porque era vista como função apenas. Podemos fazer a mesma coisa, passar a classe como primeiro argumento pois ela é uma instância de Class:

# precisaremos usar isso logo adiante...
from functools import partial

### Agora já podemos pensar nela como a classe 'Class'
def newClass(nome, atributos):
     cls = {'nome':nome} # cria o dicionário para a classe somente com seu nome
     for k, v in atributos.items():
     # aqui se um método for o newNome, ele tem de receber a mesma
     # alteração e passar a receber 'cls' como primeiro argumento
     if k == 'new'+nome:
         v = partial(v, cls)
     # e atribuímos tudo normalmente
     cls[k] = v
     return cls

### Classe Pessoa

# construtor, corresponde ao __new__ de Python
def newPessoa(cls, nome, nascimento): # agora a classe mesmo vem aqui
    inst = {} # a nova instância
    inst['classe'] = cls # a instância tem de saber a que classe pertence, e
    # agora pode saber dinamicamente

    # Agora a instância vai criar os métodos embrulhando as chamadas
    # para as funções originais em uma nova, já se incluindo nela
    for k, v in cls.items():
        # Se for função...
        if callable(v):
            # é, então vamos embrulhar...
            metodo = partial(v, inst)
            # a linha acima equivale a: lambda *a, **k: v(inst, *a, **k

            # mas não podemos usar isso pois as variáveis nos loops
            # são passadas simbolicamente e acabaríamos somente com a
            # última chamada.
            inst[k] = metodo

    # a própria newPessoa vai chamar initPessoa()
    inst['init'+cls['nome']](nome, nascimento)
    return inst

# inicializador, corresponde ao __init__ de Python
def initPessoa(inst, nome, nascimento):
    inst['nome'] = nome
    inst['nascimento'] = nascimento

def idade(inst, hoje):
    hd, hm, ha = hoje
    nd, nm, na = inst['nascimento']
    x = ha - na
    return x

# e agora initPessoa() tem de entrar aqui também
Pessoa = newClass('Pessoa', {'newPessoa':newPessoa,
 'initPessoa':initPessoa,
 'idade':idade})
### Fim da definição de classe

hank = Pessoa['newPessoa']('Hank Moody', (8, 11, 1967))

# Como agora temos o metodo idade no dicionário instância e ele sabe a
# quem pertence, podemos fazer a chamada direto

print hank['idade']((6, 11, 2008))

# Para garantir que está funcionando mesmo, vamos criar uma nova
# instância com valores diferentes

fante = Pessoa['newPessoa']('John Fante', (8, 4, 1909))
print fante['idade']((6, 11, 2008))

E pronto. Esse código implementa a metaclasse Class, responsável por criar qualquer classe tendo o nome e seus atributos; o método equivalente ao __new__, que cria a instância; e o equivalente ao __init__, que inicializa. Os métodos foram simulados usando uma função embrulhando a original, mas em Python de fato eles utilizam descriptors e não são copiados para todas as instâncias, sendo gerados dinamicamente.

Finalmente, a surpresa final, só para mostrar mais uma vez a genialidade e a simplicidade da implementação. Aqui eu implementei uma metaclasse base o mais simples possível para mostrar como na teoria não há distinção entre métodos e funções em Python. O que acontece se eu resolvo usar essas funções de inicialização e cálculo da idade com a metaclasse padrão real de Python, ao invés da minha função que apenas simula uma criando dicionários?

>>> def initPessoa(inst, nome, nascimento):
... inst['nome'] = nome
... inst['nascimento'] = nascimento
...
>>> def idade(inst, hoje):
... hd, hm, ha = hoje
... nd, nm, na = inst['nascimento']
... x = ha - na
... return x
...
>>> Pessoa = type('Pessoa', (), {'__init__':initPessoa,
... 'idade':idade})
>>> Pessoa
<class '__main__.Pessoa'>

Surge uma classe, exatamente como seria se tivesse sido declarada diretamente.

É preciso apenas corrigir o código para acessar tudo como atributos normais e não como chaves de dicionários. Feito isso, tudo funciona normalmente, sem distinção alguma:

>>> def initPessoa(inst, nome, nascimento):
... inst.nome = nome
... inst.nascimento = nascimento
...
>>> def idade(inst, hoje):
... hd, hm, ha = hoje
... nd, nm, na = inst.nascimento
... x = ha - na
... return x
...
>>> Pessoa = type('Pessoa', (), {'__init__':initPessoa,
... 'idade':idade})
>>> print Pessoa
<class __main__.Pessoa>
>>> hank = Pessoa('Hank Moody', (8, 11, 1967))
>>> print hank
<__main__.Pessoa object at 0xb7bab5cc>
>>> print hank.idade((6, 11, 2008))
41
>>> fante = Pessoa('John Fante', (8, 4, 1909))
>>> print fante
<__main__.pessoa>
>>> print fante.idade((6, 11, 2008))
99

Para não deixar dúvidas, tudo isso equivale a:

class Pessoa(object):
    def __init__(self, nome, nascimento):
        self.nome = nome
        self.nascimento = nascimento

 def idade(self, hoje):
    hd, hm, ha = hoje
    nd, nm, na = self.nascimento
    x = ha - na
    return x

Enfim, python faz todo o resto que vimos aqui enquanto você não está olhando.

Comments