Quinta-feira, Novembro 06, 2008

O porquê do self explícito... será que agora vai?

Eu tenho um sonho, que um dia todos os pythonistas sejam julgados não por usar vim ou emacs, django ou plone, ou pelo quanto odeiam Java, mas pelo quanto gostam da linguagem e tentam entendê-la antes de apontar defeitos.


Brincadeiras à parte, o assunto "o porquê do self-explícito" voltou à tona, ontem à noite no IRC, hoje dominou a lista, e cheguei à conclusão que as explicações que existem em português ainda não bastam e muita gente ainda não entendeu. Quando vejo alguém dizendo que não gosta, que já leu as explicações e discussões, mas ainda acha que deve haver uma maneira de resolver, então é porque ainda não entendeu bem e não percebeu que não é um defeito. É uma característica, seja negativa ou não, depende de cada um, mas é uma característica conseqüente de todos os fundamentos da linguagem.

Depois de uma breve discussão ontem acabei pensando em uma maneira mais simplificada ainda de explicar que talvez ajude os indecisos, e de quebra ainda ajuda a entender um monte de outras coisas, desde a diferença entre __init__ e __new__, passando por descriptors e até metaclasses. Primeiro, retornando resumidamente aos pontos já bem explicados:

1. 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.

2. Python dá suporte sintático a programação orientada a objetos, mas não a tudo. Não cabe discutir se isso é bom ou ruim, discutível ou não, mas é a filosofia da linguagem ser o mais simples e dar suporte apenas ao que tem importância prática, sem puritanismo. O programador é mais produtivo se não tem de ficar brigando com compiladores, mas tem de saber fazer bom uso dessa liberdade.

3. Python não pode simplesmente "ser como Ruby" como alguns querem, pois as duas linguagens usam conceitos diametralmente opostos para implementar programação OO e funcional ao mesmo tempo.

4. Em Ruby os elementos primários são classes, e funções definidas 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 um objeto especial que consegue saber a quem ele é atribuído, os descriptors.

5. 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 seu esforço não é necessário 99.9% do tempo

Tudo isso já foi bem discutido e explicado na lista e em outros lugares. Considerando isso, a idéia mais fácil que tive para explicar foi usando dicionários, pois fica muito mais claro como acontece de fato. Partindo da idéia de que não existisse suporte sintático a OO em Python e você quisesse programar OO mesmo assim, usando dicionários, da mesma forma que faria com C ou em Lua, por exemplo. Vamos primeiro à definição de classe com o exemplo mais trivial, uma classe Pessoa que vai armazenar nome e data de nascimento da pessoa (não ando muito criativo para exemplos). Ler os comentários no código é essencial.

Não tive tempo de colorir a sintaxe, mas duvido que isso atrapalhe quem queira entender, mas garanto que todos os códigos rodam (a menos que o Blogger tenha bagunçado a indentação de novo).



### 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 ali no dicionário
hank['nome']
hank['nascimento']





Até aqui, aparentemente programamos seguindo os princípios de OO sem precisar do suporte sintático. Podemos até mesmo já colocar o próprio 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))



Essa 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 estão criando uma "instância", não importa do que.

A coisa começa a ficar complicada e surge 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))



Agora na hora de tentar usá-lo que vai aparecer um probleminha. 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
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' aqui é o equivalente ao 'self', e as coisas começam a clarear por aqui.



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 de que classe é a instância 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, claro:



### 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 de que classe é!
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. Aos mais experientes, me perdoem se fui condescendentemente simplista até aqui, mas achei melhor exagerar na simplicidade para ficar com uma referência válida para todos, inclusive os que acabam de chegar à Python. Meu único temor é que talvez acreditem que o suporte a OO de Python é essa gambiarra mostrada aqui. Não é! Isso é apenas um exemplo que ilustra a grosso modo o que acontece por debaixo dos panos.

Então o problema crítico é 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. Resumindo, 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 novamente pois já sabe 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 de que classe é!

# 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 funcionando. 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. Aquele 'inst' é definitivamente a solução mais simples, e não é possível adotar outra coisa a não ser com um hack que arruinaria completamente o trabalho.

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. O código está lá e tão exaustivamente explicado quanto aqui:

http://www.pythonbrasil.com.br/moin.cgi/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 então 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 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 de que classe é!

# 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, afinal ela ainda está usando a global Pessoa diretamente para informar a instância de que classe ela é.

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 de que classe é, 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
# 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 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 de Python e de como as opções para implementar um 'self' implícito arruina tudo isso e vai de frente à filosofia da linguagem. 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. Adivinha 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




Surge uma classe, exatamente como seria se tivesse sido criada normalmente!

É 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, caso tenha ficado, isso tudo é o equivalente a apenas:



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...

17 comentários:

JS disse...

Santa paciência, Werneck! :-)
Eu já tinha entendido essa questão do self - mas fiz questão de ler de novo, seguindo os passos: de acordo com um insight que tive no Dojo na LatinoWare: "exercitar os fundamentos é como fazer o tronco de uma árvore crescer - só ai teremos força para expandir os ramos".

Já que você já escreveu tudo isso, se importaria de falar um pouco sobre como funcionam os descriptors e esse binding dinâmico dos métodos para as instâncias?

(A propósito, eu gastei algum tempo hoje tentando um hack para criar um atributo "privado de verdade" para satisfazer nosso amigo javanista que apareceu na lista -- mas conclui que definitivamente não dá. Dá sim pra escrever dezenas de linhas pra tornar o acesso direto a um atributo bem complicado, mas sempre vai ter uma porta de entrada e um camkinho labiríntico a seguir. Eu estava tentando usr o módulo inspect e seus frames, para garantir que o caller de acessor pertencia a própria instância do atributo - mas quando mais eu me esforçava, masi buracos eu achava.

Pedro Werneck disse...

Obrigado JS

Você leu minha mente. Escrevi isso bem corrido, mas enquanto estava nele surgiu a idéia de depois falar sobre descriptors, não só métodos, mas tudo que utiliza eles (property, super), justamente porque o que me impedia antes era ter um ponto de partida legal com explicações básicas. Esse post de hoje cumpre bem esse papel, então agora parece que vai.

Sobre criar um atributo privado MESMO, eu arrisco dizer que é impossível. Talvez, forçando muito a barra da maneira como você falou, inspecionando código de todas as chamadas e verificando frames, seja possível fazer de maneira que em teoria alguém que não tenha qualquer acesso ao código fonte e não faça idéia do que está havendo realmente não consiga. Ficaria algo como esse NoSelf que citei aí, que também foi feito só como piada para mostrar a alguém como era algo mais difícil do que parece, sem chance de uso real. Mas vale a pena brincar qualquer hora... depois me mostre o que você fez.

Thiago F Pappacena disse...

Muito boa explicação, Werneck!

Nunca realmente me preocupei com o self explícito no Python, e acho um jeito bem charmoso para suportar programação funcional junto com OO. Aliás, acho que as pessoas que são religiosamente contra o self explícito nunca programaram de forma organizada no paradigma funcional, com structs sendo passadas para a funções. ;-)

Só torço para que o GvR troque a mensagem de erro com a contagem de parâmetros enviados a métodos com self declarado explicitamente, já que isso realmente confunde bem qualquer novato!

abs!

Fernando Ribeiro disse...

Um dia vou ter um filho assim...

PS.: Só não deixo meu filho brincar de tiro ao alvo :P

Anônimo disse...

Boa explicação de uma maneira de se implementar OO. Não encarem como um flame, mas é que eu também queria entender isso melhor. Essa explicação não é de porque o self é necessário, apenas é uma explicação de uma das muitas maneiras de se implementar OO. Da mesma forma como se implementaria em C puro usando structs e ponteiros e talvez algumas macros para decorar (provavelmente como surgiu o primeiro "C with classes").

Em Ruby, os elementos primários não são classes e métodos de classe (isso seria Java). Em Ruby os elementos primários são "objetos" e "blocos de código" (lambdas). Uma classe é um objeto, uma instância da classe "Class", tanto que podemos fazer "Pessoa = Class.new". "Chamar um método" na realidade é "enviar uma mensagem" tanto que podemos fazer "p = Pessoa.new; p.nome" ou "p.send(:nome)". E o "self" explícito no caso é desnecessário porque usa-se "bindings" para "conectar" o bloco de código (implementação do método) ao contexto do objeto.

Juracy Filho disse...

Grande Werneck !

Parabéns pelo post, adorei o exercício como um todo, bastante elucidativo !

Pedro Werneck disse...

Pappacena,

Exatamente. O ponto é bem esse, de que qualquer um que já tivesse programado OO em uma linguagem sem suporte não deveria ver problema algum, então a maneira mais fácil de explicar foi essa.

Acho que isso ocorre com frequência cada vez maior porque hoje muitos começam a aprender a programar com linguagens orientadas a objeto, como Java. O sujeito começa do zero com o professor mandando simplesmente ignorar aquela sintaxe toda e que depois ele aprende, daí quando vai usar de fato aquilo parece a única coisa normal e aceitável.

Quanto à mensagem de erro, nisso eu concordo plenamente, o problema é que ou vai ficar uma mensagem de erro genérica em tudo, ou seja, qualquer erro no número de parâmetros vai sempre ter lá uma mensagenzinha entre parênteses avisando que pode ser o 'self', ou terá de haver uma captura e mudança da mensagem dentro do descriptor que representa o método, sem deixar a exception propagar naturalmente até o topo. Concordo que é uma necessidade, mas tenho minhas dúvidas quanto à maneira correta de implementar.

Pedro Werneck disse...

Fernando Ribeiro

Brincar não pode mesmo, porque não é brincadeira. :p

Pedro Werneck disse...

Ao Anônimo

Você não entendeu bem o sentido de "primários" no artigo, e eu ainda tomei cuidado de não usar "primeira-ordem" ou "primitivos" até para evitar isso.

Sim, em Python os elementos "primários" no sentido que você emprega também são objetos e blocos de código, uma classe também é um objeto, uma instância de type(), e também podemos fazer Pessoa = type('Pessoa', (), {}).

Existem linguagens orientadas a objeto e programação orientada a objetos, assim como existe linguagem funcional e programação funcional. Você pode programar como quiser em qualquer linguagem, mas algumas dão suporte a determinado estilo ou mais de um.

Ruby é uma linguagem orientada a objetos, seguindo Smalltalk mesmo, que implementa mensagens e bindings de fato e por isso não precisa do self explícito. Python é uma linguagem híbrida, onde tudo são objetos, mas pende mais para funcional na base.

Python então acaba tendo de criar um suporte sintático à programação orientada a objetos, usando descriptors e namespaces. Ruby faz a mesma coisa para criar um suporte sintático à programação funcional, tendo de esconder a classe que na verdade abriga os métodos que você usa como funções. Da mesma forma que com Python métodos na verdade são funções e em algum lugar a chamada de método vai resultar em uma chamada da função que você definiu no namespace da classe, em Ruby funções são na verdade métodos que em algum lugar resultam em uma chamada de um método de uma classe oculta.

A maneira mais fácil de ilustrar isso é mostrando como em Ruby você pode criar atributos de instância e como assim as funções de Ruby violam um dos princípios de programação funcional, que é não ter nenhum efeito adicional fora da função. Por exemplo:

def foo
@foo = "eu devia existir só em foo()"
end

def bar
@bar = "e eu devia existir só em bar"
end

def who
puts instance_variable_get("@foo")+ " mas sou acessível em who!"
puts instance_variable_get("@bar")+" mas eu também estou acessível em who!"
end

foo()
bar()
who()

vinícius. disse...

Fantástico.

Nunca tive problemas com o self explicito do Python, até porque a linguagem é tão boa que digitar self é o de menos, mas agora entendi os motivos. Não tinha entendido antes porque 'não procurei saber', simplesmente usava e pronto.

Esse seu texto é muito claro, primeira vez que fui ler sobre o assunto e consegui entender muito bem. Parabéns!

Anônimo disse...

Talvez o grande problema que há com o self redundante (é assim que prefiro chamar esta singularidade da linguagem, tal como Bruce Eckel disse em sua apresentação na Pycon) é que o Python chama a atenção (e atrai as pessoas) por ser um projeto bem feito e conduzido, voltado para a para produtividade do desenvolvedor, então causa estranheza ter que digitar mecanicamente "self" a cada novo método, independente se há motivos internos para que ele esteja ali sendo repetido até causar náuseas.
Além disso fica aquela sensação que a implementação de OO do Python é meia bomba, em segundo plano em relação aos outros paradigmas. E talvez até seja...
Já perdi as esperanças que o self redundante se vá e sei os motivos, mas ele ainda me incomoda, deixando uma estranha impressão de estar digitando algo fútil que está ali por pura teimosia e, de qualquer forma, é uma pena que não tenha como consertar isso.

Pedro Werneck disse...

Menos mal. É exatamente esse meu objetivo, que todo mundo que leia entenda porque está lá e não pode sair. Não quero convencer ninguém a gostar.

Eu discordo do Bruce Eckel em usar o termo "redundante", porque não vejo redundância alguma, e na verdade até gosto dele, talvez já pela minha maneira de abstrair o código Python. Eu não vejo funções, métodos e classes, quem conhece a idéia da linguagem que eu comecei a desenvolver talvez entenda que eu vejo tudo só como código e namespaces.

A implementação OO de Python não tem nada de meia-bomba, pelo contrário, é muito boa no que interessa. O problema é que como eu disse logo acima, muitos confundem "linguagem orientada a objetos" com "programação orientada a objetos", e Python não é a primeira.

Python é uma linguagem com *suporte* à programação orientada a objetos, e um suporte excelente, mas não é uma linguagem "pura" em nenhum sentido, porque essa não é a filosofia da linguagem. Todos os paradigmas têm seus prós e contras, e Python tenta pegar o melhor de cada, mas não dá pra ser perfeita.

Se alguém é tão purista, vai atrás de LISP ou Smalltalk... se quer produtividade, fique com Python.

Anônimo disse...

Até concordo com a explicação e tal (aliás, eu não sou o mesmo anônimo acima, só preguiça de criar conta pra postar mesmo :-)

O problema do "self" não é o "self", é apenas o fato do Python ser "considerada" uma linguagem OO (sim, ela é híbrida) e nenhuma outra linguagem OO do mercado, estática ou dinâmica, precisar de "self" explícito.

Ainda não vi nenhum caso de uso onde ter o "self" explícito ajude ordens de grandeza onde outras linguagens OO não resolvem. Sempre tem edge-cases claro, mas por causa de 0.001% de casos exceção eu preciso digitar o "self" explícito 99% do tempo?

Outra coisa, Python, assim como Ruby, Não se classifica como linguagem funcional. Nenhuma das duas, por exemplo, garante zero efeitos-colaterais. O exemplo acima com as variáveis de instância do Ruby não prova nada, ele não 'viola'. Nem mesmo Erlang poderia ser considerado 100% funcional. Apenas linguagens como Lisp e Haskell podem. Mas não é esse o ponto.

O ponto é: praticidade x preciosismo. Quando eu vi o titulo do blog eu imaginava: "bom, finalmente alguém vai explicar porque todas as outras linguagens OO não tem 'self' explícito e vai explicar o valor - ordens de grandeza maior - de me forçar a usar 'self' explícito o tempo todo". Sem essa resposta, o "self" continuará a ser um empecilho mais do que uma ajuda. Muita gente não se incomoda, mas o ponto é: se está lá, alguma razão pragmática tem que ter. Senão, não vejo erro na requisição do Bruce Eckel (por mais que ele tenha se expressado da maneira errada).

Marcos Douglas disse...

Perfeito!

Aquele que, após ler o texto, ainda pensar "não dá pra retirar o self?" é porque não quer entender a linguagem e sua filosofia.

Fico feliz em entrar para o mundo Python e conseguir raciocinar que classes são, na verdade, namespaces para funções e o "self" apenas trás o estado atual da "classe" pra dentro da função executada (como disse na lista python-brasil).

Ainda bem que não fiquei no purgatório por muito tempo, sofrendo com o self...

Obrigado pelo conhecimento.

Forte abraço

Pedro Werneck disse...

Ao outro anônimo,

Quem considera Python uma linguagem OO e usa a comparação com outras para apontar o self explícito como algo errado em Python está provavelmente apenas enganado, ou apenas preocupado com a sintaxe. Você pode dizer que não gosta, que odeia, que acha uma abominação, mas não que está errado.

Não disse que Python é linguagem funcional, muito menos Ruby. O exemplo acima com as variáveis de instância em funções de Ruby é para ilustrar como ela é de fato na maior parte OO e usa um açúcar sintático para simular funções com métodos, enquanto Python faz o contrário. A grande diferença é que o que Python faz também permite muitos outros recursos interessantes

Nenhuma das duas está certa ou errada. Alguém com herança de linguagem funcional provavelmente vai se sentir mais à vontade em Python. Como foi bem dito por alguns aqui, quem já programou OO em uma linguagem funcional não vê nada de errado com o self explícito, pelo contrário, é bem natural.

Realmente, nisso você está certo, o ponto é praticidade x preciosismo, mas essa obsessão em querer eliminar o self explícito também é um preciosismo em relação à sintaxe, que não leva em conta a complicação que isso envolve.

Simplesmente não há argumento contrário que supere todos os outros que há a favor. Essa discussão é essa coisa interminável porque é fácil para qualquer recém chegado entender o argumento contrário, digitar mais 4 caracteres, mas é difícil entender os argumentos a favor, que envolvem conhecimento da linguagem, do compilador e da natureza geral da filosofia e evolução dela.

A essa altura a razão pragmática do 'self' já está mais do que evidente. Se você ainda não viu... simplesmente não há mais nada a fazer.

Quanto ao Bruce Eckel, eu conhecia os argumentos e tive a oportunidade de falar do assunto pessoalmente com ele na Pycon. Não somente não fui convencido, como pelo que soube, ele é que foi embora do Brasil convencido pelo Ramalho de que o self explícito no acesso aos atributos não é redundante como ele afirmava... quem sabe se ele tivesse ficado mais tempo. :)

Anônimo disse...

Então, cuidado, concordo com você de novo: é errado um novato chegar no Python e dizer que o "self" está errado. Mas o "preciosismo" é: já que tem o "self" explícito, o que dá para fazer com ela que não se consegue fazer em nenhuma outra linguagem? O ponto é: só o Python tem self explícito, então alguma utilidade muito, mas muito diferente, ele tem que ter. Senão é de fato apenas mais 4 letrinhas à toa em cada método. Quem é experiente em Python diz "quem reclama de self não entende para que ele serve". Então: para que ele serve? (digo: o que de tão diferente dá para fazer com esse self explicito que eu não faço em qualquer outra linguagem OO normal? Seja C#, seja Java, seja Ruby, seja Object Pascal?

Pedro Werneck disse...

Anônimo,

Não leve a mal se soar um pouco rude, mas acabo de voltar meio cansado de um ótimo encontro com amigos programadores e depois de muita conversa produtiva (e improdutiva mas divertida), e uma potencial boa noite de sono antes de um dia de trabalho e um final de semana que promete ser divertido, é difícil ler o que você escreveu e não achar que é trollagem.

Dizer que não serve para nada ou perguntar para que o self serve e comparar com C#, Java, Ruby é a mesma coisa que dirigir um Porsche e perguntar para que serve o consumo maior do que um Gol 1000, ou atirar com um .357 Magnum e perguntar porque tem um recuo maior do que um .38. Você só pergunta se não sabe do que os dois são capazes . Se você ainda não entendeu, o self explícito não está lá porque queremos. Ele está lá porque é um efeito colateral.

Seus argumentos são claramente de alguém que não tem muita experiência em Python, então se você quer saber que utilidades Python tem graças a essas decisões que resultam nisso, então use Python o suficiente para entender e apreciar o dinamismo da linguagem e precisar resolver problemas sérios que o exijam, porque eu tenho maneiras melhores de investir meu tempo do que ficar aqui escrevendo foos e bars para te mostrar o que dá pra fazer.

Se o número de letras que você tem que digitar é o que te faz decidir se uma linguagem é a melhor ou não para algo e é o que te fez impedir de se aprofundar em Python até hoje, sinto muito. Se seu problema é esse mesmo, use isso aqui: http://www.pythonbrasil.com.br/moin.cgi/NoSelf

E por favor, eu nunca esperava ver sinais de flamewar surgindo por aqui, então se for continuar postando comentários, não o faça mais como anônimo, ok?