map with function for each element?

Viewed 406

Very often I process single elements of tuples like this:

size, duration, name = some_external_function()
size = int(size)
duration = float(duration)
name = name.strip().lower()

If some_external_function would return some equally typed tuple I could use map in order to have a (more functional) closed expression:

size, duration, name = map(magic, some_external_function())

Is there something like an element wise map? Something I could run like this:

size, duration, name = map2((int, float, strip), some_external_function())

Update: I know I can use comprehension together with zip, e.g.

size, duration, name = [f(v) for f, v in zip(
   (int, float, str.strip), some_external_function())]

-- I'm looking for a 'pythonic' (best: built-in) solution!

To the Python developers:

What about

(size)int, (duration)float, (name)str.strip = some_external_function()

? If I see this in any upcoming Python version, I'll send you a beer :)

6 Answers

Quite simply: use a function and args unpacking...

def transform(size, duration, name):
    return int(size), float(duration), name.strip().lower()

# if you don't know what the `*` does then follow the link above...    
size, name, duration = transform(*some_external_function())

Dead simple, perfectly readable and testable.

Map does not really apply here. It comes in handy when you want to apply a simple function over all elements of a list, such as map(float, list_ints).

There isn't one explicit built-in function to do this. However, a way to simplify your approach and avoid n separate calls to the functions to be applied, could be to define an iterable containing the functions, and apply them to the returned non-unpacked tuple from the function on a generator comprehension and then unpack them:

funcs = int, float, lambda x: x.strip().lower()
t = 1., 2, 'Some String  ' # example returned tuple

size, duration, name = (f(i) for f,i in zip(funcs, t))

Or perhaps a little cleaner:

def transform(t, funcs):
    return (f(i) for f,i in zip(funcs, t))

size, duration, name = transform(t, funcs)

size
# 1
duration
# 2.0
name
# 'some string'
class SomeExternalData:
    def __init__(self, size: int, duration: float, name: str):
        self.size = size
        self.duration = duration
        self.name = name.strip().lower()

    @classmethod
    def from_strings(cls, size, duration, name):
        return cls(int(size), float(duration), name)


data = SomeExternalData.from_strings(*some_external_function())

It's far from a one-liner, but it's the most declarative, readable, reusable and maintainable approach to this problem IMO. Model your data explicitly instead of treating individual values ad hoc.

AFAIK there is no built-in solution so we can write generic function ourselves and reuse it afterwards

def map2(functions, arguments):  # or some other name
    return (function(argument) for function, argument in zip(functions, arguments))  # we can also return `tuple` here for example

The possible problem can be that number of arguments can be less than number of functions or vice versa, but in your case it shouldn't be a problem. After that

size, duration, name = map2((int, float, str.strip), some_external_function())

We can go further with functools.partial and give a name to our "transformer" like

from functools import partial
...
transform = partial(map2, (int, float, str.strip))

and reuse it in other places as well.

Based on Bruno's transform, which I think is the best answer to the problem, I wanted to see if I could make a generic transform function that did not need a hardcoded set of formatters, but could take any number of elements, given a matching number of formatters.

(This is really overkill, unless you need a large number of such magic mappers or if you need to generate them dynamically.)

Here I am using Python 3.6's guaranteed dictionary order to "unpack" the formatters in their declared order and separate them from inputs variadic.

def transform(*inputs, **tranformer):
    return [f(val) for val, f in zip(inputs, tranformer.values())]

size, duration, name = transform(*some_external_function(), f1=int, f2=float, f3=str.lower)

And to make the process even more generic and allow predefined transform functions you can use operator.partial.

from functools import partial

def prep(f_tranformer, *format_funcs):
    formatters = {"f%d"%ix : func for ix, func in enumerate(format_funcs)} 
    return partial(transform, **formatters)


transform2 = prep(transform, int, float, str.lower)

which you can use as:

size, duration, name = transform2(*some_external_function())

I'll second bruno's answer as being my preferred choice. I guess it will depend on how often you are calling this function will determine how much value it is in refactoring such a hindrance. If you were going to be calling that external function multiple times, you could also consider decorating it:

from functools import wraps

def type_wrangler(func):
    def wrangler():
        n,s,d = func()
        return str(n), int(s), float(d)
    return wrangler


def external_func():
    return 'a_name', '10', '5.6'

f = type_wrangler(external_func)

print(f())
Related