3

I'm trying to apply a function to every element of a list containing arbitrary sub-levels of sublists. Like so.

a = [1,2,3]
b = [[1,2,3],[4,5,6]]
c = [[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]

function = lambda x: x+1
def apply(iterable,f): 
    # do stuff here
print(apply(a,function)) # [2,3,4]
print(apply(b,function)) # [[2,3,4],[5,6,7]]
print(apply(c,function)) # [[[2,3,4],[5,6,7]],[[8,9,10],[11,12,13]]]

basically i can't find a way to write the apply function. I tried with numpy, but that's not the solution, of course, because the contents of the list could also be strings, objects ...

Giuppox
  • 1,393
  • 9
  • 35

4 Answers4

6

Sounds like recursion should be able to solve that:

a = [1,2,3]
b = [[1,2,3], [4,5,6]]
c = [[[1,2,3], [4,5,6]], [[7,8,9], [10,11,12]]]


f = lambda x : x+1

def apply(iterable, f): 
    # suggestion by Jérôme:
    # from collections.abc import Iterable and use
    # isinstance(iterable, collections.abc.Iterable) so it works for tuples etc. 
    if isinstance(iterable, list):
        # apply function to each element
        return [apply(w, f) for w in iterable]
    else:
        return f(iterable)

print(apply(a, f)) # [2,3,4]
print(apply(b, f)) # [[2,3,4],[5,6,7]]
print(apply(c, f)) # [[[2,3,4],[5,6,7]],[[8,9,10],[11,12,13]]]
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • This would not work in general iterable.. – adir abargil Dec 06 '20 at 21:52
  • 2
    This. Except I'd test isinstance(iterable, collections.abc.Iterable) – Jérôme Dec 06 '20 at 21:52
  • hi, thanks for answering my question. i had also thought on this solution, but found out on online forums that it not practical to use function recursion, and that sometimes causes errors... (RecursionError?) – Giuppox Dec 06 '20 at 21:56
  • @Jer good idea - but that would take an import. To be more pythonic the try:except: ones are also nice – Patrick Artner Dec 06 '20 at 21:56
  • @Giuppox there are cases where recursion is fine. Unless you input a roughtly 999+ times levels of list with inner lists, this wont get you into problems ... unless it does and you need to redesign. You get recursion errors if you do not handle your basecases correctly OR if your input leads to too many recursions. for this application recursion is just fine – Patrick Artner Dec 06 '20 at 21:58
  • @Moosefeather i understand, also found a pretty good explanation here https://stackoverflow.com/questions/3323001/what-is-the-maximum-recursion-depth-in-python-and-how-to-increase-it – Giuppox Dec 06 '20 at 22:00
  • @Giuppox As a side note, you can always unwrap recursive functions using loops but it will usually be a lot messier. – ssp Dec 06 '20 at 22:02
  • 2
    @adir while your statement is correct - the example iterable used in the question is a list. For that it works fine - it would not work for a string or dict or tuple etc. but for those @ Jérôme suggestion to use `isinstance(iterable, collections.abc.Iterable)` would work. – Patrick Artner Dec 06 '20 at 22:02
  • 1
    @PatrickArtner i think that something like `type(iterable) in [list,str,tuple,set]` could also be good... – Giuppox Dec 06 '20 at 22:06
  • 1
    As a general rule, don't test "type" unless you have a good reason. But you could use something like `isinstance(iterable, (list, str, tuple, set))`, passing a list to `isinstance`. I don't see the point here, I'd rather use `Iterable`, but there are cases where it is useful. – Jérôme Dec 07 '20 at 07:52
  • @PatrickArtner what about something like this (at the place of the first return) `return type(iterable)([apply(w,f) for w in iterable])`. isn't it better to make the function return an object of the same type? – Giuppox Dec 08 '20 at 12:18
  • @guippox not needed - my code handles `isinstance(..., list)` and it returns a list. I opt for keeping this simple. You can make any code more complex and then you would need to also care for other return types - this goes for using abc.Iterable as well. Your question was about lists and sublists, the given data was lists. The output was list. This handles lists just fine. And lists is what most will use when stumbling over this. If you care about strings as iterables you can't add 1 and would need some other function to apply. If you care about tuples/dicts/sets you can take this and adapt it – Patrick Artner Dec 08 '20 at 12:22
1

Here is one way to do it. Note that strings are also iterable so depending on your use case you might need to add more checking.

def apply(iterable, f):
    try:
        iterator = iter(iterable)
        for i in iterator:
            apply(i, f)
    except Exception as e:
        f(iterable)

c = [[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]]
apply(c, print)
questionerofdy
  • 513
  • 3
  • 7
  • hi, i had also thought on this solution, but found out on online forums that it not practical to use function recursion, and that sometimes causes errors... – Giuppox Dec 06 '20 at 21:54
1

Give this recursion a shot:

def apply(it, func):
    try:
        return [apply(e) for e in it]
    except TypeError:
        return func(it)

Note that this will iterate over any iterable unless you specify otherwise, for example you can check in the beginning if it is a dict and just apply it the func on it.. add

if isinstance(it,dict):
    func(it)
adir abargil
  • 5,495
  • 3
  • 19
  • 29
1

If all your nested lists are the same shape you should be able to do this with numpy:

import numpy as np

ary = np.array([['a', 'b'], ['c', 'd'], ['e', 'f']])
res = np.vectorize(lambda c: c + ':)')(ary)
print(res)
# [['a:)' 'b:)']
#  ['c:)' 'd:)']
#  ['e:)' 'f:)']]
ssp
  • 1,666
  • 11
  • 15