57

What is the analogue of Haskell's zipWith function in Python?

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
georg
  • 211,518
  • 52
  • 313
  • 390
Yrogirg
  • 2,301
  • 3
  • 22
  • 33
  • 7
    Could you explain how it works? There are plenty of people knowing Python, but only someof them know Haskell well enough. – Tadeck Mar 19 '12 at 07:50
  • 5
    @Tadeck: `zipWith` is like `map`, except it traverses two lists in parallel, applying a function to the corresponding items from each list. If one list is longer, the extra elements are ignored. For example, `zipWith (*) [1, 2, 3] [7, 8] == [7, 16]`. – hammar Mar 19 '12 at 12:01

6 Answers6

62

map()

map(operator.add, [1, 2, 3], [3, 2, 1])

Although a LC with zip() is usually used.

[x + y for (x, y) in zip([1, 2, 3], [3, 2, 1])]
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • 11
    Note: This has different behavior compared to `zipWith` when the lengths of the lists don't match. Haskell's `zipWith` truncates the input lists to the length of the shortest one, while Python's `map` passes `None` in place of the missing elements from the shorter list. For example, `zipWith (+) [1, 2], [3, 2, 1] == [4, 4]`, while `map(operator.add, [1, 2], [3, 2, 1])` throws an exception from trying to add an integer and `None`. – hammar Mar 19 '12 at 12:07
  • 8
    @hammar: you're talking about `map` in Python 2.x. `map` in Python 3.x (as well as `imap` in 2.x) stops after the shortest list ends. Also, `zip` stops after the shortest list ends in 2.x and 3.x – newacct Mar 19 '12 at 20:00
43

You can create yours, if you wish, but in Python we mostly do

list_c = [ f(a,b) for (a,b) in zip(list_a,list_b) ] 

as Python is not inherently functional. It just happens to support a few convenience idioms.

dsign
  • 12,340
  • 6
  • 59
  • 82
  • 19
    If you want to be slightly more elegant, you could do ``[f(*list_c) for list_c in zip(list_a, list_b)]`` - using the ``splat`` operator to unpack the tuple rather than stating it twice. This also has the advantage of that you can just add more arguments to the zip function and it'll work happily if you need to. – Gareth Latty Mar 19 '12 at 08:25
  • 5
    Note that if you are using Python 2.x, the above code isn't lazy: `zip()` will build a list that will be used once. In Python 2.x you can use `itertools.izip()` for lazy evaluation; in Python 3 you get lazy evaluation with the built-in `zip()`. – steveha Mar 19 '12 at 09:12
  • 3
    @steveha but note that it isn't truly lazy in (the closest Python has to) the Haskell sense even using `izip` instead of `zip`, since it is given as a list comprehension rather than a generator expression. – lvc Mar 19 '12 at 09:46
  • @lvc, I'm perfectly aware that the above code shows a list comprehension. Go ahead and read what I wrote again: `zip()` will build a list that is used once. To avoid building and then destroying that list, you can use `itertools.izip()`. I don't know Haskell, but it seems likely that even in Haskell, if the desired output product is a list, at some point the lazy code will be evaluated into a list. I suggested writing lazy code for the incidental work along the way to building the final, actually desired list. – steveha Mar 19 '12 at 21:25
  • 2
    @steveha what you said is that "the above code isn't lazy" in 2.x. It isn't lazy in 3.x either: it will produce the whole output list immediately. In 3.x it won't *also* construct an intermediate value for the ouput of `zip`, so in that sense there may be a bit more laziness, but the *code as a whole* isn't lazy no matter which version of Python you're using. For that, as @lvc said, you'd need a generator expression. (As well as `izip` to avoid materializing the whole intermediate list on accessing the first item.) – ben w Mar 19 '12 at 21:30
  • @benw, are you saying I was wrong to use the word "lazy" in conjunction with not building and destroying an intermediate list? I thought my use of the term "lazy" was correct: values aren't computed until we need them, and in this case "when we need them" is when they go into the list that is the desired final output. http://en.wikipedia.org/wiki/Lazy_evaluation All that said, I really don't feel that strongly about this, and if it is important to you that the word "lazy" never appear when a list is being built, then I apologize for offending your sensibilities. – steveha Mar 19 '12 at 21:44
13

You can use map:

>>> x = [1,2,3,4]
>>> y = [4,3,2,1]
>>> map(lambda a, b: a**b, x, y)
[1, 8, 9, 4]
heinrich5991
  • 2,694
  • 1
  • 19
  • 26
  • This works, and I love that it's purely functional, but being new to python I don't get why this works. I would have expected `a` and `b` to both be arrays – Christian Bongiorno Jun 08 '23 at 16:06
6

A lazy zipWith with itertools:

import itertools

def zip_with(f, *coll):
    return itertools.starmap(f, itertools.izip(*coll))

This version generalizes the behaviour of zipWith with any number of iterables.

Taurus Olson
  • 3,153
  • 2
  • 26
  • 21
4

I know this is an old question, but ...

It's already been said that the typical python way would be something like

results = [f(a, b) for a, b in zip(list1, list2)]

and so seeing a line like that in your code, most pythonistas will understand just fine.

There's also already been a (I think) purely lazy example shown:

import itertools

def zipWith(f, *args):
    return itertools.starmap(f, itertools.izip(*args))

but I believe that starmap returns an iterator, so you won't be able to index, or go through multiple times what that function will return.

If you're not particularly concerned with laziness and/or need to index or loop through your new list multiple times, this is probably as general purpose as you could get:

def zipWith(func, *lists):
    return [func(*args) for args in zip(*lists)]

Not that you couldn't do it with the lazy version, but you could also call that function like so if you've already built up your list of lists.

results = zipWith(func, *lists)

or just like normal like:

results = zipWith(func, list1, list2)

Somehow, that function call just looks simpler and easier to grok than the list comprehension version.


Looking at that, this looks strangely reminiscent of another helper function I often write:

def transpose(matrix):
    return zip(*matrix)

which could then be written like:

def transpose(matrix):
    return zipWith(lambda *x: x, *matrix)

Not really a better version, but I always find it interesting how when writing generic functions in a functional style, I often find myself going, "Oh. That's just a more general form of a function I've already written before."

  • Wow, I looked at this again and you could just do `zipWith = map` Hadn't noticed that before either. – everythingfunctional Aug 24 '17 at 21:06
  • If you want an iterator why not just `results = (f(a, b) for a, b in izip(list1, list2))`? – Elmex80s Jan 09 '18 at 10:41
  • @Elmex80s, iterators aren't an immutable value. Someone coming from a purely functional language like Haskell would be used to immutability by default. So my answer assumes that you *wouldn't* want an iterator. – everythingfunctional Jan 09 '18 at 19:46
4

Generally as others have mentioned map and zip can help you replicate the functionality of zipWith as in Haskel.

Generally you can either apply a defined binary operator or some binary function on two list.An example to replace an Haskel zipWith with Python's map/zip

Input: zipWith (+) [1,2,3] [3,2,1] 
Output: [4,4,4] 

>>> map(operator.add,[1,2,3],[4,3,2])
[5, 5, 5]
>>> [operator.add(x,y) for x,y in zip([1,2,3],[4,3,2])]
[5, 5, 5]
>>> 

There are other variation of zipWith aka zipWith3, zipWith4 .... zipWith7. To replicate these functionalists you may want to use izip and imap instead of zip and map.

>>> [x for x in itertools.imap(lambda x,y,z:x**2+y**2-z**2,[1,2,3,4],[5,6,7,8],[9,10,11,12])]
>>> [x**2+y**2-z**2 for x,y,z in itertools.izip([1,2,3,4],[5,6,7,8],[9,10,11,12])]
[-55, -60, -63, -64] 

As you can see, you can operate of any number of list you desire and you can still use the same procedure.

Abhijit
  • 62,056
  • 18
  • 131
  • 204
  • 3
    `map()` can take any arbitrary number of sequences. – Ignacio Vazquez-Abrams Mar 19 '12 at 08:19
  • Your first list comprehension doesn't need to use `operator.add`, it can just be `[x+y for x,y in ...]`. Likewise, there isn't much point in writing code like `[x for x in imap...]` - it is a bit clearer to write `list(imap...)`, but then you might as well just use `map`. – lvc Mar 19 '12 at 09:51