What is the good python3 equivalent for auto tuple unpacking in lambda?

PythonPython 3.x

Python Problem Overview


Consider the following python2 code

In [5]: points = [ (1,2), (2,3)]

In [6]: min(points, key=lambda (x, y): (x*x + y*y))
Out[6]: (1, 2)

This is not supported in python3 and I have to do the following:

>>> min(points, key=lambda p: p[0]*p[0] + p[1]*p[1])
(1, 2)

This is very ugly. If the lambda was a function, I could do

def some_name_to_think_of(p):
  x, y = p
  return x*x + y*y

Removing this feature in python3 forces the code to either do the ugly way(with magic indexes) or create unnecessary functions(The most bothering part is to think of good names for these unnecessary functions)

I think the feature should be added back at least to lambdas alone. Is there a good alternative?


Update: I am using the following helper extending the idea in the answer

def star(f):
  return lambda args: f(*args)

min(points, key=star(lambda x,y: (x*x + y*y))

Update2: A cleaner version for star

import functools

def star(f):
    @functools.wraps(f)
    def f_inner(args):
        return f(*args)
    return f_inner

Python Solutions


Solution 1 - Python

No, there is no other way. You covered it all. The way to go would be to raise this issue on the Python ideas mailing list, but be prepared to argue a lot over there to gain some traction.

Actually, just not to say "there is no way out", a third way could be to implement one more level of lambda calling just to unfold the parameters - but that would be at once more inefficient and harder to read than your two suggestions:

min(points, key=lambda p: (lambda x,y: (x*x + y*y))(*p))

Python 3.8 update

Since the release of Python 3.8, PEP 572 — assignment expressions — have been available as a tool.

So, if one uses a trick to execute multiple expressions inside a lambda - I usually do that by creating a tuple and just returning the last component of it, it is possible to do the following:

>>> a = lambda p:(x:=p[0], y:=p[1], x ** 2 + y ** 2)[-1]
>>> a((3,4))
25

One should keep in mind that this kind of code will seldom be more readable or practical than having a full function. Still, there are possible uses - if there are various one-liners that would operate on this point, it could be worth to have a namedtuple, and use the assignment expression to effectively "cast" the incoming sequence to the namedtuple:

>>> from collections import namedtuple
>>> point = namedtuple("point", "x y")
>>> b = lambda s: (p:=point(*s), p.x ** 2 + p.y ** 2)[-1]

Solution 2 - Python

According to http://www.python.org/dev/peps/pep-3113/ tuple unpacking are gone, and 2to3 will translate them like so:

> As tuple parameters are used by lambdas because of the single > expression limitation, they must also be supported. This is done by > having the expected sequence argument bound to a single parameter and > then indexing on that parameter: > > lambda (x, y): x + y > > will be translated into: > > lambda x_y: x_y[0] + x_y[1]

Which is quite similar to your implementation.

Solution 3 - Python

I don't know any good general alternatives to the Python 2 arguments unpacking behaviour. Here's a couple of suggestion that might be useful in some cases:

  • if you can't think of a name; use the name of the keyword parameter:

      def key(p): # more specific name would be better
          x, y = p
          return x**2 + y**3
    
      result = min(points, key=key)
    
  • you could see if a namedtuple makes your code more readable if the list is used in multiple places:

      from collections import namedtuple
      from itertools import starmap
    
      points = [ (1,2), (2,3)]
      Point = namedtuple('Point', 'x y')
      points = list(starmap(Point, points))
    
      result = min(points, key=lambda p: p.x**2 + p.y**3)
    

Solution 4 - Python

While the destructuring arguments was removed in Python3, it was not removed from comprehensions. It is possible to abuse it to obtain similar behavior in Python 3.

For example:

points = [(1,2), (2,3)]
print(min(points, key=lambda y: next(x*x + y*y for (x,y) in [y])))

In comparison with the accepted answer of using a wrapper, this solution is able to completely destructure the arguments while the wrapper only destructures the first level. That is, you can do

values = [(('A',1),'a'), (('B',0),'b')]
print(min(values, key=lambda y: next(b for ((a,b),c) in (y,))))

In comparison to the accepted answer using an unwrapper lambda:

values = [(('A',1),'a'), (('B',0),'b')]
print(min(points, key=lambda p: (lambda a,b: (lambda x,y: (y))(*a))(*p)))

Alternatively one can also use a list instead of a tuple.

values = [(('A',1),'a'), (('B',0),'b')]
print(min(points, key=lambda y: next(b for (a,b),c in [y])))

This is just to suggest that it can be done, and should not be taken as a recommendation. However, IMO, this is better than the hack of using using multiple expressions in a tuple and returning the last one.

Solution 5 - Python

Consider whether you need to unpack the tuple in the first place:

min(points, key=lambda p: sum(x**2 for x in p))

or whether you need to supply explicit names when unpacking:

min(points, key=lambda p: abs(complex(*p))

Solution 6 - Python

I think the better syntax is x * x + y * y let x, y = point, let keyword should be more carefully chosen.

The double lambda is the closest version. lambda point: (lambda x, y: x * x + y * y)(*point)

High order function helper would be useful in case we give it a proper name.

def destruct_tuple(f):
  return lambda args: f(*args)

destruct_tuple(lambda x, y: x * x + y * y)

Solution 7 - Python

Based on Cuadue suggestion and your comment on unpacking still being present in comprehensions, you can use, using numpy.argmin :

result = points[numpy.argmin(x*x + y*y for x, y in points)]

Solution 8 - Python

Another option is to write it into a generator producing a tuple where the key is the first element. Tuples are compared starting from beginning to end so the tuple with the smallest first element is returned. You can then index into the result to get the value.

min((x * x + y * y, (x, y)) for x, y in points)[1]

Solution 9 - Python

There may be a real solution to this, using PyFunctional!

Although not currently supported, I've submitted a tuple arg unpacking feature request to support:

(
    seq((1, 2), (3, 4))
    .map(unpack=lambda a, b: a + b)
)  # => [3, 7]

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbalkiView Question on Stackoverflow
Solution 1 - PythonjsbuenoView Answer on Stackoverflow
Solution 2 - Pythonnjzk2View Answer on Stackoverflow
Solution 3 - PythonjfsView Answer on Stackoverflow
Solution 4 - PythonRahul GopinathView Answer on Stackoverflow
Solution 5 - PythonchepnerView Answer on Stackoverflow
Solution 6 - Pythonanthony.hlView Answer on Stackoverflow
Solution 7 - Pythonnjzk2View Answer on Stackoverflow
Solution 8 - PythondazView Answer on Stackoverflow
Solution 9 - PythonKacheView Answer on Stackoverflow