“Pegadinha”: Cuidado ao encadear múltiplas chamadas de métodos

Num dos testes antigos do PySide, havia uma inocente linha de código com QFile().metaObject().methodCount(), que na nova versão estava causando uma falha de segmentação dentro da Qt. O que estava acontecendo era que o QMetaObject retornado pelo metaObject() estava sendo apagado pelo QFile() criado, invalidando a área de memória que methodCount() tentava acessar. Agora por que diabos ele estava sendo deletado, já que eu chamava o método direto nele? A resposta está no modo como o CPython é implementado, sendo uma máquina virtual de pilha.

Usando o módulo dis nessa linha, temos o seguinte resultado:

0 LOAD_GLOBAL              0 (QFile)
3 CALL_FUNCTION            0
6 LOAD_ATTR                1 (metaObject)
9 CALL_FUNCTION            0
12 LOAD_ATTR                2 (foo)
15 CALL_FUNCTION            0
18 POP_TOP
19 LOAD_CONST               0 (None)
22 RETURN_VALUE

Dissecando instrução por instrução e seus efeitos na pilha, vamos assumir que esteja inicialmente vazia. Apenas as 4 primeiras instrução são necessárias:

  • LOAD_GLOBAL (QFile) – Topo da pilha é a classe QFile
  • CALL_FUNCTION – Remove QFile do topo e coloca o resultado da chamada, no caso, a nova instância de QFile, com refcount 1
  • LOAD_ATTR(metaObject) – Remove a instância de QFile do topo (decrementa o refcount) e coloca o resultado de getattr(instância, ‘metaObject’) no topo. Nesse caso, o resultado é um “bound method” A chamada a getattr incrementa a referência da instância de QFile, logo ela não morre.
  • CALL_FUNCTION – Remove o metodo metaObject do topo e coloca o resultado, no caso a instância de QMetaObject retornada. Ao remover o método, a referência à instância de QFile é removida, chegando a 0. Então o destrutor do binding chama o destrutor de C++, que por sua vez deleta o objeto C++ do QMetaObject, invalidando o ponteiro usado pelo binding.

Ou seja, devido essas instruções, não se pode garantir que um objeto criado anonimamente numa chamada de metodo e usado imediatamente irá estar “vivo” em chamadas subsequentes.

Vale notar que esse problema aparece em outras implementações de Python baseadas no CPython, como o Stackless e o Unladen Swallow. Implementações que usam outros tipos de máquina virtual como o Jython, IronPython e Pypy não sofrem desse problema.

Lambdas em laços for

Duas vezes num espaço de pouco mais de poucas semana me deparei com duas situações em que o programador era vítima de uma característica pouco conhecida da instrução lambda de python.

Ambas as situações envolviam usar lambda dentro de um laço for para criar funções dinamicamente de acordo com o valor fornecido pelo for naquela iteração. Seria algo mais ou menos assim:

for x in range(10):
algo_assincrono.connect(lambda : foobar(x))

No caso, o programador queria que a funcao foobar fosse chamada para cada x gerado no for, enquanto que na prática a função é sempre chamada com o último valor atribuído a x. Isso acontece porque a instrução lambda de python não é exatamente igual às outras lambdas de outras linguagens.

Em python, lambda é apenas uma mandeira simples de encapsular uma expressão dentro de uma função anônima, sem resolver os nomes das variáveis em tempo de definição. Ou seja, lambdas de python não possuem um escopo (espaço de nomes) próprio, trabalhando no escopo em que foram definidas. E como o laço for também não cria um espaço de nomes próprio, a variável x no caso acima estará com o último valor atribuído a ela até o momento da execução do lambda.

Uma alternativa para esse problema é usar aplicação parcial de funções, disponível como a função partial no módulo functools. Ela trabalha recebendo como primeiro parâmetro a função alvo e em seguida os argumentos que devem ser aplicados parcialmente, retornando uma função que receberá apenas os argumentos restantes. Por exemplo:

>>> def foo(x,y,z):
... return x+y+z
...
>>> foo(3)
Traceback (most recent call last):
File "", line 1, in
TypeError: foo() takes exactly 3 arguments (1 given)
>>> from functools import partial
>>> bar = partial(foo, 3)
>>> bar(4,5)
12
>>> bar(4)
Traceback (most recent call last):
File "", line 1, in
TypeError: foo() takes exactly 3 arguments (2 given)
>>> bar(0,0)
3
>>>

Voltando para o exemplo inicial, utilizando partial, ficaria assim:
for x in range(10):
algo_assincrono.connect(partial(foobar, x))

Zen do Python na prática

– Como é o jeito padrão de implementar árvore em python?

– Rapaz, até onde eu sei cada um implementa do jeito que precisa.

–  Sim, mas não tem um jeito “padrão”?

– Hum, acho que não.

– Então eu vou ter que implementar essa árvore e disponibilizar p/ cara p/ ele herdar dela e…

– Porque você precisa de uma árvore?

– Ah, eu tenho plugins e eles precisam retornar os menus e submenus na forma de uma árvore.

– Por que não diz ao cara apenas que ele precisa ter um atributo ‘children’ com uma lista dos filhos que por sua vez podem ter também o atributo children e assim até chegar na folha da ‘árvore’. Dessa forma ele fica livre para implementar do jeito que quiser.

– É, boa idéia…

É mais fácil pedir desculpa que pedir permissão

Raycife: Agora com sombras

Apesar de ter aumentado consideravelmente a granularidade (tá parecendo coisa de z-buffer ou algo do tipo), corrigi um bug besta pelo qual as sombras não estavam aparecendo: O raio de luz estava com a direção invertida (luz – ponto_de_verificação, e não ponto_de_verificação – luz)…

Abaixo, o resultado: 100×100 pixels e 100 raios/pixel (26 minutos, mas usando o computador enquanto processava):

Próximos passos: resolver essa bronca de granularidade e adicionar reflexão especular.

Status do projeto: raycife+

First full window of raycife

Esse é o estado atual, apenas com iluminação ambiente e a componente difusa (e um belo bug de ordenação). A imagem de referência pode ser encontrada no fim dessa página.

Dados: Janela 200×200 com 20 raios por pixel.

Tempo de processamento num Celeron 440 com 1 GB de RAM: +-15 minutos (muito lento…)

Profiling python com cprofile

Durante o desenvolvimento do raycife+, a performance estava ficando impraticável, chegando a cerca de 3 a 4 minutos para uma cena simples de 100×100 pixels e 7 objetos no meu velho celeron. Seguindo as dicas desse site, rodei o cprofile e pude ver onde poderia ganhar tempo.

No arquivo principal do programa esse código que executa a função main com os argumentos fornecidos na string e salva o resultado num arquivo de log.

if __name__ == '__main__':
    import cProfile
    cProfile.run('main(["raycifeplus.py", "./samples/cornellroomsmall.sdl"])' filename='raycife.cprof')

Para ler o resultado do arquivo de saída:

from pstats import Stats
stats = Stats("raycife.cprof")
stats.sort_stats('time').print_stats()

Exemplo da saída:

Mon Apr 21 11:23:35 2008    raycife.cprof

37298787 function calls in 237.746 CPU seconds

Ordered by: internal time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
1  168.955  168.955  237.734  237.734 ./raycifeplus.py:71(render)
26660558   46.400    0.000   46.409    0.000 /home/lauro/dev/raytracer/raycifeplus/gameobjects/vector3.py:165(__iter__)
5723042    9.791    0.000    9.807    0.000 /home/lauro/dev/raytracer/raycifeplus/gameobjects/vector3.py:265(__sub__)
2422914    4.633    0.000    4.636    0.000 /home/lauro/dev/raytracer/raycifeplus/gameobjects/vector3.py:369(__rmul__)
2422914    4.114    0.000    4.125    0.000 /home/lauro/dev/raytracer/raycifeplus/gameobjects/vector3.py:224(__add__)
10000    2.927    0.000    4.841    0.000 ./raycifeplus.py:80(render_pixel)
1472    0.478    0.000    0.916    0.001 ./raycifeplus.py:99(trace_point)
841    0.129    0.000    0.299    0.000 /home/lauro/dev/raytracer/raycifeplus/objfile.py:30(intersect)
1    0.119    0.119    0.182    0.182 /home/lauro/dev/raytracer/raycifeplus/pnm.py:18(write)
10000    0.044    0.000    0.044    0.000 /home/lauro/dev/raytracer/raycifeplus/pnm.py:15(set_pixel)

Nesse caso, deu para ver que parte do tempo era perdido no módulo vector3, uma implementação de vetores em 3 dimensões usando python.

No próximo vou falar sobre uma tentativa rápida de implementar a mesma funcionalidade usando C++ e criando o binding usando o SIP.

Nota: O código acima ainda poderia ser refinado para receber argumentos na linha de comando mas como é apenas um teste rápido, pode ficar p/ depois uma flag “-p” para habilitar o profiling.

Nota2: Sim, eu sei que a otimização prematura é a raíz de todo mal mas nesse caso é um teste pontual que não vai mudar o algoritmo, além de poder conhecer novas tecnologias 🙂

‘Use’ o Google no seu site (com Python!)

Pelo menos é o que dá a entender o conceito do AppEngine.

Você desenvolve usando Python (Vem com django instalado, apesar de algumas mudanças \o/ ), manda o código e lá é hospedado e servido pela infra estrutura do Google. O pacote gratuito dá direito a 500MB de espaço e cpu/tráfego para +- 5 milhões de page views. Há suporte para acesso aos serviços de dados (Datastore/BigTable) e contas (Google Accounts).

Numpy + PIL para processamento de imagens

Disparado, as melhores ferramentas que usei para uma disciplina na faculdade até agora.

Numpy para manipulação de matrizes e a PIL para abrir/salvar a imagem e pegar as informações. Alguns métodos já prontos não podem ser utilizados (afinal, é p/ aprender a implementar na mão) mas ainda assim é tudo bem direto e rápido.

Basicamente o trabalho se resume a pegar uma imagem e aplicar algumas transformações pontuais, espaciais e no domínio da frequência (Fourier, sempre ele…). Assim que entregar a versão final coloco o código online.

Análise do curso de Python p/ turma de design

Há algumas semanas eu e Marcelo fomos apresentar um mini-curso de python/pymaemo para uma turma de design no CAC/UFPE, representando o INdT.

O planejado era apresentar a linguagem python e em seguida como desenvolver programas usando pygame no maemo. Porém, visto que eles não tinham pouca ou nenhuma experiência prévia em programação, a opção ficou em aproveitar os dois dias para praticar bastante python e em outra oportunidade partir para algo mais específico

No primeiro dia, após uns problemas de infra-estrutura (murphy, sempre ele), demos uma passada rápida pelo que é python e fomos p/ laboratório instalar e abrir o IDLE para a parte prática. Durante esse dia, apresentando o conceito de função, um deles soltou “Como assim, função?”. O conceito já estava tão enraizado que acabou ficando meio complicado explicar, mas depois de um tempo, usando como analogia bater claras em neve numa receita, todo mundo conseguiu entender, acho 🙂

No segundo dia (acho que tinha metade da turma), a estratégia foi diferente: Eles iriam ficar o tempo todo no computador, bastante exercícios e seguir o ritmo deles, mas sempre tentando forçar um pouco mais. E foi exatamente assim que eles (ou pelo menos os que estavam no computador) conseguiram aprender mais, vendo os próprios erros e consertando.

Mas o diferencial na segunda parte foi mostrar um pouco do que dá p/ fazer com o pygame, em especial com um mini-wrapper que criei numa madrugada (portanto, muito incompleto ainda) especificamente para esse curso: o simplegame. Ao ver algo do domínio deles (jogos) que a coisa ficou bem mais clara e até o humor deles mudou (acordaram).

Num post futuro eu falo mais sobre o simplegame. Por enquanto vale dizer que é um wrapper que abstrai (ou tenta abstrair?) a parte chata de loop, tratamento de eventos, colisões e outras coisas relativa a jogos simples.

Resumindo:
• Cuidado com coisas que você acha óbvias. Você pode ter que explicar elas para alguém que não acha isso óbvio.
• Programação não se aprende com livros, curso, se aprende fazendo. E a longo prazo
• Eles podem surpreender. Eu tinha feito um código simples de umas 7 linhas como resolução de um exercício e ir aprimorando e os caras já mandaram a versão de 2 linhas de cara…

Ah, me falaram desse curso depois desse post. Parece que já tava prevendo…

Experiência: Python para pessoas de ciências sociais/humanas

Hoje estava conversando com minha namorada e ela perguntou se eu tinha alguma idéia de curso ou hobby para ela. Brincando falei sobre aprender a programar e para minha surpresa, ela gostou da idéia.

Então, o desafio é apresentar Python (creio que seja a melhor linguagem para a tarefa) a alguém sem nenhuma experiência na área de exatas. Por enquanto, estou pensando em tentar seguir um pouco da linha do Aprendendo Python (Muito bom por sinal). Alguma sugestão?