4

I have a 3 element python tuple that I'm trying to sort or re-arrange using the indices of a 3-element list, and I want to know what the most concise way to do this is.

So far I've got:

my_tuple = (10, 20, 30)
new_positions = [2, 0, 1]
my_shuffled_tuple = my_tuple[new_positions[0]], my_tuple[new_positions[1]], my_tuple[new_positions[2]]
# outputs: (30, 10, 20)

I also get the same result if I do:

my_shuffled_tuple = tuple([my_tuple[i] for i in new_positions])

Is there a more concise way to create my_shuffled_tuple?

Bas Swinckels
  • 18,095
  • 3
  • 45
  • 62
tomr_stargazer
  • 199
  • 3
  • 12

3 Answers3

6

One way to do this is with a generator expression as an argument to tuple, which accepts an iterable:

In [1]: my_tuple = (10, 20, 30)
   ...: new_positions = [2, 0, 1]
   ...: 

In [2]: my_shuffled_tuple = tuple(my_tuple[i] for i in new_positions)

In [3]: my_shuffled_tuple
Out[3]: (30, 10, 20)

If speed is an issue and you are working with a large amount of data, you should consider using Numpy. This allows direct indexing with a list or array of indices:

In [4]: import numpy as np

In [5]: my_array = np.array([10, 20, 30])

In [6]: new_positions = [2, 0, 1]  # or new_positions = np.array([2, 0, 1])

In [7]: my_shuffled_array = my_array[new_positions]

In [8]: my_shuffled_array
Out[8]: array([30, 10, 20])
Community
  • 1
  • 1
Bas Swinckels
  • 18,095
  • 3
  • 45
  • 62
  • I'd already used the `tuple([my_tuple[i] for i in new_positions])` syntax in my question. But thanks Bas for the note about numpy! A syntax like `my_array[new_positions]` is along the lines of what I was hoping for in my original question - I'll consider switching my tuple to a numpy array, although speed isn't really an issue in my application. – tomr_stargazer Nov 16 '14 at 19:24
  • 1
    There is a tiny but important difference between your syntax and the one in my answer, it is the two rectangular brackets! In your case, you first construct a list using a **list comprehension**, which is then iterated over to construct the tuple. I use a **generator expression**, which does not make the intermediate list. See the link in my answer for the difference. In this case, it does not matter a lot, but in general it allows efficient 'lazy' operations without making big intermediate lists. E.g. `max(int(line) for line in big_file)` will never have more than one line in memory. – Bas Swinckels Nov 16 '14 at 19:38
  • Oh! You're right -- I didn't catch that and really appreciate the clarification! I will now mark your answer as "accepted" because that distinction would have totally evaded me - thanks much. – tomr_stargazer Nov 16 '14 at 20:03
  • Do note that [generator expressions are often slower than list comprehensions](http://stackoverflow.com/questions/11964130/list-comprehension-vs-generator-expressions-weird-timeit-results) especially for small-sized iterables. The distinction between generator expressions and list comprehensions for purposes of a question like this is *almost* completely irrelevant, and in a lot of cases the generator expression is arguably worse (for example, if you've already made the array `new_positions` like in your example, and it's small, then iterating it with a generator expression is just slower). – ely Nov 16 '14 at 20:47
  • 1
    Now, using the generator expression inside the `tuple` constructor may not suffer from this, since the `tuple` constructor is optimized for this at the C-level. But the bigger point is that bringing in any subtleties about generator expressions vs. comprehensions is over-blown, usually amounting to pre-mature or unnecessary optimization, and sometimes being even worse than that. I've seen code where someone takes pains to make a 2-element generator and manually call `next` twice. That's bad -- even if it had some resource benefit, the readability-for-newcomers penalty is too severe. – ely Nov 16 '14 at 20:51
  • Thanks much prpl for the extra information! I'm fortunate enough that I don't have to worry about performance in this case, and am mainly trying to optimize on conciseness and readability. I will keep the notes on generators versus list-comprehensions in mind. – tomr_stargazer Nov 17 '14 at 02:16
3

You can use operator.itemgetter like this:

from operator import itemgetter

my_tuple = (10, 20, 30)
new_positions = [2, 0, 1]

print itemgetter(*new_positions)(my_tuple)

If you will be accessing the elements of my_tuple (or other things too) in the new ordering a lot, you can save this itemgetter as a helper function:

access_at_2_0_1 = itemgetter(*new_positions)

and then access_at_2_0_1(foo) will be the same as tuple(foo[2], foo[0], foo[1]).

This is very helpful when you are trying to work with an argsort-like operation (where lots of arrays need to be re-accessed in a sort order that comes from sorting some other array). Generally, by that point you should probably be using NumPy arrays, but still this is a handy approach.

Note that as itemgetter relies on the __getitem__ protocol (derp) it is not guaranteed to work with all types of iterables, if that is important.

ely
  • 74,674
  • 34
  • 147
  • 228
2

use a comprehension in a tuple() built-in function (it accept generators)

>>> my_tuple = (10, 20, 30)
>>> new_positions = [2, 0, 1]
>>> tuple(my_tuple[i] for i in new_positions)
(30, 10, 20)
Mazdak
  • 105,000
  • 18
  • 159
  • 188