7
\$\begingroup\$

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:

  1. 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')
    >>>
    
  2. 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'])
    >>>
    
  3. 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)
>>>
\$\endgroup\$
  • 2
    \$\begingroup\$ In Python 3, I think you can use : a, b, *c, where the c will swallow the rest of the list. \$\endgroup\$ – holroy Nov 14 '15 at 18:56
  • \$\begingroup\$ @holroy And thus use a, b, *_ for the first form. You can't have default values out-of-the-box though. \$\endgroup\$ – 301_Moved_Permanently Nov 14 '15 at 19:54
9
\$\begingroup\$

I'll look at each of your three examples individually.

  1. 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.

  2. 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.

  3. 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)
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.