I needed to link some page explaining what inlineCallbacks and
deferredGenerator are, and ... failed to find one. So here is a
short explanation and the example code (working scripts).
Code size comparison: raw deferreds - 29 lines,
deferredGenerator - 26 lines, inlineCallbacks - 17 lines (non-empty lines, lookup function measured).
inlineCallbacks and deferredGenerator are syntax glues
which make Twisted programs far easier to read and write.
Thanks to both, one can avoid creating the callback functions
explicite and handle exceptions in natural manner.
Below I wrote 3 versions of the simple (but working) script. Only the
lookup function body is different (and this function is expected to
make some HTTP queries and return deferred fired with the info
extracted from results).
Raw deferreds
from twisted.internet import reactor, defer
from twisted.web.client import getPage
import re
def lookup(country, search_term):
main_d = defer.Deferred()
def first_step():
query = "http://www.google.%s/search?q=%s" % (country,search_term)
d = getPage(query)
d.addCallback(second_step, country)
d.addErrback(failure, country)
def second_step(content, country):
m = re.search('<div id="?res.*?href="(?P<url>http://[^"]+)"',
content, re.DOTALL)
if not m:
main_d.callback(None)
return
url = m.group('url')
d = getPage(url)
d.addCallback(third_step, country, url)
d.addErrback(failure, country)
def third_step(content, country, url):
m = re.search("(.*?)", content)
if m:
title = m.group(1)
main_d.callback(dict(url = url, title = title))
else:
main_d.callback(dict(url=url, title="{not-specified}"))
def failure(e, country):
print ".%s FAILED: %s" % (country, str(e))
main_d.callback(None)
first_step()
return main_d
def printResult(result, country):
if result:
print ".%s result: %s (%s)" % (country, result['url'], result['title'])
else:
print ".%s result: nothing found" % country
def runme():
all = []
for country in ["com", "pl", "nonexistant"]:
d = lookup(country, "Twisted")
d.addCallback(printResult, country)
all.append(d)
defer.DeferredList(all).addCallback(lambda _: reactor.stop())
reactor.callLater(0, runme)
reactor.run()
Note: it is of course not necessary to pass country as callback
parameter, I left it as an example of how does this work.
Deferred generators
Note: Python 2.4 required
General idea:
@defer.deferredGenerator
def someFunction():
a = 1
x = defer.waitForDeferred(
deferredReturningFunction(a))
yield x
b = x.getResult()
x = defer.waitForDeferred(
anotherDeferredReturningFunction(a, b))
yield x
c = x.getResult()
yield c
Our script:
from twisted.internet import reactor, defer
from twisted.web.client import getPage
import re
@defer.deferredGenerator
def lookup(country, search_term):
try:
d = defer.waitForDeferred(
getPage("http://www.google.%s/search?q=%s" % (country,search_term)))
yield d
content = d.getResult()
m = re.search('<div id="?res.*?href="(?P<url>http://[^"]+)"',
content, re.DOTALL)
if not m:
yield None
return
url = m.group('url')
d = defer.waitForDeferred(
getPage(url))
yield d
content = d.getResult()
m = re.search("(.*?)", content)
if m:
title = m.group(1)
yield dict(url=url, title=title)
return
else:
yield dict(url=url, title="{not-specified}")
return
except Exception, e:
print ".%s FAILED: %s" % (country, str(e))
def printResult(result, country):
if result:
print ".%s result: %s (%s)" % (country, result['url'], result['title'])
else:
print ".%s result: nothing found" % country
def runme():
all = []
for country in ["com", "pl", "nonexistant"]:
d = lookup(country, "Twisted")
d.addCallback(printResult, country)
all.append(d)
defer.DeferredList(all).addCallback(lambda _: reactor.stop())
reactor.callLater(0, runme)
reactor.run()
Inline callbacks
Note: Python 2.5 required
General idea:
@defer.inlineCallbacks
def someFunction():
a = 1
b = yield deferredReturningFunction(a)
c = yield anotherDeferredReturningFunction(a, b)
defer.returnValue(c)
Our script:
from twisted.internet import reactor, defer
from twisted.web.client import getPage
import re
@defer.inlineCallbacks
def lookup(country, search_term):
try:
query = "http://www.google.%s/search?q=%s" % (country,search_term)
content = yield getPage(query)
m = re.search('<div id="?res.*?href="(?P<url>http://[^"]+)"',
content, re.DOTALL)
if not m:
defer.returnValue(None)
url = m.group('url')
content = yield getPage(url)
m = re.search("(.*?)", content)
if m:
defer.returnValue(dict(url=url, title=m.group(1)))
else:
defer.returnValue(dict(url=url, title="{not-specified}"))
except Exception, e:
print ".%s FAILED: %s" % (country, str(e))
def printResult(result, country):
if result:
print ".%s result: %s (%s)" % (country, result['url'], result['title'])
else:
print ".%s result: nothing found" % country
def runme():
all = []
for country in ["com", "pl", "nonexistant"]:
d = lookup(country, "Twisted")
d.addCallback(printResult, country)
all.append(d)
defer.DeferredList(all).addCallback(lambda _: reactor.stop())
reactor.callLater(0, runme)
reactor.run()
Additional remarks
After I posted this article, Jan-Paul Calderone (one of the Twisted core developers)
offered his, more idiomatic
version of the raw deferred example. Here it is (I fixed two small typos):
def lookup(country, search_term):
query = "http://www.google.%s/search?q=%s" % (country,search_term)
d = getPage(query)
def cbFirstPage(content, country):
m = re.search('<div id="?res.*?href="(?P<url>http://[^"]+)"',
content, re.DOTALL)
if not m:
return None
url = m.group('url')
d = getPage(url)
def cbSecondPage(content, country, url):
m = re.search("(.*?)", content)
if m:
title = m.group(1)
return dict(url = url, title = title)
else:
return dict(url=url, title="{not-specified}")
d.addCallback(cbSecondPage, country, url)
return d
d.addCallback(cbFirstPage, country)
def ebGeneric(err, country):
print ".%s FAILED: %s" % (country, str(err))
d.addErrback(ebGeneric, country)
return d
That is the best comparison of the three styles I have ever seen.
I have done a lot of programming with raw Deferreds, but have never looked seriously into using the newer tricks...
Do you know if there are any big overheads associated with the newer styles?
Thank you.
I have been using deferredGenerators a lot, and I never felt they induce any kind of overhead. Just starting inlineCallbacks (recently migrated to python 2.5) and feel the same. Well, my apps are network-bound...
There may be some tiny performance differences, but I'd find it very, very strange if they were noticeable in any real program.
Your best bet is to test ;-) (rewrite my scripts not to connect anywhere, iterate million times and measure)
Yeah, I'll have to do some tests and see how things work out.
Nice examples... thanks...
Hi I'm new to Twisted and as you noticed there is no information about diffrent approaches in documentation. Before I found your post I didn't know about inlineCallbacks. Your post helped me understand them, and I think they rocks! Thank You!
Very interesting examples. I love the concept of writing asynchronous code in a ... synchronous manner. Even though it only applies to simple cases, it makes the code really easier to read.
... and that's the main point. Writing the code using callbacks is only marginally harder, but the difference in reading is worth it.
+1 for JP Calderone implementation,
I think it is more consistent as the exception is treated with an errback, which is nicer than a big try an a catch on all exception.