1+ {
2+ "metadata" : {
3+ "name" : " "
4+ },
5+ "nbformat" : 3 ,
6+ "nbformat_minor" : 0 ,
7+ "worksheets" : [
8+ {
9+ "cells" : [
10+ {
11+ "cell_type" : " heading" ,
12+ "level" : 1 ,
13+ "metadata" : {},
14+ "source" : [
15+ " 7.11. Inlining Callback Functions"
16+ ]
17+ },
18+ {
19+ "cell_type" : " markdown" ,
20+ "metadata" : {},
21+ "source" : [
22+ " # Problem\n " ,
23+ " You\u2019 re writing code that uses callback functions, but you\u2019 re concerned about the pro\u2010 \n " ,
24+ " liferation of small functions and mind boggling control flow. You would like some way\n " ,
25+ " to make the code look more like a normal sequence of procedural steps.\n " ,
26+ " \n " ,
27+ " # Solution\n " ,
28+ " Callback functions can be inlined into a function using generators and coroutines. To\n " ,
29+ " illustrate, suppose you have a function that performs work and invokes a callback as\n " ,
30+ " follows (see Recipe 7.10):"
31+ ]
32+ },
33+ {
34+ "cell_type" : " code" ,
35+ "collapsed" : false ,
36+ "input" : [
37+ " def apply_async(func, args, *, callback):\n " ,
38+ " # Compute the result\n " ,
39+ " result = func(*args)\n " ,
40+ " # Invoke the callback with the result\n " ,
41+ " callback(result)"
42+ ],
43+ "language" : " python" ,
44+ "metadata" : {},
45+ "outputs" : [],
46+ "prompt_number" : 1
47+ },
48+ {
49+ "cell_type" : " markdown" ,
50+ "metadata" : {},
51+ "source" : [
52+ " Now take a look at the following supporting code, which involves an `Async` class and\n " ,
53+ " an `inlined_async` decorator:"
54+ ]
55+ },
56+ {
57+ "cell_type" : " code" ,
58+ "collapsed" : false ,
59+ "input" : [
60+ " from queue import Queue\n " ,
61+ " from functools import wraps\n " ,
62+ " \n " ,
63+ " class Async:\n " ,
64+ " def __init__(self, func, args):\n " ,
65+ " self.func = func\n " ,
66+ " self.args = args\n " ,
67+ " \n " ,
68+ " def inlined_async(func):\n " ,
69+ " @wraps(func)\n " ,
70+ " def wrapper(*args):\n " ,
71+ " f = func(*args)\n " ,
72+ " result_queue = Queue()\n " ,
73+ " result_queue.put(None)\n " ,
74+ " while True:\n " ,
75+ " result = result_queue.get()\n " ,
76+ " try:\n " ,
77+ " a = f.send(result)\n " ,
78+ " apply_async(a.func, a.args, callback=result_queue.put)\n " ,
79+ " except StopIteration:\n " ,
80+ " break\n " ,
81+ " return wrapper"
82+ ],
83+ "language" : " python" ,
84+ "metadata" : {},
85+ "outputs" : [],
86+ "prompt_number" : 2
87+ },
88+ {
89+ "cell_type" : " markdown" ,
90+ "metadata" : {},
91+ "source" : [
92+ " These two fragments of code will allow you to inline the callback steps using `yield`\n " ,
93+ " statements. For example:"
94+ ]
95+ },
96+ {
97+ "cell_type" : " code" ,
98+ "collapsed" : false ,
99+ "input" : [
100+ " def add(x, y):\n " ,
101+ " return x + y\n " ,
102+ " \n " ,
103+ " @Async.inlined_async\n " ,
104+ " def test():\n " ,
105+ " r = yield Async(add, (2, 3))\n " ,
106+ " print(r)\n " ,
107+ " r = yield Async(add, ('hello', 'world'))\n " ,
108+ " print(r)\n " ,
109+ " for n in range(10):\n " ,
110+ " r = yield Async(add, (n, n))\n " ,
111+ " print(r)\n " ,
112+ " print('Goodbye')"
113+ ],
114+ "language" : " python" ,
115+ "metadata" : {},
116+ "outputs" : [],
117+ "prompt_number" : 3
118+ },
119+ {
120+ "cell_type" : " markdown" ,
121+ "metadata" : {},
122+ "source" : [
123+ " If you call `test()`, you\u2019 ll get output like this:\n " ,
124+ " ```\n " ,
125+ " 5\n " ,
126+ " helloworld\n " ,
127+ " 0\n " ,
128+ " 2\n " ,
129+ " 4\n " ,
130+ " 6\n " ,
131+ " 8\n " ,
132+ " 10\n " ,
133+ " 12\n " ,
134+ " 14\n " ,
135+ " 16\n " ,
136+ " 18\n " ,
137+ " Goodbye\n " ,
138+ " ```"
139+ ]
140+ },
141+ {
142+ "cell_type" : " code" ,
143+ "collapsed" : false ,
144+ "input" : [
145+ " test()"
146+ ],
147+ "language" : " python" ,
148+ "metadata" : {},
149+ "outputs" : [
150+ {
151+ "output_type" : " stream" ,
152+ "stream" : " stdout" ,
153+ "text" : [
154+ " 5\n " ,
155+ " helloworld\n " ,
156+ " 0\n " ,
157+ " 2\n " ,
158+ " 4\n " ,
159+ " 6\n " ,
160+ " 8\n " ,
161+ " 10\n " ,
162+ " 12\n " ,
163+ " 14\n " ,
164+ " 16\n " ,
165+ " 18\n " ,
166+ " Goodbye\n "
167+ ]
168+ }
169+ ],
170+ "prompt_number" : 5
171+ },
172+ {
173+ "cell_type" : " markdown" ,
174+ "metadata" : {},
175+ "source" : [
176+ " Aside from the special decorator and use of `yield`, you will notice that no callback\n " ,
177+ " functions appear anywhere (except behind the scenes)."
178+ ]
179+ },
180+ {
181+ "cell_type" : " markdown" ,
182+ "metadata" : {},
183+ "source" : [
184+ " # Discussion\n " ,
185+ " This recipe will really test your knowledge of callback functions, generators, and control\n " ,
186+ " flow.\n " ,
187+ " \n " ,
188+ " First, in code involving callbacks, the whole point is that the current calculation will\n " ,
189+ " suspend and resume at some later point in time (e.g., asynchronously). When the \n " ,
190+ " calculation resumes, the callback will get executed to continue the processing. The \n " ,
191+ " `apply_async()` function illustrates the essential parts of executing the callback, although\n " ,
192+ " in reality it might be much more complicated (involving threads, processes, event \n " ,
193+ " handlers, etc.).\n " ,
194+ " \n " ,
195+ " The idea that a calculation will suspend and resume naturally maps to the execution\n " ,
196+ " model of a generator function. Specifically, the `yield` operation makes a generator\n " ,
197+ " function emit a value and suspend. Subsequent calls to the `__next__()` or `send()`\n " ,
198+ " method of a generator will make it start again.\n " ,
199+ " \n " ,
200+ " With this in mind, the core of this recipe is found in the `inline_async()` decorator\n " ,
201+ " function. The key idea is that the decorator will step the generator function through all\n " ,
202+ " of its `yield` statements, one at a time. To do this, a result queue is created and initially\n " ,
203+ " populated with a value of `None`. A loop is then initiated in which a result is popped off\n " ,
204+ " the queue and sent into the generator. This advances to the next yield, at which point\n " ,
205+ " an instance of `Async` is received. The loop then looks at the function and arguments,\n " ,
206+ " and initiates the asynchronous calculation `apply_async()`. However, the sneakiest part\n " ,
207+ " of this calculation is that instead of using a normal callback function, the callback is set\n " ,
208+ " to the queue `put()` method.\n " ,
209+ " \n " ,
210+ " At this point, it is left somewhat open as to precisely what happens. The main loop\n " ,
211+ " immediately goes back to the top and simply executes a `get()` operation on the queue.\n " ,
212+ " If data is present, it must be the result placed there by the `put()` callback. If nothing is\n " ,
213+ " there, the operation blocks, waiting for a result to arrive at some future time. How that\n " ,
214+ " might happen depends on the precise implementation of the `apply_async()` function.\n " ,
215+ " \n " ,
216+ " If you\u2019 re doubtful that anything this crazy would work, you can try it with the \n " ,
217+ " multiprocessing library and have async operations executed in separate processes:\n " ,
218+ " ```python\n " ,
219+ " if __name__ == '__main__':\n " ,
220+ " import multiprocessing\n " ,
221+ " pool = multiprocessing.Pool()\n " ,
222+ " apply_async = pool.apply_async\n " ,
223+ " \n " ,
224+ " # Run the test function\n " ,
225+ " test()\n " ,
226+ " ```\n " ,
227+ " Indeed, you\u2019 ll find that it works, but unraveling the control flow might require more\n " ,
228+ " coffee.\n " ,
229+ " \n " ,
230+ " Hiding tricky control flow behind generator functions is found elsewhere in the \n " ,
231+ " standard library and third-party packages. For example, the `@contextmanager` decorator in\n " ,
232+ " the `contextlib` performs a similar mind-bending trick that glues the entry and exit\n " ,
233+ " from a context manager together across a yield statement. The popular \n " ,
234+ " [Twisted package](http://twistedmatrix.com/) has inlined callbacks that are also similar."
235+ ]
236+ }
237+ ],
238+ "metadata" : {}
239+ }
240+ ]
241+ }
0 commit comments