Function to enable attribute access for generator instances. Simplifies data sharing for advanced uses of generators and provides much of the functionality sought by PEP 288. <br> Most uses of generators have no need for data sharing. This recipe is for the few tough cases which can be written more elegantly when attribute access is enabled.
Python, 56 lines
Generators simplify writing iterators which produce data as needed rather than all at once. The yield keyword freezes execution state, eliminating the need for instance variables and progress flags. As well, generators automatically create the __iter__() and next() methods for the iterator interface.
Generators would also be useful for writing complex data consumer routines. Again, the ability to freeze and restart eliminates the code to explicitly save and restore the execution state between calls. However, it is difficult to cleanly pass data into consumers written using generators.
This recipe provides a simple way for generator instances to read and write attributes just like their class-based counterparts. To use the recipe, add a self parameter to the beginning of a generator definition. Then, call enableAttributes() to turn on attribute sharing.
After that, use attributes the same way you would with classes and instances. Writing complex consumers becomes trivially easy. Likewise, it simplifies implementation and increases the capabilities of other cookbook recipes using generators to create cooperative multitasking, co-routines, tasklets, continuations, finite state machines, and other wonders.
When called, a generator definition creates a generator instance without attribute access. The enableAttributes routine wraps and replaces the generator definition with a new factory function that creates an attribute enabled class instance.
enableAttributes goes to great lengths to make sure the new factory function resembles the original generator (same name, same doc string, preserved function attributes, and an internal structure allowing autocompleters and pydoc to find the correct arguments).
When run, the factory function creates a generator instance using the original generator definition. The instance is embedded in a shell class instance whose function is to store attributes and to forward .next() calls to the generator instance.
UNDERSTANDING THE DETAILED WORKINGS OF THE EXAMPLE
The key to understanding enableAttributes() is to study what is passed to exec() in the code sample: <pre> _redef_tmp = outputCaps #1 def outputCaps(logfile): #2 wrapped = type('_GeneratorWrapper', (), outputCaps._realgen.__dict__)() #3 wrapped.__iter__ = lambda self: self #4 wrapped.next = outputCaps._realgen(wrapped, logfile).next #5 return wrapped outputCaps.__doc__ = _redef_tmp.__doc__ #6 outputCaps._realgen = _redef_tmp #7 del _redef_tmp #8 </pre>
1 the original generator is saved.
2 a new factory function is defined in its place (note, the parameter list excludes self so that pydoc will show the generator correctly).
'wrapped' starts out as an instance of an empty class whose dictionary is the same as the original generator.
3 'wrapped' is an instance of an empty class that shares the same function dictionary as the original generator (this enables behavior like class variables).
4 to support the iterator protocol and match generator behavior, the instance is given an __iter__ method which returns self.
5 the original generator is called and the resulting generator-iterator is used to handle calls to the next() method.
6 the new factory function gets a docstring from the original generator definition.
7 the reference to the original generator definition is stored in the new factory function's attribute dictionary. this completes the wrapping.
8 the temporary variable is cleaned from the workspace. this is important because exec() runs the redefinition code with the globals from the workspace of the original generator definition. that workspace is left unchanged except for repointing the generator reference to the new factory function.