2

Given the following example code:

def myfunc(item):
  if item == 2:
    item = 1

mylist = [1,2,3]
for i in mylist:
  myfunc(i)
print(mylist) # output is [1, 2, 3]
# desired output is [1, 1, 3]

I would like to have a function that is called for some or all elements of a list. This function should be able to alter these elements.

What would be the the cleanest solution for this problem?

Fabian Henze
  • 980
  • 1
  • 10
  • 27

7 Answers7

7

If you don't need the list to be modified in-place, you can create a new list with the new values. To this end, your functions should simply return the new value:

def myfunc(item):
    if item == 2:
        return 1
    return item

Then you can use map() or a list comprehension to construct the new list:

mylist = [1, 2, 3]
print map(myfunc, mylist)
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 1
    While I was aiming for in-place editing, I like your map() approach. If no one comes up with something better, I will go with this :) – Fabian Henze Feb 29 '12 at 00:44
4

If you want to modify in-place your list you need to work with its indexes.

Something like:

for i,elm in enumerate(mylist):
    mylist[i] = myfunc(elm)

With a myfunc that looks like:

def myfunc(item):
    if item == 2:
        return 1
    else:
        return item

With this function you wouldn't actually need a function at all, but I hope you got my point :)


Expanding a bit Sven's comment, you could use list-slicing + map():

mylist[:] = map(myfunc, mylist)

I actually prefer this one than to use enumerate(). And since you seem to be on Python 3 this wouldn't even build a temporary list.

A generator-expression would work too! I wouldn't use it in some real code, because to me looks less readable, but it might me an interesting example:

>>> mylist = [0, 1, 2, 3, 4]
>>> mylist[:] = (x == 2 and 1 or x for x in mylist)
>>> mylist
[0, 1, 1, 3, 4]

Edit: What I thought about the non building of a temp list (for map() and for the generator-expressions) turned out to be false: see Sven's comment or his explanation from this other answer.

Community
  • 1
  • 1
Rik Poggi
  • 28,332
  • 6
  • 65
  • 82
  • Using indices to modify a list in-place is not strictly necessary -- `mylist[:] = map(myfunc, mylist)` would also work. (It would temporarily create a new list, though.) – Sven Marnach Feb 29 '12 at 10:51
  • @SvenMarnach: It's not imperative to use `enumerate()`, but `mylist[:]` is in a way working with mylist indexes, don't you think? – Rik Poggi Feb 29 '12 at 11:03
  • @Sven: Now that I think of it the OP seems to be on Python 3, so `map()` wouldn't even build a temp list :) I really like this `map()` solution. – Rik Poggi Feb 29 '12 at 13:53
  • Even on Python 3, it would somehow build a temporary list, at least when using CPython. This can only be seen by examining the CPython source code -- see this [Python 2.x answer](http://stackoverflow.com/questions/4948293/python-slice-assignment-memory-usage/4948508#4948508), which is still valid for Python 3.x. – Sven Marnach Feb 29 '12 at 14:37
  • @Sven: I would've never guessed that, thanks! I edited my answer. – Rik Poggi Feb 29 '12 at 15:06
3

As you've written myfunc, working on individual items, it cannot modify the list in place. This is a good thing.

myfunc receives the name item bound to some value. You can rebind the name item to a new value, but that of course doesn't affect any other bindings to the same value. All a list is is an ordered sequence of bindings to values, so to change what it contains you have to rebind some of the indexes in the list to new values (or change the values themselves, but numbers can't be changed; there's no way to change the number 1 into the number 2, you can only rebind something that referred to the number 1 to refer to the number 2 instead). With no reference to the list, there's no way for myfunc to change the bindings in the list.

But myfunc shouldn't have to care about the list. It works on items, and it doesn't care whether you got those items from a list, or a dictionary, or read them from a file, or whatever. This is what I said is a good thing about myfunc not being able to modify your list; otherwise you'd have to rewrite myfunc for every different context your items might appear in.

Instead, just have myfunc return the new value:

def myfunc(item):
  if item == 2:
    item = 1
  return item

Now the code that calls myfunc can do what you want. This code is aware that the items are being fetched from a list, and that you're trying to transform the list in place by replacing each item with whatever myfunc does to it. So this is the place you should implement that intention; trying to push it down into myfunc is not the way to keep complexity away from myfunc.

mylist = [1,2,3]
for idx, item in enumerate(mylist):
    mylist[idx] = myfunc(item)

If you find you're repeating this pattern all the time for lots of different functions, you can abstract that out into another function:

def inplace_map(items, func):
    for i, item in enumerate(items):
        items[i] = func(item)

Now you've defined the transformation logic once, and each different item transformation once (and you can re-use the item transformation functions in other contexts than list transformations), and for any given place where you want to transform the items in a list in-place through a function all you have to do is call inplace_map, which very clearly states what you're doing.

Ben
  • 68,572
  • 20
  • 126
  • 174
2

Try the python list comprehension. It's a favorite of pythonistas!

[myfunc(x) for x in mylist]

Prepend with mylist = to overwrite the old list with the new one.

Prashant Kumar
  • 20,069
  • 14
  • 47
  • 63
  • In my opinion this is usually a better idea than what the OP is trying to do, but it's not an answer to the OP's question, which was about modifying a list in place.. – Ben Feb 29 '12 at 01:09
  • As per the comment the OP made on Sven's answer, the OP is willing to sacrifice in-place modification. – Prashant Kumar Feb 29 '12 at 01:14
1

Go back and read what datatypes are what.

  1. Your function never returned anything (learn about scopes)
  2. You sent a list, not a single item to the function (datatypes)

Code:

def myfunc(itemlist):
    for i,item in enumerate(itemlist):
        if item == 2:
            itemlist[i] = 1
    return itemlist

mylist = [1,2,3]
mylist = myfunc(mylist)
print(mylist)

output is [1, 1, 3]
desired output is [1, 1, 3]

Prashant Kumar
  • 20,069
  • 14
  • 47
  • 63
platinummonkey
  • 808
  • 8
  • 19
  • 1. I know about scopes. I was aiming for in-place editing of the list. I also want to keep the complexity away from `myfunc()`, because I would like to have multiple functions like this and would rather not duplicate the code. 2.oops, that is actually a typo. sorry – Fabian Henze Feb 29 '12 at 00:38
1

You can not alter the number 2 - it is immutable, ergo it will forever be the number 2.

For in-place editing what you really want is to alter the object mylist itself, not alter the elements, and to do this you would need to pass a reference to mylist into your function.

def myfunc(mylist, myindex):
  if mylist[myindex] == 2:
    mylist[myindex] = 1

Alternatively, the cleanest solution is just to build a new list using a list comprehension.

def myfunc(item):
  if item == 2:
    item = 1
  return item

mynewlist = [myfunc(i) for i in mylist]
wim
  • 338,267
  • 99
  • 616
  • 750
1

You can modify a list in place by using the index of the element:

>>> l=[1,2,3]
>>> def f(l):
...    for i,v in enumerate(l):
...       if v==2: l[i]=1
... 
>>> f(l)
>>> l
[1, 1, 3]

If you in place mod is more than one element, use a slice on the LH:

>>> def f2(l):
...    for i,v in enumerate(l):
...       if v==3: l[:]=[-1,-2,-3]
... 
>>> f2(l)
>>> l
[-1, -2, -3]
the wolf
  • 34,510
  • 13
  • 53
  • 71