Often I need to perform multiple assignment from a sequence
(e.g. from the result of a split()
) but with some semantics
that, AFAIK, are not available in the language:
Assign N first elements and silently ignore the rest (like Perl's list assignment).
>>> a, b = 'foo bar is here'.split() >>> a, b ('foo', 'bar') >>>
Assign the remaning items to the last variable in the variable list:
>>> a, b, c = 'foo bar is here'.split() >>> a, b, c ('foo', 'bar', ['is', 'here']) >>>
If the sequence doesn't have enough values, set the variables to a default value (
None
or any other, again similar to Perl):>>> a, b, c, d, e = 'foo bar is here'.split() >>> a, b, c, d, e ('foo', 'bar', 'is', 'here', None) >>>
I have designed an iterator function to help me with this, but I'm concerned about my solution to this problem from the point of view of pythonic adherence:
Usefulness: Using an iterator for this kind of job is the right option, or there is something more appropriate? Or even worse: my need to do this kind of assignment in Python is not favoring other language features?
Clearness: The implementation is solid and clear enough? Or is there some other way to implement this that would be cleaner?
In other words, this solution is better discarded or can it be improved?
from itertools import *
def iterassign(iterable, *, count, use_default=False, default=None, tail=False):
"""Iterattor for multiple assignment.
:param iterable: The iterable.
:param int count: The maximum count of iterations.
:param bool use_default: If set, and iterable ends before max_count,
continue iteration using :param:`default`.
:param default: Default value to use when :param:`use_default` is
set to ``True``.
"""
it = iter(iterable)
for idx, element in enumerate(chain(islice(it, 0, count), repeat(default))):
if idx >= count: break
yield element
else:
raise StopIteration()
if tail:
yield list(it)
Usage examples:
>>> ret = 'foo bar is here'.split()
>>> a, b = iterassign(ret, count=2)
>>> a, b
('foo', 'bar')
>>> a, b, c = iterassign(ret, count=2, tail=True)
>>> a, b, c
('foo', 'bar', ['is', 'here'])
>>> a, b, c, d, e = iterassign(ret, count=5, use_default=True)
('foo', 'bar', 'is', 'here', None)
>>>
1 Answer 1
I'll look at each of your three examples individually.
Ignoring remaining values
In Python 2:
>>> a, b = 'foo bar is here'.split()[:2] >>> a, b ('foo', 'bar')
In Python 3 (
_
used by convention, double underscore,__
, is also appropriate):>>> a, b, *_ = 'foo bar is here'.split() >>> a, b ('foo', 'bar')
When ignoring remaining values, using the built-in language features would be the most Pythonic approach. I think they're clear enough that they do not need to be re-invented.
Capture remaining values
In Python 2:
>>> split_string = 'foo bar is here'.split() >>> a, b, c = split_string[2:] + [split_string[2:]] >>> a, b, c ('is', 'here', ['is', 'here'])
In Python 3:
>>> a, b, *c = 'foo bar is here'.split() >>> a, b, c ('is', 'here', ['is', 'here'])
In Python 3, the
*
feature is again more Pythonic than your approach. In Python 2, that approach won't work if you're dealing with a non-indexable type.Fill remaining values with
None
. Here is a somewhat similar question. That question and the Python 2 solution only work for lists.Python 2:
>>> split_string = 'foo bar is here'.split() >>> a, b, c, d, e = split_string + [None] * (5-len(split_string)) >>> a, b, c, d, e ('foo', 'bar', 'is', 'here', None)
Python 3.5 (this is not very clear in my opinion):
>>> a, b, c, d, e, *_ = *'foo bar is here'.split(), *[None]*5 >>> a, b, c, d, e ('foo', 'bar', 'is', 'here', None)
There isn't really a built-in language construct for this so this case might warrant re-inventing. However, this kind of
None
-filling seems rare and may not necessarily be clear to other Python programmers.
A review of your actual implementation
Don't use import *
in Python. In Python we value our namespaces highly because namespaces allow us to see what variables came from what modules.
from itertools import *
Do this instead:
from itertools import chain, islice, repeat
You didn't use use_default
in your code at all. It seems like you could use another islice
instead of an enumerate
to restructure your code like this:
def iterassign(iterable, *, count, default=None, tail=False):
it = iter(iterable)
for x in islice(chain(islice(it, 0, count), repeat(default)), 0, count):
yield x
if tail:
yield list(it)
Iterators are the right solution to this problem (assuming we've decided it's one that needs solving).
If we want to rewrite this to only handle case 3 (because we've already decided we can just use Python's built-in language features for case 1 and 2, we could rewrite like this:
def fill_remaining(iterable, *, length, default=None):
"""Return iterable padded to at least ``length`` long."""
return islice(chain(iterable, repeat(default)), 0, length)
Which would work like this:
>>> a, b, c, d, e = fill_remaining(ret, length=5)
>>> a, b, c, d, e
('foo', 'bar', 'is', 'here', None)
Explore related questions
See similar questions with these tags.
a, b, *c
, where the c will swallow the rest of the list. \$\endgroup\$a, b, *_
for the first form. You can't have default values out-of-the-box though. \$\endgroup\$