Finally figured out member generators...

Steven Taschuk staschuk at telusplanet.net
Mon Mar 3 20:06:10 EST 2003


Quoth Bjorn Pettersen:
  [...]
>  >>> class D:
>  ...   def __init__(self): self.v = 5
>  ...   def __iter__(self): return self
>  ...   def next(self):
>  ...       for i in range(10):
>  ...          yield i + self.v
>  ...
>  >>> d = D()
>  >>> list(d)
> 
> which will recurse infitely. That __iter__ should return self.next(),
> thus creating and returning an iterator object because of generator
> semantics didn't occur to me since the whole point was to have the class
> be it's own iterator... Oh, well, it was fun anyways :-)

Um.  I think you're conflating iterators with iterables.  In the
code above, if D.__iter__ is changed to
	return self.next()
as you suggest, then instances of D will be iterable, but they
will not be iterators, since the next method does not return
elements (it returns a generator object).

Iterables must supply __iter__(), returning an iterator; iterators
must supply next(), returning successive elements of the sequence
at each call.  Iterators are stateful by specification; iterables
need not be.

Iterables may be used in for loops and whatnot; the loop obtains
(by calling __iter__) an iterator for the iterable and uses that
iterator's next() method to obtain elements.

(The distinction is somewhat obscured by the fact that iterators
themselves are iterable.)

In particular, if you just want to be able to write

	d = D()
	for x in d:
		...

then you want instances of D to be iterables, not necessarily
iterators.  In this case you could write, for example:

	class SpamIterable:
		def __init__(self, scale):
			self.scale = scale
		def __iter__(self):
			return SpamIterator(self)

	class SpamIterator:
		def __init__(self, iterable):
			self.d = d
			self.val = 0
		def __iter__(self):
			return self
		def next(self):
			if self.val >= 5:
				raise StopIteration()
			val = self.val
			self.val += 1
			return val*self.d.scale

Thus:

	>>> x = SpamIterable(3)
	>>> x
	<foo.SpamIterable instance at 0x815565c>
	>>> iter(x)
	<foo.SpamIterator instance at 0x8155a04>
	>>> list(x)
	[0, 3, 6, 9, 12]

The list() call obtains (by calling SpamIterable.__iter__) a
SpamIterator object, then uses that object's next method to obtain
the elements.  Note that the iterable and iterator objects are
distinct.

A generator function, as you say, is handy here, because it lets
you avoid writing a separate iterator class:

	class WithGenerator:
		def __init__(self, scale):
			self.scale = scale
		def __iter__(self):
			for i in range(5):
				yield i*self.scale

But the iterator is still a distinct object; it's just a generator
object instead of an instance of a class you wrote yourself:

	>>> x = WithGenerator(3)
	>>> x
	<foo.WithGenerator instance at 0x81212a4>
	>>> g = iter(x)
	>>> g
	<generator object at 0x8155aa0>
	>>> while 1:
	...     g.next()
	... 
	0
	3
	6
	9
	12
	Traceback (most recent call last):
	  File "<stdin>", line 2, in ?
	StopIteration
	>>> list(x)
	[0, 3, 6, 9, 12]

-- 
Steven Taschuk             "The world will end if you get this wrong."
staschuk at telusplanet.net    (Brian Kernighan and Lorrinda Cherry,
                            "Typesetting Mathematics -- User's Guide")





More information about the Python-list mailing list