I love Python’s “zip” function. I’m not sure just what it is about zip that I enjoy, but I have often found it to be quite useful. Before I describe what “zip” does, let me first show you an example:
>>> s = 'abc' >>> t = (10, 20, 30) >>> zip(s,t) [('a', 10), ('b', 20), ('c', 30)]
As you can see, the result of “zip” is a sequence of tuples. (In Python 2, you get a list back. In Python 3, you get a “zip object” back.) The tuple at index 0 contains s[0] and t[0]. The tuple at index 1 contains s[1] and t[1]. And so forth. You can use zip with more than one iterable, as well:
>>> s = 'abc' >>> t = (10, 20, 30) >>> u = (-5, -10, -15) >>> list(zip(s,t,u)) [('a', 10, -5), ('b', 20, -10), ('c', 30, -15)]
(You can also invoke zip with a single iterable, thus ending up with a bunch of one-element tuples, but that seems a bit weird to me.)
I often use “zip” to turn parallel sequences into dictionaries. For example:
>>> names = ['Tom', 'Dick', 'Harry'] >>> ages = [50, 35, 60] >>> dict(zip(names, ages)) {'Harry': 60, 'Dick': 35, 'Tom': 50}
In this way, we’re able to quickly and easily product a dict from two parallel sequences.
Whenever I mention “zip” in my programming classes, someone inevitably asks what happens if one argument is shorter than the other. Simply put, the shortest one wins:
>>> s = 'abc' >>> t = (10, 20, 30, 40) >>> list(zip(s,t)) [('a', 10), ('b', 20), ('c', 30)]
(If you want zip to return one tuple for every element of the longer iterable, then use “izip_longest” from the “itertools” package.)
Now, if there’s something I like even more than “zip”, it’s list comprehensions. So last week, when a student of mine asked if we could implement “zip” using list comprehensions, I couldn’t resist.
So, how can we do this?
First, let’s assume that we have our two equal-length sequences from above, s (a string) and t (a tuple). We want to get a list of three tuples. One way to do this is to say:
[(s[i], t[i]) # produce a two-element tuple for i in range(len(s))] # from index 0 to len(s) - 1
To be honest, this works pretty well! But there are a few ways in which we could improve it.
First of all, it would be nice to make our comprehension-based “zip” alternative handle inputs of different sizes. What that means is not just running range(len(s)), but running range(len(x)), where x is the shorter sequence. We can do this via the “sorted” builtin function, telling it to sort the sequences by length, from shortest to longest. For example:
>>> s = 'abcd' >>> t = (10, 20, 30) >>> sorted((s,t), key=len) [(10, 20, 30), 'abcd']
In the above code, I create a new tuple, (s,t), and pass that as the first parameter to “sorted”. Given these inputs, we will get a list back from “sorted”. Because we pass the builtin “len” function to the “key” parameter, “sorted” will return [s,t] if s is shorter, and [t,s] if t is shorter. This means that the element at index 0 is guaranteed not to be longer than any other sequence. (If all sequences are the same size, then we don’t care which one we get back.)
Putting this all together in our comprehension, we get:
>>> [(s[i], t[i]) for i in range(len(sorted((s,t), key=len)[0]))]
This is getting a wee bit complex for a single list comprehension, so I’m going to break off part of the second line into a function, just to clean things up a tiny bit:
>>> def shortest_sequence_range(*args): return range(len(sorted(args, key=len)[0])) >>> [(s[i], t[i]) for i in shortest_sequence_range(s,t) ]
Now, our function takes *args, meaning that it can take any number of sequences. The sequences are sorted by length, and then the first (shortest) sequence is passed to “len”, which calculates the length and then returns the result of running “range”.
So if the shortest sequence is ‘abc’, we’ll end up returning range(3), giving us indexes 0, 1, and 2 — perfect for our needs.
Now, there’s one thing left to do here to make it a bit closer to the real “zip”: As I mentioned above, Python 2’s “zip” returns a list, but Python 3’s “zip” returns an iterator object. This means that even if the resulting list would be extremely long, we won’t use up tons of memory by returning it all at once. Can we do that with our comprehension?
Yes, but not if we use a list comprehension, which always returns a list. If we use a generator expression, by contrast, we’ll get an iterator back, rather than the entire list. Fortunately, creating such a generator expression is a matter of just replacing the [ ] of our list comprehension with the ( ) of a generator expression:
>>> def shortest_sequence_range(*args): return range(len(sorted(args, key=len)[0])) >>> g = ((s[i], t[i]) for i in shortest_sequence_range(s,t) ) >>> for item in g: print(item) ('a', 10) ('b', 20) ('c', 30)
And there you have it! Further improvements on these ideas are welcome — but as someone who loves both “zip” and comprehensions, it was fun to link these two ideas together.
The post Implementing “zip” with list comprehensions appeared first on Lerner Consulting Blog.