Receita: Executando Funções em Intervalos de Tempo
Em fevereiro de 2004 surgiu na lista python-brasil uma pergunta sobre como se pode executar uma determinada função de intervalos em intervalos, como a função setInterval do JavaScript. Uma primeira solução utilizando apenas funções e timers foi proposta por Jonas Galvez, uma segunda foi proposta por mim (FelipeLessa) e uma outra terceira foi achada também por Jonas. Cada solução tem suas vantagens e desvantagens, veja cada uma e tire sua própria conclusão quanto preferências de implementação e aplicabilidade no seu código. Atualização (02/07/04): E agora temos uma quarta solução, usando a API do Twisted.
Conteúdo
Primeira solução -- usando funções
Descrição
Essa é a primeira solução proposta. Basicamente uma função geradora cria uma outra que possui dentro de si um Timer() que, ao invés de chamar a função que foi solicitada, chama a si própria, recriando o Timer() que foi destruído e executando a função. Uma outra função acessa um valor interno da função que está rodando o Timer() e a instrui para parar o loop.
Vantagens e desvantagens
A vantagem clara é a simplicidade do código e de como ele é mantido, porém essa solução tem um ponto fraco: os ciclos de CPU e os recursos de sistema que são gastos com a criação e a destruição contínua dos objetos Timer(). É claro que essa diferença pode não ser percebida em intervalos grandes como o dos exemplos abaixo, porém em intervalos de décimos ou centésimos de segundo essa pode ser uma grande diferença.
Código
1 from threading import Timer
2
3 def setInterval(function, interval, *params, **kwparams):
4 def setTimer(wrapper):
5 wrapper.timer = Timer(interval, wrapper)
6 wrapper.timer.start()
7
8 def wrapper():
9 function(*params, **kwparams)
10 setTimer(wrapper)
11
12 setTimer(wrapper)
13 return wrapper
14
15 def clearInterval(wrapper):
16 wrapper.timer.cancel()
Exemplo de uso
1 # Nos exemplos assumirei que o módulo "cookbook" contém todos os códigos
2 from cookbook import setInterval
3 from os.path import getsize
4
5 FILE = '/var/log/syslog'
6
7 def monitor(lastsize = [-1])
8 size = getsize(FILE)
9 if size <> lastsize[0]:
10 print 'O tamanho do arquivo agora é %d kilobytes (%d bytes)' % \
11 (round(size / 1024.0), size)
12 lastsize[0] = size
13
14 if __name__ == '__main__':
15 print 'Checando a cada um minuto o tamanho do arquivo "%s".' % FILE
16 print 'Atenção: esse programa NUNCA termina.'
17 interval_monitor = setInterval(monitor, 60)
18 while True:
19 pass
Segunda solução -- usando classes e sleep
Descrição
Essa é a segunda solução proposta por FelipeLessa também na lista python-brasil. Ao invés de um Timer() ser recriado constantemente, a função time.sleep() é utilizada dentro de um laço while, dessa forma evitando a contrução/destruição de objetos constante.
Para os curiosos, as únicas diferenças entre o exemplo dessa solução e da primeira estão nas linhas 17, 18 e 19.
Vantagens e desvantagens
Essa implementação praticamente não gasta qualquer tipo de recurso entre uma execução e outra, tendo entre elas apenas a função time.sleep() e a checagem do laço while, dessa forma tendo praticamente nenhuma sobrecarga mesmo em intervalos curtos.
A única desvantagem é visual: há quem não goste de utilizar classes para esse tipo de tarefa.
Código
1 from threading import Thread
2 from time import sleep
3
4 class IntervalRunner(Thread):
5 def __init__(self, interval, function, *args, **kwargs):
6 super(self, IntervalRunner).__init__(self)
7
8 self.interval = interval
9 self.function = function
10 self.args = args
11 self.kwargs = kwargs
12 self.executing = False
13
14 def run(self):
15 self.executing = True
16 while self.executing:
17 self.function(*self.args, **self.kwargs)
18 time.sleep(self.interval)
19
20 def stop(self):
21 self.executing = False
Exemplo de uso
1 # Nos exemplos assumirei que o módulo "cookbook" contém todos os códigos
2 from cookbook import IntervalRunner
3 from os.path import getsize
4
5 FILE = '/var/log/syslog'
6
7 def monitor(lastsize = [-1])
8 size = getsize(FILE)
9 if size <> lastsize[0]:
10 print 'O tamanho do arquivo agora é %d kilobytes (%d bytes)' % \
11 (round(size / 1024.0), size)
12 lastsize[0] = size
13
14 if __name__ == '__main__':
15 print 'Checando a cada um minuto o tamanho do arquivo "%s".' % FILE
16 print 'Atenção: esse programa NUNCA termina.'
17 interval_monitor = IntervalRunner(60, monitor)
18 interval_monitor.start()
19 interval_monitor.join() # Opcional, claro
Terceira solução -- usando classes e eventos
Descrição
Essa é a terceira solução proposta (e última, pelo menos enquanto eu escrevo ), originalmente postada no ASPN Python Cookbook. Para utilizá-la você precisa criar uma subclasse da mesma e o checador de atividade é implementado usando um objeto Event().
Vantagens & Desvantagens
Pode-se dizer que essa solução é uma irmã da segunda, filosofia igual e implementação diferente. Ao invés dela chamar uma função, ela já possui em si a função, o que significa que você deve criar uma subclasse. Essa é então a vantagem e a desvantagem. Se você precisa de um maior controle talvez goste dessa implementação, mas para tarefas simples é o mesmo que matar mosca com rifle de elefante.
Código
1 import threading
2
3 class TaskThread(threading.Thread):
4 """Thread that executes a task every N seconds"""
5
6 def __init__(self):
7 threading.Thread.__init__(self)
8 self._finished = threading.Event()
9 self._interval = 15.0
10
11 def setInterval(self, interval):
12 """Set the number of seconds we sleep between executing our task"""
13 self._interval = interval
14
15 def shutdown(self):
16 """Stop this thread"""
17 self._finished.set()
18
19 def run(self):
20 while 1:
21 if self._finished.isSet(): return
22 self.task()
23
24 # sleep for interval or until shutdown
25 self._finished.wait(self._interval)
26
27 def task(self):
28 """The task done by this thread - override in subclasses"""
29 pass
Exemplo de uso
1 # Nos exemplos assumirei que o módulo "cookbook" contém todos os códigos
2 from cookbook import TaskThread
3 from os.path import getsize
4
5 FILE = '/var/log/syslog'
6
7 class FileSizeTracker(TaskThread):
8 def __init__(self):
9 super(self, FileSizeTracker).__init__(self)
10 self._interval = 60.0 #segundos
11 self._lastsize = -1
12
13 def task(self):
14 size = getsize(FILE)
15 if size <> self._lastsize:
16 print 'O tamanho do arquivo agora é %d kilobytes (%d bytes)' % \
17 (round(size / 1024.0), size)
18 self._lastsize = size
19
20 if __name__ == '__main__':
21 print 'Checando a cada um minuto o tamanho do arquivo "%s".' % FILE
22 print 'Atenção: esse programa NUNCA termina.'
23 thread = FileSizeTracker()
24 thread.start()
25 thread.join() # Opcional, claro
Quarta solução -- usando o Twisted
Descrição
O Twisted Matrix é uma plataforma pra desenvolvimento de aplicações para a Internet. Obviamente não compensa usar uma biblioteca dessa magnitude simplesmente para executar uma função em determinados intervalos, porém, caso seu programa já utilize o Twisted, então dessa forma você vai poder continuar usando o loop principal do Twisted para todo seu programa. O exemplo é uma versão ligeiramente modificada da documentação do Twisted.
Devo lembrar que o Twisted não utiliza múltiplos threads ou processos, portanto não sofre dos problemas dos mesmos (a implementação do Python é a melhor possível).
Para maiores informações sobre a class LoopingCall do Twisted, veja a documentação referente.
Código
1 # Nenhum. Tudo já foi feito =).
Exemplo de uso
1 from twisted.internet import reactor, task
2 from os.path import getsize
3
4 FILE = '/var/log/syslog'
5
6 def monitor(lastsize = [-1])
7 size = getsize(FILE)
8 if size <> lastsize[0]:
9 print 'O tamanho do arquivo agora é %d kilobytes (%d bytes)' % \
10 (round(size / 1024.0), size)
11 lastsize[0] = size
12
13 if __name__ == '__main__'
14 print 'Checando a cada um minuto o tamanho do arquivo "%s".' % FILE
15 print 'Atenção: esse programa NUNCA termina.'
16 l = task.LoopingCall(monitor)
17 l.start(1.0)
18 reactor.run()
Conclusão Final
Basicamente pode-se criar o seguinte caminho de perguntas e respostas:
1) Você já usa o Twisted Matrix no seu programa?
- Se sim, prefira a quarta solução.
- Se não, vá para a próxima pergunta.
2) Você tem algo contra o uso de classes no código que usa o intervalo?
- Se sim, utilize a primeira solução e tenha a certeza de que não vai ter a performance alterada.
- Se não, vá para a pergunta 3.
3) Você precisa criar algum tipo de controle avançado da função que será executada ou precisa de outras funcionalidades que só podem ser obtidas usando uma classe (como multiple inheritance)?
- Se sim, utilize a terceira solução. Com ela você cria a sua própria subclasse e pode reutilizá-la quantas vezes quiser.
- Se não, utilize a segunda solução. Esta solução provê uma interface igual à primeira solução porém sem o gargalo que pode vir a existir nesta última.
Volta para CookBook.