0

I was trying to use a generator to save on some memory and ran into a problem. I am a bit surprised at the result because my understanding was that integers were immutable, so can someone explain what's going on?

>>>> a = []
>>>> for i in range(10):
....     a.append((i for _ in [0]))
....     
>>>> list(a[0])
[9]

When I do it using list comprehension instead it does what I want:

>>>> a = []
>>>> for i in range(10):
....     a.append([i for _ in [0]])
....     
>>>> a
[[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]]

I can sort of reason that what's going on is that the generator is somehow getting a "reference" to the value of i, which, after the last time through the loop is 9, but this seems anti-pythonic as python doesn't have references as such (at least as far as I understand).

Questions:

  1. What is going on? How is this possible? A link to some python docs that can explain exactly what's going on would be appreciated.
  2. How can I do what I want (use a generator with a variable that's going to change in the future but which I need the current value for something)?

Update:

Realistic use case:

def get_some_iter(a, b):
    iters = []
    for i in a:
        m = do_something(i, len(b))
        iters.append((SomeObj(j, m) for j in itertools.combinations(b, i))

    return itertools.chain(*iters)
CrazyCasta
  • 26,917
  • 4
  • 45
  • 72
  • You don't use i in the gen exp until the loop is over so the i points to 9, the list comp appends each i through the loop, this is exactly what you would expect. What are you actually trying to do? – Padraic Cunningham Jan 24 '16 at 00:21
  • Well the generator expression creates a generator object. I assume that object would have references to everything that I used, rather then references to references which is what it seems to be doing. For instance, if I did this all inside a function and then i fell out of scope, how would I then be able to use the generator? – CrazyCasta Jan 24 '16 at 00:31
  • You are appending a generator with a reference to `i`, when the loop finishes i is 9 and that is when you consume the generator so you get 9, it is not a million miles away from how a lambda works in a loop, the difference between `lambda i=i:` and `lambda i:`. Can you add a small realistic use case? – Padraic Cunningham Jan 24 '16 at 00:33
  • Do you actually want to do something like `yield from (SomeObj(j, m) for j in itertools.combinations(b, i)`? – Padraic Cunningham Jan 24 '16 at 00:51
  • I guess so, except that I'm using python2.7 :( – CrazyCasta Jan 24 '16 at 00:55
  • all yield from is another loop, for i in ... yield i will do the same job, you already have chain so if you need to flatten use chain in the loop, `for i in chain(*whatever);yield i` – Padraic Cunningham Jan 24 '16 at 00:56
  • Basically you want a generator function – Padraic Cunningham Jan 24 '16 at 01:01
  • No, `for i in chain(*whatever);yield i` wouldn't work. I guess I could just do `for _ in my_generator: yield _` though. – CrazyCasta Jan 24 '16 at 01:25

2 Answers2

2

Generators are lazily evaluated. Until you sent it to list(), that generator was not executing anything. When you did send it to list(), it then executed. What was i at that point? The last value it was assigned: 9.

list comprehensions are eagerly evaluated. Each time that expression is encountered, its contents are immediately evaluated. The first time through the loop, i is 0, and that value is immediately retrieved and stored. This produces a different value for each loop iteration.

TigerhawkT3
  • 48,464
  • 6
  • 60
  • 97
-1

Well yes the generator keeps the reference to i, because there is not really any other solution for it internally.

The best solution would be to have it all as a generator:

a = ((i for _ in [0]) for i in range(10))
for g in a:
    print(list(g))
[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]

Of course it doesn't solve the problem if you need to access directly to the n+1 value without getting the first n. If you need to do so I think the most pythonic way would be to build your own iterator, something like this :

class ListIter(object):
    def __init__(self, i):
        self.i= i
        self.iter = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.iter > 0:
            raise StopIteration
        else:
            self.iter += 1
            return self.i

a = [ListIter(i) for i in range(10)]
list(a[0])
[0]
Mijamo
  • 3,436
  • 1
  • 21
  • 21
  • I would assume that it kept the value of the `i` reference rather then a reference to `i` itself. In the same way as when I do `a=[]; b=a` I've not copied the list, but I have copied the reference to the list. – CrazyCasta Jan 24 '16 at 00:41
  • The problem is that where would it be stored internally ? It would be a bit of black magic. This is why you should build your own iterator, where you can explicitely store i as an attribute. Anyway generators are iterators so my example of iterable would not take more memory than what you originally intended to do, it just takes a few more lines but it works ;-) – Mijamo Jan 24 '16 at 00:44
  • @CrazyCasta, `i` is only evaluated when your generator is evaluated , `i` is just a name that you keep pointing to a new object, if python kept all that data in memory it would defeat the purpose of a generator. – Padraic Cunningham Jan 24 '16 at 00:49
  • @PadraicCunningham No, I think you misunderstand the point of a generator. `i` is outside the context of the generator. The point of a generator is to not keep a reference to more then one of the items that it generates at a time, not to not keep references to values provided by the context. Also, can you show me where it says that generates are referring to names? – CrazyCasta Jan 24 '16 at 00:52
  • You are right CrazyCasta, i is outside the context of the generator but python has no way of knowing where to stock all the values of i for the generators. This is why a custom class is here necessary, to explicitely tell Python where to stock i. – Mijamo Jan 24 '16 at 00:57
  • @CrazyCasta, If you printed `i` outside the for loop and you saw `9` would you be surprised? That is exactly the same thing your code is doing, i is a variable that keeps changing pointing to new values, when you evaluate i the loop is over so you get 9 – Padraic Cunningham Jan 24 '16 at 01:00
  • What I'm surprised by is that python doesn't copy the used variables when you create a generator. This is what finally succinctly answered my question, though I'm still trying to find a reference in the python docs for it: http://stackoverflow.com/a/938493/1695766 – CrazyCasta Jan 24 '16 at 01:27