2

I am going through the exercises of this site https://anandology.com/python-practice-book/working-with-data.html when I tried to recreate the zip function through list comprehension. Now I created this function. But instead of getting a list I get a generator :-(

def zipp (liste1,liste2):
    length= len(liste1)
    zipped=[]
    [zipped.append(tuple(liste1[i], liste2[i]) for i in range(length))]
    return zipped

I searched a little in here and found this: Python: why does list comprehension produce a generator?

Accordingly I used the "tuple" statement already but to no awail.

I have no idea why I get a generator even with the tuple() inserted. So my questions:

  1. why?
  2. What do I need to change or where can I read/hear more to get "enlightened" myself?
  3. How could I use the generator to get the result? (or where can I read about this?)

Thanks.

edit: The result I expect is list of tuple with member of each list in it. This I what I should get:

zipp([1, 2, 3], ["a", "b", "c"]) -> [(1, "a"), (2, "b"), (3, "c")]

Andreas K.
  • 389
  • 4
  • 17
  • 4
    This is abusing a side-effect of list comprehensions; this is not considered good practice. You're using the list comprehension to drive what should be a `for` loop. – roganjosh Nov 21 '18 at 16:54
  • You should definitely never use a list comprehension for side-effects. *especially* not in this case, were your list-comprehension should be doing the appending... – juanpa.arrivillaga Nov 21 '18 at 16:55
  • It's bending my mind a little to understand exactly what you want your output to look like. Can you please show what you are expecting? The outer list comp is throwing me – roganjosh Nov 21 '18 at 16:56
  • Anyway, `tuple(liste1[i], liste2[i]) for i in range(length)` is a generator expression, which is sort of like a list comprehension that creates a generator instead. So don't use that. – juanpa.arrivillaga Nov 21 '18 at 16:57
  • 1
    @roganjosh they are trying to re-implement `zip`. – juanpa.arrivillaga Nov 21 '18 at 16:57
  • can't you use `list(zip())` ? – Jean-François Fabre Nov 21 '18 at 16:57
  • @Jean-FrançoisFabre I think the point is to re-implement `zip` as an exercise – juanpa.arrivillaga Nov 21 '18 at 16:58
  • in that case the length is incorrect. Should be min of both lengths. – Jean-François Fabre Nov 21 '18 at 16:58
  • @Jean-FrançoisFabre well, that's definitely one issue. – juanpa.arrivillaga Nov 21 '18 at 16:59
  • @juanpa.arrivillaga mm, so the outer list isn't really supposed to be substituting a `for`, that's what the (erroneous) gen expr is for. Ok, I wasn't seeing it – roganjosh Nov 21 '18 at 16:59
  • @roganjosh ah jeez, yeah didn't even notice that myself, this is simply a call to `.append()` wrapped in a list: `[ ]` – juanpa.arrivillaga Nov 21 '18 at 17:00
  • @Jean-FrançoisFabre It is an exercise. And yes, you are right I should check for the length f both lists. – Andreas K. Nov 21 '18 at 18:49
  • @roganjosh Thanks for the feedback. You are right a for loop would be the way I'd take normally. But the exercise was to use a list comprehension. And this was the best I could come up. But one important question for me for better understanding: what is meant by "side-effect of list comprehensions"? – Andreas K. Nov 21 '18 at 18:53
  • 1
    List comprehensions can mutate mutable objects as though the list comprehension was a regular `for` loop. So, I could use a list comprehension like so; `my_list = []; [my_list.append(x) for x in range(5)]`. In this instance, the list comprehension does not generate its own list at all. Instead, it modifies a different list altogether. This is considered a _side effect_; the correct approach would be `my_list = [x for x in range(5)]`.. Silly example, but hopefully illustrates the point. – roganjosh Nov 21 '18 at 18:59
  • 1
    Sorry for the multiple edits, on a phone. Obviously the best way in my example would just be `my_list = list(range(5))` but I'm shooting for an example with minimal fighting against autocorrect to illustrate my point – roganjosh Nov 21 '18 at 19:04
  • Ok. After reading it a few times I think I got the point. Thanks for explaining! – Andreas K. Nov 21 '18 at 19:06

2 Answers2

2

you're putting a generator in your object:

tuple(liste1[i], liste2[i]) for i in range(length)

(and tuple doesn't work too, just remove it....)

(and don't use comprehensions for side effects)

The best way would be to rewrite it completely using a list comprehension which actually returns something, as list comprehensions are supposed to, taking the min of both lengths to fully emulate zip:

def zipp (liste1,liste2):
    return [(liste1[i], liste2[i]) for i in range(min(len(liste1),len(liste2)))]

classic loop version (no comprehensions)

def zipp (liste1,liste2):
   result = []
   for i in range(min(len(liste1),len(liste2))):
       result.append((liste1[i], liste2[i]))
   return result

of course this is nothing else than list(zip(liste1,liste2)) (forcing iteration on zip)

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • I would just add the straight-forward for-loop way as well, just to illustrate what list-comprehensions are actually doing – juanpa.arrivillaga Nov 21 '18 at 17:13
  • Your zipp solution is actually what I was looking for! Thanks. Btw, I fixed mine too, but it got even uglier: `[zipped.append([tuple([liste1[i], liste2[i]]) for i in range(length)])]` or without tuple:`zipped.append([(liste1[i], liste2[i]) for i in range(length)])` – Andreas K. Nov 21 '18 at 19:00
  • don't append to a list with a list, extend it. – Jean-François Fabre Nov 21 '18 at 19:29
1

trying to answer your questions...

1.- why?

A: As many have mention, you return the zipped list as a list generators within the list comprehension [zipped.append(tuple(liste1[i], liste2[i]) for i in range(length))], not a nice way of doing by the way. that's why you get those generators.

2.- What do I need to change or where can I read/hear more to get "enlightened" myself?

A: If you still want to do that way, you only have to append the two items, by moving the parenthesis and removing the tuple function, like so:

def zipp (liste1, liste2):
    length = len(liste1)
    zipped = []
    [zipped.append( (liste1[i], liste2[i]) ) for i in range(length) ]  # not the best way, but still works. This created list is never used.
    return zipped

then it's possible to return the list

zipp([1,2,3], ['a','b','c'])

Note that this asumes both list have same length. Otherwise you have to choose one (like you are doing) or finding the minimum of both lengths (it's also posible to choose the longest and fill with whatever it's needed):

min(len(liste1), len(liste2))

3.- How could I use the generator to get the result?

A: For it to be a generator you need to yield the nedeed value:

def zipp2 (liste1,liste2):
    i = 0
    minval = min(len(liste1), len(liste2))
    while i< minval:
        yield (liste1[i], liste2[i])
        i += 1

# call the function generator
gen = zipp2([1,2,3], ['a','b','c'])
print(gen)
for p in gen:
    print(p)

and get the results...

<generator object zipp2 at 0x7fe46bef3db0>
(1, 'a')
(2, 'b')
(3, 'c')
Traxidus Wolf
  • 808
  • 1
  • 9
  • 18
  • Thanks for the zipp2 snippet. But my question regarded my accidental generator generation. I thought there might be a way to get real values by using this accidental generator? – Andreas K. Nov 21 '18 at 18:55
  • as @Jean-François Fabre's comment says: don't append it, extend it. like so: `zipped.extend( [(liste1[i], liste2[i]) for i in range(length)] )` – Traxidus Wolf Nov 21 '18 at 22:58
  • extend() method takes an interable as argument, so it also works without the square brackets: `zipped.extend( (liste1[i], liste2[i]) for i in range(length) )` – Traxidus Wolf Nov 21 '18 at 23:14
  • Ok. Thanks. I didn't now this method yet. – Andreas K. Nov 22 '18 at 07:36