google web font

понедельник, 14 марта 2016 г.

How to expand an iterator into a list in Python3

One of the most noticeable changes from Python 2 to Python 3 is that map, filter, zip and range builtins now return iterators instead of lists. This is usually a good feature to have, because, well, nobody uses range outside of a for statement. But there may be rare cases when resulting list might take less memory then the iterator itself, especially when you nesting them like this:
def increase(self, *increments):
    self.values = map(sum, zip(self.values, increments))
This is going to eat all the memory and crash with a several gigabyte core dump. So the question is: how does one expand an iterator into a sequence in Python 3? Well, I don't want to hurt the Zen of Python, but there are more than one way to do so.

The most obvious one is this:
def increase(self, *increments):
    self.values = tuple(map(sum, zip(self.values, increments)))
It's too obvious to write the whole post about it. It is as obvious as to get rid of the iterator in favor of the list comprehension:
def increase(self, *increments):
    self.values = [sum(x) for x in zip(self.values, increments)]
(Don't forget to use square brackets, or else you will get a generator, which is practically the same as iterator in this case).

I mean, there are some more exotic ways to do it.
Any iterable object can be expanded with the asterisk operator, we all do it every day:
self.increase(self, *increments):
    self.set_values(*map(sum, zip(self.values, increments)))

def set_values(self, *values):
    self.values = values
This operator expands the iterable into a list of positional arguments, then set_values method uses another asterisk to collect all positional arguments back into a list.

But that extra method is unnecessary, because it is possible to just use asterisk inside square brackets:
def increase(self, *increments):
    self.values = [*map(sum, zip(self.values, increments))]
Here, a list is created by the "square brackets statement" which gets its values from the expanded iterable. It is also possible to create a set using curly braces:
def increase(self, *increments):
    self.values = {*map(sum, zip(self.values, increments))}
But if you need a tuple you need to do something more then just replacing braces with parens:
def increase(self, *increments):
    self.values = (*map(sum, zip(self.values, increments)),)
That comma after the map function call converts "just parens" into a tuple. Actually, there is no need in parens at all:
def increase(self, *increments):
    self.values = *map(sum, zip(self.values, increments)),
Is this exotic enough?

Well, no. We can expand our iterable directly into a sequence, which is usually done like this:
a, b, c = range(3)
(a, b, c) = range(3)
[a, b, c] = range(3)
This it the simplest form of the feature, and here is more powerful one:
def increase(self, *increments):
    [*self.values] = map(sum, zip(self.values, increments))
Just like with arbitrary positional parameters, one sequence item, self.values, captures many items from the source iterable into a single list. So, the most exotic way to expand an iterator into a tuple is this:
def increase(self, *increments):
    *self.values, = map(sum, zip(self.values, increments))
You don't need parens, remember? self.values is still a list.

The most pythonic way is the first one, when the map function call is directly wrapped by the tuple. Some may prefer list comprehension, though.

I hope there are no more simple (one-line) ways to solve the problem. But if you know one, please, do me a favor and write a comment.

Комментариев нет:

Отправить комментарий