15
\$\begingroup\$

I have a list of lists which represents the options I can chose from.

I have a sequence of indices which represent which option lists I want to take elements from, and in which order.

E.g. if I have

choices = [
    [ 1, 2, 3 ],
    [ 4, 5, 6 ],
    [ 7, 8, 9 ]
]

sequence = [ 2, 0, 1, 1 ]

I want my output to be

[7, 8, 9, 1, 2, 3, 4, 5, 6, 4, 5, 6]
#index 2  index 0  index 1  index 1

I have found three possible solutions:

choice = sum( ( choices[i] for i in sequence ), [] )

choice = reduce( operator.add, ( choices[i] for i in sequence ) )

choice = [ element for i in sequence for element in choices[i] ]

I would like to know which of these do people find the most pythonic and to know if there are any other elegant solutions.

\$\endgroup\$
5
  • \$\begingroup\$ Per the style guide, consider removing the extraneous whitespace inside brackets. \$\endgroup\$
    – jonrsharpe
    Commented Jul 10, 2014 at 14:08
  • \$\begingroup\$ @jonrsharpe this per the company style guide, unfortunately. \$\endgroup\$
    – Griffin
    Commented Jul 10, 2014 at 14:10
  • \$\begingroup\$ Ah, I see - it's probably worth mentioning that you're following such a thing! Do you need the whole list choice, or would an iterable do? What do you do with it next? \$\endgroup\$
    – jonrsharpe
    Commented Jul 10, 2014 at 14:12
  • \$\begingroup\$ @jonrsharpe an iterable is fine. \$\endgroup\$
    – Griffin
    Commented Jul 10, 2014 at 14:13
  • \$\begingroup\$ assuming that they all do the same thing, this may be a matter of opinion, or this whole question could fall under "code not written (yet)" \$\endgroup\$
    – Malachi
    Commented Jul 10, 2014 at 14:30

4 Answers 4

19
\$\begingroup\$

Unless I actually needed the whole list at once, I would probably use itertools for this:

from itertools import chain

choice = chain.from_iterable(choices[i] for i in sequence)

If you do need the list, you can still use this with an explicit conversion:

choice = list(chain.from_iterable(choices[i] for i in sequence))

Note: this fits pretty close to Nobody's suggestion - here chain.from_iterable is flatten and the generator expression is making the sample.

\$\endgroup\$
0
6
\$\begingroup\$

I find neither of your solutions very pythonic.

Using sum to concatenate lists seems like a hack.

Using reduce looks better but still has the problem of making the selection too verbose to read it fast.

The last one is far too much. Understanding what it does takes way too long.

Actually the problem you are solving is two subproblems:

  1. taking a list of samples from the choices collection
  2. flattening the resulting list

So the code should be more like

flatten(samples(choices, sequence))

I am no regular user of python so I can't tell you much about library implementations of flatten and samples but I am quite sure that there are many implementations around on the net (not necessarily under these names though).

\$\endgroup\$
3
  • \$\begingroup\$ Also, although not deprecated per se, reduce has moved from the built-ins to functools in 3.x, which could be interpreted as suggesting its use isn't Pythonic. \$\endgroup\$
    – jonrsharpe
    Commented Jul 10, 2014 at 14:35
  • \$\begingroup\$ @jonrsharpe: Given that "Pythonic" often means what GvR thinks, and he doesn't like map/reduce/filter, it's safe to say they aren't pythonic. Nothing that relies on anonymous functions is, really, just look at that syntax. \$\endgroup\$
    – Phoshi
    Commented Jul 10, 2014 at 15:43
  • 1
    \$\begingroup\$ Guido doesn't dislike map and filter, he likes them so much that he created a special type of expression that combines them to form generator expressions and comprehensions of various types: list, set, dict; and he's on record as saying most people use reduce when they mean to use sum, and I'd agree. \$\endgroup\$
    – Aaron Hall
    Commented Jul 10, 2014 at 17:57
2
\$\begingroup\$

I agree with @jonrsharpe: Use itertools.chain().

The problem with the first two solutions…

choice = sum( ( choices[i] for i in sequence ), [] )
choice = reduce( operator.add, ( choices[i] for i in sequence ) )

is that adding lists does not scale well, if sequence is long.

The third solution…

choice = [ element for i in sequence for element in choices[i] ]

doesn't suffer from repeated copying like the first two. However, it's less readable than the itertools.chain version.

\$\endgroup\$
0
1
\$\begingroup\$

You want something to be pythonic? It seems you would like to have a one liner. Simple AND elegant is more pythonic in my opinion.

The "most" pythonic in my opinion would be this:

choice = []
for choice_index in sequence:
    if choice_index >= len(choices):
        break
    choice += choices[choice_index]

For a one-liner? I will wrap it in a function and use that like this for repetitive work.

def generate_choice_list(choices, sequence):
    choice = []
    for choice_index in sequence:
        if choice_index >= len(choices):
            return []
        choice += choices[choice_index]
    return choice

This has the added advantage of eliminating wrong entries with the sequence list, like returning empty list for incorrect sequence list.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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