28

I have a 2D list that looks like this:

table = [['donkey', '2', '1', '0'], ['goat', '5', '3', '2']]

I want to change the last three elements to integers, but the code below feels very ugly:

for row in table:
    for i in range(len(row)-1):
        row[i+1] = int(row[i+1])

But I'd rather have something that looks like:

for row in table:
    for col in row[1:]:
        col = int(col)

I think there should be a way to write the code above, but the slice creates an iterator/new list that's separate from the original, so the references don't carry over.

Is there some way to get a more Pythonic solution?

MERose
  • 4,048
  • 7
  • 53
  • 79
Conrad.Dean
  • 4,341
  • 3
  • 32
  • 41

7 Answers7

20
for row in table:
    row[1:] = [int(c) for c in row[1:]]

Does above look more pythonic?

Shekhar
  • 7,095
  • 4
  • 40
  • 45
  • 13
    While this is technically an in-place operation, **two** extra lists are being created inside the loop. The first is created by the `row[1:]` slice (argument to `map`). The second is created by the use of a list comprehension. – Wesley Apr 15 '11 at 19:15
  • This is called "list comprehensions" [https://docs.python.org/2/tutorial/datastructures.html#list-comprehensions] – Kalle Richter Jun 12 '14 at 19:10
13

Try:

>>> for row in table:
...     row[1:]=map(int,row[1:])
... 
>>> table
[['donkey', 2, 1, 0], ['goat', 5, 3, 2]]

AFAIK, assigning to a list slice forces the operation to be done in place instead of creating a new list.

MAK
  • 26,140
  • 11
  • 55
  • 86
  • 1
    Is it still considered pythonic to use map over a comprehension? – Nicholas Mancuso Apr 15 '11 at 04:31
  • @Nicholas Mancuso: I think both are similar in terms of performance. I would argue `map` is not less readable (especially if you familiar with functional programming). So, I guess it comes down to personal preference. I use both, whichever one looks simpler in that particular context is best IMHO. Although AFAIK, Guido thinks list comprehensions are better. But I don't see any objective reason for that. – MAK Apr 15 '11 at 04:35
  • I like map, but always get docked points from PyLint when I use it compared to the comprehensions, so I just stopped using it. Objectively there are no benefits of one over the other, but who can deny the Benevolent Dictator For Life? – Nicholas Mancuso Apr 15 '11 at 04:38
  • 2
    @Nicholas Mancuso `map` is completely pythonic. What's not pythonic is the tangled mess that you often get when you try to jam too much into a `lambda` to use with it. – Michael J. Barber Apr 15 '11 at 04:39
  • map used to be faster when used with builtin functions (such as int()), but I think LC's closed the gap a while back. For less experienced programmers the LC is definitely easier to read/understand – John La Rooy Apr 15 '11 at 04:55
  • I was trying to get this in one line. Tried `map(lambda row: map(int, row[1:]), table)`, but how can I assign it back to `table`? – ajmartin Apr 15 '11 at 05:23
  • @ajmartin: just do `table[::]=` to whatever expression you use. this makes it do the operation in place. – MAK Apr 15 '11 at 05:31
  • 1
    @MAK: why the extra `:` in your last comment and in the second line of your answer? it is not needed, and is noise. – gurney alex Apr 15 '11 at 06:42
  • 2
    While this is technically an in-place operation, **two** extra lists are being created inside the loop. The first is created by the `row[1:]` slice (argument to `map`). The second is created by `map`. The space usage is *3n-1* for a `row` of length *n*. As *n* increases, the use of a simple inner loop becomes more space-efficient. – Wesley Apr 15 '11 at 19:11
8

I like Shekhar answer a lot.

As a general rule, when writing Python code, if you find yourself writing for i in range(len(somelist)), you're doing it wrong:

  • try enumerate if you have a single list
  • try zip or itertools.izip if you have 2 or more lists you want to iterate on in parallel

In your case, the first column is different so you cannot elegantly use enumerate:

for row in table:
    for i, val in enumerate(row):
        if i == 0: continue
        row[i] = int(val)
gurney alex
  • 13,247
  • 4
  • 43
  • 57
  • 7
    "if you find yourself writing `for i in range(len(somelist))`, you're doing something wrong" --- This is probably the best advice that someone can give to a person learning the pythonic idioms. Python's strength is what it gives you in readability, and when someone transitions from a lang like Java where this construct is typical they'll really miss out on the true advantages to working in Python. +1 – Conrad.Dean Jan 22 '12 at 19:11
  • You could improve it a little with `for i, val in enumerate(row[1:]):` and thus getting rid of the `if i == 0` – erickrf Nov 13 '12 at 00:56
  • @erickrf this creates a shallow copy of the row, and afterwards you need to use row[i+1] = int(val). Not sure if this improves a lot. – gurney alex Nov 13 '12 at 08:41
  • The correctness of "if you find yourself writing `for i in range(len(somelist))`, you're doing something wrong" is questionable. Enumerate is not only slower, but may also be considered less readable. See [this answer](https://stackoverflow.com/a/11990189/1059398) for reference. – virtualxtc Aug 14 '18 at 00:40
3

Your "ugly" code can be improved just by calling range with two arguments:

for row in table:
    for i in range(1, len(row)):
        row[i] = int(row[i])

This is probably the best you can do if you insist on changing the items in place without allocating new temporary lists (either by using a list comprehension, map, and/or slicing). See Is there an in-place equivalent to 'map' in python?

Although I don't recommend it, you can also make this code more general by introducing your own in-place map function:

def inplacemap(f, items, start=0, end=None):
    """Applies ``f`` to each item in the iterable ``items`` between the range
    ``start`` and ``end``."""
    # If end was not specified, make it the length of the iterable
    # We avoid setting end in the parameter list to force it to be evaluated on
    # each invocation
    if end is None:
        end = len(items)
    for i in range(start, end):
        items[i] = f(items[i])

for row in table:
    inplacemap(int, row, 1)

Personally, I find this less Pythonic. There is preferably only one obvious way to do it, and this isn't it.

Community
  • 1
  • 1
Wesley
  • 10,652
  • 4
  • 37
  • 52
1

Use list comprehensions:

table = [row[0] + [int(col) for col in row[1:]] for row in table]
Nicholas Mancuso
  • 11,599
  • 6
  • 45
  • 47
  • +1 I had no idea you could chain list comprehensions together like that! I probably won't be using something this nested because I work with a lot of people that might find this unreadable, but I'll definitely keep this in mind for personal projects. Thanks! – Conrad.Dean Apr 15 '11 at 15:41
0

This will work:

table = [[row[0]] + [int(v) for v in row[1:]] for row in table]

However you might want to think about doing the conversion at the point where the list is first created.

Keith
  • 42,110
  • 11
  • 57
  • 76
  • You're right. treating my table with a special sub-table inside of it has been pretty cumbersome in all my other algorithms so I've got a raw data table now. – Conrad.Dean Apr 15 '11 at 15:45
-1

This accomplishes what you are looking for. It is a readable solution. You can go for similar one using listcomp too.

>>> for row in table:
...     for i, elem in enumerate(row):
...             try:
...                     int(elem)
...             except ValueError:
...                     pass
...             else:
...                     row[i] = int(elem)
... 
Senthil Kumaran
  • 54,681
  • 14
  • 94
  • 131
  • The only answer with proper validation in place. Though performing int cast twice is wasteful and defeats the idea behind try-except block – Muposat Jul 08 '16 at 15:28