views:

443

answers:

4

Lets say I have a generator function that looks like this:

def fib():
    x,y = 1,1
    while True:
        x, y = y, x+y
        yield x

Ideally, I could just use fib()[10] or fib()[2:12:2] to get indexes and slices, but currently I have to use itertools for these things. I can't use a generator for a drop in replacement for lists.

I believe the solution will be to wrap fib() in a class:

class Indexable(object):
    ....

fib_seq = Indexable(fib())

What should Indexable look like to make this work?

+5  A: 
import itertools
class Indexable(object):
    def __init__(self,it):
        self.it=it
    def __iter__(self):
        for elt in self.it:
            yield elt
    def __getitem__(self,index):
        try:
            return next(itertools.islice(self.it,index,index+1))
        except TypeError:
            return list(itertools.islice(self.it,index.start,index.stop,index.step))

You could use it like this:

it=Indexable(fib())
print(it[10])
#144
print(it[2:12:2])
#[610, 1597, 4181, 10946, 28657]

Notice that it[2:12:2] does not return [3, 8, 21, 55, 144] since the iterator had already advanced 11 elements because of the call to it[10].

Edit: If you'd like it[2:12:2] to return [3, 8, 21, 55, 144] then perhaps use this instead:

class Indexable(object):
    def __init__(self,it):
        self.it=it
        self.already_computed=[]
    def __iter__(self):
        for elt in self.it:
            self.already_computed.append(elt)
            yield elt
    def __getitem__(self,index):
        try:
            max_idx=index.stop
        except AttributeError:
            max_idx=index
        n=max_idx-len(self.already_computed)+1
        if n>0
            self.already_computed.extend(itertools.islice(self.it,n))
        return self.already_computed[index]            

This version saves the results in self.already_computed and uses those results if possible. Otherwise, it computes more results until it has sufficiently many to return the indexed element or slice.

unutbu
To exhibit the same behavior as a list, __getitem__ would need to rewind the generator. Is there an easy way to do that?
Jason Christa
I don't know if there's any way to do that, easy or not, but the Indexable class could just store all the already-generated items in a list. Then `__getitem__` would pull the numbers directly from the list, after first advancing the generator if necessary.
MatrixFrog
Thanks MatrixFrog; that's what I ended up doing.
unutbu
That's a handy solution. It looks like there are two ways to accomplish my question, using a result cache like you have or copying the iterator. It is just a trade-off between using more memory or cpu.
Jason Christa
A: 

So based off the code from ~unutbu and adding in a little itertools.tee:

import itertools

class Indexable(object):
    def __init__(self, it):
        self.it = it

    def __iter__(self):
        self.it, cpy = itertools.tee(self.it)
        return cpy

    def __getitem__(self, index):
        self.it, cpy = itertools.tee(self.it)
        if type(index) is slice:
            return list(itertools.islice(cpy, index.start, index.stop, index.step))
        else:
            return next(itertools.islice(cpy, index, index+1))
Jason Christa
Right, but this option burns CPU every time you use it. For lists that are too long to store in ~unutbu's `already_computed` the CPU time required here is likely to be excessive too.
gnibbler
I think it also might come down to the complexity of the generator, if the code inside the generator is CPU heavy using already_computed is the best option but perhaps if it is simple like xrange just recomputing it is better.
Jason Christa
A: 

If it's a 1-use slice than you could simply use the method written by ~unutbu. If you need to slice multiple times you would have to store all the intermediate values so you can "rewind" the iterator. Since iterators can iterate anything it would not have a rewind method by default.

Also, since a rewinding iterator has to store every intermediate result it would (in most cases) have no benefit over simply doing list(iterator)

Basically... you either don't need an iterator, or you're not specific enough about the situation.

WoLpH
`list(fib())` will give you a MemoryError eventually
gnibbler
How about the case where I want the 6,000th fibonacci number. The clearest way to express this is fib[6000] even though there are several ways to get it, they just aren't expressed as elegantly.
Jason Christa
My point is, in nearly all real-life situations you would make the object itself sliceable instead of implementing slicing at the generator.If you don't, it would be impossible to optimize the function call properly. For any large fibonacci number you wouldn't want to generate the entire list just to get a number, you would use a method to "guess" the number.
WoLpH
A: 

Here is ~unutbu's answer modified to subclass list. Obviously abuse such as append, insert etc. will produce weird results!

you do get __str__ and __repr__ methods for free though

import itertools
class Indexable(list):
    def __init__(self,it):
        self.it=it
    def __iter__(self):
        for elt in self.it:
            yield elt
    def __getitem__(self,index):
        try:
            max_idx=index.stop
        except AttributeError:
            max_idx=index
        while max_idx>=len(self):
            self.append(next(self.it))
        return list.__getitem__(self,index)
gnibbler