22

I have a list of tuples

servers = [('server1', 80 , 1, 2), ('server2', 443, 3, 4)]

I want to create a new list that only has the first two fields as in:

 [('server1', 80), ('server2', 443)]

but I cannot see how to craft a list comprehension for more than one element.

hosts = [x[0] for x in servers]  # this works to give me ['server1', server2']

hostswithports = [x[0], x[1] for x in servers] # this does not work

I prefer to learn the pythonic way vs using a loop - what am I doing wrong?

Bill
  • 618
  • 6
  • 15

4 Answers4

42

You can use extended iterable unpacking.

>>> servers = [('server1', 80 , 1, 2), ('server2', 443, 3, 4)]
>>> [(server, port) for server, port, *_ in servers]
[('server1', 80), ('server2', 443)]

Using _ as a throwaway placeholder-name is a common convention.

timgeb
  • 76,762
  • 20
  • 123
  • 145
  • 3
    This is the only one that, by only looking at the line in question, I have any idea what is happening. Readability matters: +1 – TemporalWolf Oct 30 '18 at 20:38
  • 2
    ... and for older versions, `[(server, port) for server, port, _, _ in servers]` works just as well. – tobias_k Oct 30 '18 at 20:50
  • 1
    @tobias_k it is worth noting it handles things differently: `a, b, *_` will accept elements with at least two sub-elements. `a, b, _, _` only accepts elements with 4 sub-elements. So they are different in how they handle data that doesn't conform to the example. But with conforming data, they are the same... and in Py2.7 you don't get the choice ;) – TemporalWolf Oct 30 '18 at 21:14
  • I see I was just missing enclosing parentheses but I really like the readability of this approach – Bill Oct 30 '18 at 21:18
  • @TemporalWolf Of course, I thought that was clear, but you are right: OP actually never says anything about the shape of the lists, just that the first two elements are needed, so it might indeed be irregular. – tobias_k Oct 30 '18 at 21:30
18

What you were doing was almost right. You were attempting to translate each tuple in your list into a new tuple. But you forgot to actually declare the tuple. That's what the parentheses are doing:

hosts = [(x[0], x[1]) for x in servers]
Woody1193
  • 7,252
  • 5
  • 40
  • 90
17

Using basic slicing, which has the benefit of not failing if any of your list elements don't have the expected number of sub-elements.

[el[:2] for el in servers]

[('server1', 80), ('server2', 443)]
user3483203
  • 50,081
  • 9
  • 65
  • 94
9

You could use itemgetter:

from operator import itemgetter


servers = [('server1', 80 , 1, 2), ('server2', 443, 3, 4)]

result = list(map(itemgetter(0, 1), servers))

print(result)

Output

[('server1', 80), ('server2', 443)]

A more readable alternative is the following:

from operator import itemgetter

get_server_and_port = itemgetter(0, 1)
servers = [('server1', 80, 1, 2), ('server2', 443, 3, 4)]
result = [get_server_and_port(e) for e in servers]

print(result)  # [('server1', 80), ('server2', 443)]
Dani Mesejo
  • 61,499
  • 6
  • 49
  • 76
  • Instead of a list generator based on “identity” transformation you could simply call the list constructor on the iterable `map` object: `result = list(map(itemgetter(0, 1), servers))`. Otherwise +1 for the smart use of `itemgetter`. – David Foerster Oct 31 '18 at 09:39
  • 2
    An minor alternative would be to pass a slice object to itemgetter: `itemgetter(slice(n))` is equivalent to `itemgetter(0, 1, 2, ..., n-1)` in case you want to select a non-trivially sized index range. You can call the slice constructor with 2 or 3 arguments for ranges starting after the first element or ranges that skip indices respectively like with subscript slice expressions (`foo[start : stop : step]`). – David Foerster Oct 31 '18 at 09:42