How can I simulate 2.x's tuple unpacking for lambda parameters, using 3.x?
Asked Answered
C

10

142

In Python 2, I can write:

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

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

But that is not supported in 3.x:

  File "<stdin>", line 1
    min(points, key=lambda (x, y): (x*x + y*y))
                           ^
SyntaxError: invalid syntax

The straightforward workaround is to index explicitly into the tuple that was passed:

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

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

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

But because the lambda only supports a single expression, it's not possible to put the x, y = p part into it.

How else can I work around this limitation?

Cockaigne answered 19/2, 2014 at 21:36 Comment(14)
perhaps they dont want people doing that ... for your given example I would do min(points, key=lambda p: sum(x*x for x in p)Enactment
It's probably more likely for lambda to get removed from the language entirely then to reverse changes that made it harder to use, but you could try posting on python-ideas if you'd like to express a desire to see the feature added back.Eclat
It's probably more likely for lambda to get removed from the language why would lambda be removed? changes that made it harder to use? you think tuple unpacking makes lambda harder to use ?Atheling
I don't get it either, but it seems like the BDFL opposes lambda in the same spirit as he opposes map, reduce and filter.Husking
lambda was slated for removal in py3k as it's basically a blight on the language. But nobody could agree on a proper alternative for defining anonymous functions, so eventually Guido threw up his arms in defeat and that was that.Pepita
anonymous functions are a must have in any proper languages, and I quite like the lambdas. I'll have to read the whys of such a debate. (Also, even though map and filter are best replaced by comprehensions, I do like reduce)Atheling
The one thing I dislike about Python 3...Reprobate
I think your helper star function is great, and makes the accepted answer much easier to read and decode. Plus it makes for easy python 2 to 3 conversion - just take the existing lambda, remove the parenthesis from the tuple, and wrap the whole lambda in star()Underwear
I wouldn't use a lambda for the inner wrapper of the star method. That will further limit introspection possibilities. Do this instead: from functools import wraps def star(f): @wraps(f) def f_(args): return f(*args) return f_Abbatial
@Abbatial Thanks! updated.Cockaigne
@Pepita "basically a blight on the language": then make it better don't remove it. In the end nothing happened - but the entire premise of considering to remove it (along with map, fold, reduce !) causes deep suspicion of core python team.Dorinda
The Update1 (star(f)) seems a pretty nice workaround. forklifted.Dorinda
A possible real solution to this using PyFunctional!: https://mcmap.net/q/65958/-how-can-i-simulate-2-x-39-s-tuple-unpacking-for-lambda-parameters-using-3-xRoom
my update that shows star function is removed. check here for revisionCockaigne
T
52

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]
Thoron answered 19/2, 2014 at 21:48 Comment(5)
And of course, I forgot to mention that the "one and obvious" way of doing this is to actually spend the three lines function using def that is mentioned in the question under the nme some_name_to_think-of.Thoron
This answer still see some visitors - with PEP 572 implementation, there should be way to create variables inside the lambda expression, like in: key = lambda p: (x:=p[0], y:=p[1], x ** 2 + y ** 2)[-1]Thoron
assignment expressions!! i'm (pleasantly) surprised they made it inDorinda
I enjoyed that way of getting C's comma operator. :D [a, b, c][-1]Diffusive
So... we cannot do something like x,y := p yet?Straw
A
31

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.

Atheling answered 19/2, 2014 at 21:41 Comment(1)
Good that it is not removed in for loop or comprehensionsCockaigne
E
16

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)
    
Embarrassment answered 19/2, 2014 at 22:21 Comment(1)
Among all the answers for this question so far, using a namedtuple is the best course of action here. Not only you can keep your lambda but also I find that the namedtuple version is more readable as the difference between lambda (x, y): and lambda x, y: is not obvious at first sight.Transliterate
A
9

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.

Avulsion answered 25/12, 2017 at 12:24 Comment(0)
I
3

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)
Ignorant answered 19/9, 2019 at 3:26 Comment(0)
P
2

Since questions on Stack Overflow are not supposed to contain the answer in the question, nor have explicit "update" sections, I am converting OP's original "updates" to a proper answer and making it community wiki.

OP originally claimed that this solution was "extending the idea in the answer". I cannot discern which answer that meant, or which idea. The idea is functionally the same as anthony.hl's answer, but that came years later. Considering the state of answers at the time, I think this qualifies as OP's original work.)


Make a wrapper function that generalizes the process of unpacking the arguments, like so:

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

Now we can use this to transform the lambda we want to write, into one that will receive the argument properly:

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

We can further clean this up by using functools.wraps:

import functools

def star(f):
    @functools.wraps(f)
    def f_inner(args):
        return f(*args)
    return f_inner
Picaroon answered 19/2, 2014 at 21:36 Comment(0)
D
1

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)**2)
Discountenance answered 3/6, 2019 at 18:51 Comment(3)
The abs(complex(...)) trick is clever, but very specific to the circumstances of the example problem. Also, the result needs to be squared to match the spec - I'll edit that in.Picaroon
... Although I suppose it doesn't matter given that the key is being used by min, it seems appropriate to show a lambda that actually does the same thing.Picaroon
Pretty sure I just overlooked that the absolute value is the square root of the sum. I like to think if I was being that clever, I would have explained it :)Discountenance
A
0

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)]
Atheling answered 20/2, 2014 at 18:37 Comment(0)
S
0

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]
Selfdriven answered 4/12, 2018 at 19:22 Comment(0)
R
0

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]
Room answered 18/9, 2021 at 22:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.