Poor man’s profiler - decorators em Python.
Dizem que o código do editor-chefe é tão rápido e eficiente que ele executa loop infinito em dois segundos. Talvez seja verdade, talvez seja uma lenda. Mas o que importa é que nós, pobres mortais que não alcançamos o nível Jedi dele, precisamos muitas vezes ter um mecanismo que nos permita verificar quanto tempo uma função está gastando para ser executada.
A maneira óbvia de se fazer isso é adicionando timestamps antes e depois de cada ponto do caminho crítico da sua aplicação. Como você não quer ser visto como um mero Padwan pelo nosso editor-chefe, você vai arrumar outra forma de fazer isso. Como?
Uma sugestão: fazer uma função “cronômetro” que aceitasse uma função como parâmetro. Essa função cronômetro pegaria timestamps antes e depois de executar a função que foi passada como argumento, e retornaria a diferença dos tempos. Isso ainda ficaria feio. Você teria que mexer no código do mesmo jeito em todos os pontos que precisa verificar a sua função, sem contar que você está deixando o fluxo do seu código confuso, substituindo a função que realmente faz o que você quer que faça por uma outra, que faz o que você deseja indiretamente. O seu código deixaria de expressar qual é a sua verdadeira intenção de execução.
Se você fizer isso, você dá espaço para “The-one-who-can’t-be-named” entrar com uma homérica defesa de sua linguagem/plataforma, enviando 32 links de sistemas enterprise que fazem profiling direto na VM, com um singelo acréscimo de 98MB de RAM na execução do processo. Nada demais para alguém que precisa ter um computador com 2GB de RAM apenas para rodar a IDE de forma satisfatória, não é mesmo?
Mas não é o caso. Podemos fazer um profiler de forma rápida e digna, e ainda aproveitar para fazer piadinha dos programadores que usam linguagens de programação que não possuem funções como first-class citizens. Elegante que é, Python tem uma solução para isso: decorators.
Pense em decorators quando você quiser que alterar uma função sem que você deixe de expressar o real objetivo da função ou método. A nossa função cronômetro é um exemplo óbvio. Nós não queremos interferir na funcionalidade da função que vai entrar como parâmetro, mas precisamos alterar o seu comportamento de alguma forma. Assim, definimos uma função cronomêtro:
def timeit():
'''Poor man's profiler'''
def decorator(func):
def proxyfunc(self, *args, **kw):
import datetime
before = datetime.datetime.now()
res = func(self, *args, **kw)
print('%s took %s' % (func.__name__, (datetime.datetime.now() - before)))
return res
return proxyfunc
return decorator
Agora, temos uma função que executa uma outra função e imprimindo o seu nome e tempo que levou para a execução.
E, caso você esteja desconfiado que a função “foo” esteja levando tempo demais na execução, você não precisa mais alterar todos os pontos de código. Basta adicionar o decorator na declaração:
@timeit()
def foo(a, b, c):
'''Função Demoraaaaaada'''
# código
Claro que esse é um exemplo simples. Mas já pode ser usado para resolver muitos problemas. Enfim, o recurso é poderoso, mas não abuse. Há quem aprenda decorators e queira transformar todas as suas funções em decorators, o que acaba prejudicando mais do que ajudando. E procure explicações melhores do que a minha, como essa aqui. Eu só escrevi esse post para o editor-chefe ver que eu falo de programação de vez em quando…

O único problema que eu vejo nessa solução eh que você teve que colocar o @timeit() na sua função foo. Imagina se você quisesse fazer isso em varias funções, teria que anotar todas. E depois, pra entregar o produto final, teria que sair tirando as anotações.
Pra esse tipo de coisa que existe AOP (aspect oriented programming). Nesse seu exemplo voce teria 1) a definicao da funcao timeit 2) as funcoes em que voce quer aplicar timeit 3) a “cola” grudando as duas coisas, sem ter que mexer em 1 ou 2.
Se fosse java (aspectJ), voce escreveria algo como (isso aqui tah beeem estilizado) :
around() : call( foo(..) ) && args(a) {
timer.start();
try {
return proceed(a);
} finally {
timer.stop();
}
}
Eu nao programo nada em python, mas vi agora que tem uma biblioteca que simula isso, provavelmente usando decorators ou wrappers ( http://www.cs.tut.fi/~ask/aspects/aspects.html ).
Bart, eu concordo com você que você vai ser obrigado a ficar mexendo nas funções se quiser tirar e remover um decorator.
Mas não vejo um ganho em fazer usando o esquema que você falou. Qual é a diferença em ter que manipular as funções que você vai mudar diretamente e fazer a manipulação no seu arquivo de “cola”?
Tudo bem que o seu esquema provê uma camada de indireção, e que isso pode ser desejado, mas um dos mantras dos pythonistas é justamente remover indireção desnecessária: “explicit is better than implicit”.
Abraços.
Bart,
eu mexi um pouco indiretamente com AOP. Devo dizer que tem algo que me incomoda, mas acho que é simplesmente o lance da nomenclatura deles, me parece pomposa e meio complexa demais. Mas o conceito é interessante.
No caso de python, isso é realmente bem simples de fazer, inclusive usando a mesma estrutura descrita pelo Raphael. É importante notar que decorators em python são apenas um atalho (ou um açucar sintático…) para algo muito simples.
Quando eu faço
@timeit
def foo()
Na verdade eu to fazendo o seguinte: foo = timeit(foo).
A função timeit cria uma nova função, que marca o tempo e executa a foo, e eu defino que foo passa a ser a nova função. Maravilhas do de ter first class functions.
Tendo isso em mão, e sabendo que fazer instrospecção em python é mais simples do que pedir para que os colaboradores deste blog escrevam textos regularmente, você pode muito bem criar um arquivo separado (que podemos chamar de aop.py), que pegue todas as funções que você deseja cronometrar e faça a atribuição acima logo na inicialização do sistema.
Problema resolvido!
Raphael,
Achei bem interessante este uso para o decorators de python, mas realmente o uso deles não escala para quando você quer medições de diversas classes e/ou módulos de um sistema.
Os pointcuts de AOP expressam pontos onde o código pode ser inserido de uma maneira muito, mais muito expressiva e mais importante, sem tocar nos fontes do código que será interceptado. O resultado final é muito mais limpo e muito mais fácil de manter.
AOP é realmente muito superior em termos de simplicidade de escrever o que você quer; a lógica que será inserida fica centralizada um único “aspecto”; e com mecanismos para escolher os pontos de interceptação que ficam isolados do código interceptado.
Para coisas mais simples e pontuais o hacking a lá “python @decorators” dão conta, mas para atuar em diversas classes/funções de um sistema de médio porte para cima, AOP certamente é a melhor opção, quando disponível.
Miguel,
Isto não é tão simples ou elegante como seria com AOP.
ps. AOP também tem seus problemas, eu sei
(a depuração pode ficar comprometida pela manipulação de byte codes, etc)
Raphael,
a grande vantagem da proposta feita pelo Bart é que o código que sofre a medição do tempo não sabe que existe um agente externo medido quanto tempo ocorreu a sua execução. Ou seja, não há acoplamento nenhum entre o código medido e o código que mede.
Isto faz mto sentido, pois a “medição do tempo de execução” é uma preocupação ortogonal, não faz parte do domínio de negócio implementado. É uma lógica que entrecorta inúmeras classes do sistema.
Por isto a utilização de AOP, que determina quais os candidatos que devem sofrer a ação aspectual, qual a lógica adicional que deve ser executada e quando isto deve ocorrer, de forma isolada e desacoplada.
Este quando deve ocorrer me permite especificar antes, depois ou durante a execução de um método, além de existirem formas de AOP, como a dinâmica, em que modificações de comportamento ocorrem on the fly. É um paradigma de programação mto interessante, que complementa o paradigma OO.
Acredito que, por isto que enumerei, AOP tenha um ganho maior que os decorators, em termos de desacoplamento e flexibilidade.
Btw, no mundo pythoniano, achei essa idéia bem interessante…
Elegante, talvez até concorde. Se bem que tudo depende o que é elegancia..em python manipular funçoes é extremamente elegante.
Quanto à simplicidade, não sei quano simples é definir point cuts, mas faze ro que eu falei não é simples, é ridiculamente simples. Trivial. Besta. E você não mexe nos fontes do sistema, centraliza tudo em um só ponto, e pode ativar e desativar com apenas um comando.
Se isto não é elegante, não sei o que é.
Eu acho que ter funções como parametros e valor de retorno é apenas um mecanismo. O uso dele em um determinado contexto é que pode ser elegante ou não.
Isto eu imagino que seja trivial para funções ’soltas’ mesmo, mas e para métodos de classes? Esta sua técnica do arquivo aop.py se aplica? (neste caso obviamente não é possivel usar a notação @).
Se aplica sem problema algum. E inclusive, eu posso usar sim a notação @.
Como anotar um método de uma classe definida em um arquivo a.py em um outro arquivo aop.py?
class A:
def method(self, param):
print "Ola mundo ", param
def wrap(func):
def decorator(*args, **kargs):
print "antes da exec"
func(*args, **kargs)
print "depois da exec"
return decorator
a = A()
print “Executando method em instancia a”
a.method(1)
a.method = wrap(a.method)
print “\nExecutando method em a wrapeado (a.method = wrap(a.method))”
a.method(3)
b = A()
print “\nExecutando method em instancia b. Note que esta instancia nao tem wrapper”
b.method(2)
print “\nExecutando em a novamente…”
a.method(6)
A.method = wrap(A.method)
print “\nAgora vou aplicar o wrapper na instancia. Deve mexer em a e b (A.method = wrap(A.method))”
print “EM A:”
a.method(8)
print “EM B:”
b.method(10)
Você acha isto elegante?? Já viu como ficaria a versão AOP?
eu fiz o codigo para responder à sua pergunta de como fazer decorator para metodos de classes. Não fui atra´s de fazer algo bonito, elegante. APenas funcional. E outra: usar uma lib de AOP esconde todo o codigo que o AOP faz por baixo dos panos. Isso não é uma lib. São poucas linhas de python que resolvem o problema. E como diz o titulo: Poor man’s profiler. A ideia não é ser completo, enterprise. É resolver um problema de forma simples e direta, caso precise.
Oks, feito ou “não elegante” é exagero.
Mas com wildcards para os pointcuts em AOP você consegue aplicar interceptadores em muito mais métodos, escrevendo muito menos código.
Com este approach em python muito mais código deveria ser escrito, daria muito mais trabalho.
Agora se você quer interceptar poucos métodos então com esta maneira, utilizando decorators, é suficiente.
Bruno, por favor se puder poste aqui o código que permite que você use wildcards e afins. Note que se eu quiser implementar wildcards em python, é extremamente simples. Basta qu eeu crie uma funçao que receba um nome com estes caras, parseie os metodos e funçoes de um modulo ou classe e pronto.
Mas bele, volto ao pedido anterior: poste aqui o código completo de uma lib de AOP, com todas as features…vamos ver a quantidade de código que isso requer.
Se o que eu quero é usar tudo que AOP me oferece, importar uma lib é o de menos.
Ou você vai parar de falar bem do Django porque usar ele envolve importar dezenas de módulos em python?
Não faz o menor sentido falar de ter que importar uma lib ou não neste caso, mesmo porque nenhum projeto de verdade não utiliza lib alguma, ou você não utiliza nem o SDK do Python?
E eu deixei bem claro que para usos pontuais a solução acima é muito boa, apeanas não escala para interceptar um grande número de métodos em um número maior de classes, pelo menos não tanto quanto a syntaxe de AOP permite.
Assim como o bom e velho print de timestamps antes e depois dos métodos resolve bem o problema para investigações pontuais e de pequeno alcance.
Quanto ao código AOP eu posso postar aqui mais tarde sim, coisa que o Rafael Naufal deve fazer de cabeça em 1 minuto (dado o paper que ele está escrevendo, hehe).
Esse talvez não seja o melhor exemplo de uso de AOP pois não tem muita quantificação : o uso de wildcards não eh necessário se tudo o que você quer eh interceptar exatamente um método. Nesse caso ateh simples OO funcionaria (estende-se a classe, chama-se super() entre as chamadas dos timers e muda a factory pra criar o novo tipo) ou melhor ainda com mixins.
Mas vamos lah, vou escrever aqui de cabeça um codigo equivalente ao do miguel mas em CaesarJ (note que em AspectJ não dah pra fazer esses deploys automáticos sem ter que fazer algum codigo adicional).
cclass A {
void method(String param) {
System.out.println(”Ola mundo ” param);
}
}
cclass Timer {
void around(Object a) : execution(void A.method(String)) && args(a) {
System.out.println(”antes da exec”);
proceed(a);
System.out.println(”depois da exec”);
}
}
A a = new A();
System.out.print(“Executando method em instancia a”);
a.method(1);
Timer timer = new Timer();
DeploySupport.deployOnObject(timer, a);
System.out.println(“\nExecutando method em a wrapeado (a.method = wrap(a.method))”);
a.method(3);
A b = new A();
System.out.print(“\nExecutando method em instancia b. Note que esta instancia nao tem wrapper”);
b.method(2);
System.out.println(“\nExecutando em a novamente…”);
a.method(6);
deploy timer; // deploy em todas as instancias
System.out.print(“\nAgora vou aplicar o wrapper na instancia. Deve mexer em a e b (A.method = wrap(A.method))”);
System.out.print(“EM A:”);
a.method(8);
System.out.print(“EM B:”);
b.method(10);
Acho que dah pra entender a ideia, nao?
Agora vamos supor que eu quisesse medir a execucao de todos os metodos chamados run da aplicacao. Eu mudaria o meu pointcut pra:
around() : execution(void *.run(..)) && args(a)
Note que eu estou forcando serem methodos run de qualquer classe, que retornam void e tem qualquer numero de argumentos. Outra coisa que se faz eh separar o “onde/quando” do “o que” com sub-aspectos. Entao eu teria:
abstract cclass Timer {
abstract pointcut timedMethods();
void around(Object a) : timedMethods() && args(a) {
System.out.println(”antes da exec”);
proceed(a);
System.out.println(”depois da exec”);
}
}
public cclass RunTimer extends Timer {
pointcut timedMethods() : execution(void *.run(..));
}
Vale lembrar que tem bastante pesquisa sobre a manutenção desse tipo de código, jah que wildcards são facilmente quebrados e são baseados em convenções de nomenclatura de métodos/classes (como get* pra getters, etc). Ateh onde se sabe, o código eh tao bom ou melhor do que Java (pra manutenção).
AOP fornece bons constructos para lidar com preocupações ortogonais. Em AspectJ, temos:
before() : call(* Conta.*(..)) {
logger.log("Logar todos os métodos da classe Conta antes de sua execução");
}
O wildcard
* Conta.*(..)define que qualquer método da classe Conta (inclusive os privados), com qualquer número e tipo de argumentos deve ser interceptado.O mesmo pode ser utilizado para os adendos after(depois) e around (durante) a execução de um método.
after() : call(public void Conta.debitar*(double)) {
logger.log("Logar todos os métodos públicos da classe Conta depois de sua execução");
}
A última construção, utilizando wildcards, faz um match com métodos que iniciem com debitar da classe Conta e que aceitem um único argumento, do tipo double.
Mtas construções ainda podem ser utilizadas, como interceptar construtores, tratamento de exceções, etc..