Friday, June 1, 2012

Bite-sized Twisted: Introduction and the Reactor

Why I'm writing this

Twisted has enabled me to do some amazing things -- things I hadn't thought possible. Others haven't had the same delightful experience. Their descriptions of Twisted include less glowing adjectives. My intent with this and related posts is to ameliorate some of those adjectives.

I'm borrowing conceptually from JP Calderone's excellent Twisted Web in 60 Seconds series by keeping these posts relatively short. Also, there's KRONDO blog's Twisted Introduction, another great series for learning Twisted (and general Asynchronous Programming).

Bomberman

Bomberman is great! So, let's make it. By the end (or maybe somewhere in the middle) of these Bite-sized Twisted posts, I'm hoping that some semblance of a Bomberman clone emerges. That's the goal, at least.

Also, am I the only one who thought Super Smash Bros should have included Bomberman? And MegaMan?

The Reactor

The reactor is central to Twisted. You may not always interact with it directly, but it'll almost always be there, reacting away. To start the reactor, call its run method:

from twisted.internet import reactor
reactor.run()
snippet1.py

Run this code and you'll see it hang:


Nothing != Something

You might be tempted to say, "nothing happened." Don't say that. Something happened; in fact it's still happening, and will continue to happen until you stop it (press Control-C). The something happening is the reactor running. To prove it, run this code:

from twisted.internet import reactor
print 'Hello,'
reactor.run()
print 'world!'
snippet2.py

You should see the Hello, but won't see the world! until you stop the reactor:

Hello,
^Cworld!

(The ^C is what my terminal prints when I press Control-C)

What does the reactor do?

The reactor reacts to events. One kind of event it can react to is the passage of time (scheduled events):

from twisted.internet import reactor
reactor.callLater(2, reactor.stop)
reactor.run()
snippet3.py

snippet3.py tells the reactor to call reactor.stop after 2 seconds, then it starts the reactor with reactor.run(). After 2 seconds, reactor.stop is called, stopping the reactor, and the process exits.

The first argument to reactor.callLater is the number of seconds to wait before calling the function. The second argument is the function to call. It's important that it's reactor.stop, not reactor.stop(). reactor.callLater expects the reference to the function, not the result of the function. Passing function references is a common idiom in the Twisted API.

Also, you can read the callLater docs for yourself. (See twisted.internet.interfaces.IReactorTime.callLater as it says)

Make a bomb

If we're going to make Bomberman, we'll need bombs. Bombs have fuses. Let's light some fuses:

from twisted.internet import reactor

def explode(message):
print 'BOOM! ' + message

reactor.callLater(1, explode, 'shakalaka')
reactor.callLater(2, explode, 'I say')
reactor.callLater(3, reactor.stop)
reactor.run()
bomb.py

Run it. After 1 second, the first bomb explodes. After 1 more second, the second explodes. After 1 more, the process exits:

BOOM! shakalaka
BOOM! I say

In bomb.py, note two things:

First, when scheduling explode we also pass a string ('shakalaka') as an extra argument to reactor.callLater. This argument is eventually passed to explode as it's first argument. callLater takes any number of arguments and keyword arguments, which it will use to call the scheduled method.

Second, do the explosions happen when you expect them to? Consider this synchronous code, which has the same timed output as bomb.py:

import time

def explode(message):
print 'BOOM! ' + message

time.sleep(1)
explode('shakalaka')
time.sleep(1)
explode('I say')
time.sleep(1)
synchronous.py

In the synchronous code, since commands happen sequentially, you specify the timing offset from one command to the other. In the asynchronous code, scheduled commands are independent of each other: you specify the timing offset from the time that you schedule the command.

The Twisted code does the same thing as the synchronous code... so why should I use Twisted?

Though synchronous.py has the same output as bomb.py, it does not do the same thing. This will become more apparent when we start adding input/output to programs. Until then, consider how you would do this synchronously:

from twisted.internet import reactor

def explode(message):
print 'BOOM! ' + message

def funkyBomb(message):
reactor.callLater(0.25, explode, message + ' 1')
reactor.callLater(0.5, explode, message + ' 2')
reactor.callLater(0.9, explode, message + ' 3')
reactor.callLater(1.5, explode, message + ' 4')

reactor.callLater(1, funkyBomb, 'shakalaka')
reactor.callLater(1.5, funkyBomb, 'booyah')
reactor.callLater(2, funkyBomb, 'I say')
reactor.callLater(4, reactor.stop)
reactor.run()
bomb2.py
BOOM! shakalaka 1
BOOM! shakalaka 2
BOOM! booyah 1
BOOM! shakalaka 3
BOOM! booyah 2
BOOM! I say 1
BOOM! booyah 3
BOOM! I say 2
BOOM! shakalaka 4
BOOM! I say 3
BOOM! booyah 4
BOOM! I say 4

What do you think of that?!

No comments:

Post a Comment