2

FP like Haskell can bind a (var) name trivially e.g:

[(g y, h y) | x <- mylist, let y = f x]

Python does it possibly below:

mylist = [f(x) for x in mylist]
mylist = [(g(y), h(y)) for y in mylist]

Walrus assignment in Python 3.8 seems a hack to simplify list comprehensions :

[(y := f(x), g(y), h(y)) for x in mylist]

What is so far considered pythonic way in this case?

sof
  • 9,113
  • 16
  • 57
  • 83
  • 3
    well, if they added it in python 3.8, that must be pythonic. – Jean-François Fabre Nov 22 '19 at 20:33
  • It's controversial to say the least. The walrus operator is not well-liked and is tougher to understand – roganjosh Nov 22 '19 at 20:34
  • 2
    It is clearly well liked enough to have ended up in the language after governance - it seems to me that that is now the way to go. – modesitt Nov 22 '19 at 20:35
  • 3
    without this operator, you have to call `f(x)` 3 times. Not sure if it's pythonic or not, but the performance suffers. – Jean-François Fabre Nov 22 '19 at 20:35
  • 2
    @modesitt sure, after Guido stopped being the BDFL over it. Making it into the language hand-waves the collateral damage – roganjosh Nov 22 '19 at 20:37
  • 3
    This is all opinion-based. The code works; it should be closed – roganjosh Nov 22 '19 at 20:38
  • you can chain 2 comprehensions to get your result in one line. With := operator, you _have_ to create a triplet, not a couple like before. – Jean-François Fabre Nov 22 '19 at 20:39
  • I wouldn't say the walrus version "works"; it creates a list of 3-tuples, rather than a list of 2-tuples like the original definition of `mylist`. – chepner Nov 22 '19 at 20:40
  • Mean you could of course do `[(g((y := f(x))), h(y)) for x in mylist]` but that might be contentious – modesitt Nov 22 '19 at 20:42
  • @Jean-François Fabre, the walrus option here has changed the intent as side effect: tuple of two elements to three – sof Nov 22 '19 at 20:45
  • @roganjosh I disagree wholeheartedly; Stack Overflow is not just a place for questions about code that doesn't work. See for example this question, which presents working Python code and asks for a more "Pythonic" solution, with over 1000 upvotes, and **zero** downvotes: https://stackoverflow.com/questions/10660435/pythonic-way-to-create-a-long-multi-line-string – kaya3 Nov 22 '19 at 20:52
  • 1
    @kaya3 yeah, a question asked over 7 years ago. The walrus operator is controversial; all this question does is flare up people's opinion. It's not broken code and there aren't any performance gains obvious. It literally just stands to bring up opinion. That's a specific close reason. – roganjosh Nov 22 '19 at 20:57
  • Why not simply use `[(g(f(x)), h(f(x))) for x in mylist]` ? – Andreas K. Feb 05 '20 at 17:32

4 Answers4

7

The correct use of the walrus operator to create 2-tuples would be

mylist = [(g(y:=f(x)), h(y)) for x in mylist]

which, yes, is even more horrendous than the 3-tuple version. If you want to forgo the walrus operator, use the version which := was supposed to obviate:

mylist = [(g(y), h(y)) for x in mylist for y in [f(x)]]

or more simply

mylist = [(g(y), h(y)) for y in map(f, mylist)]

I would not say the walrus operator is always so ungainly, but it seems to be more trouble than it is worth here.

modesitt
  • 7,052
  • 2
  • 34
  • 64
chepner
  • 497,756
  • 71
  • 530
  • 681
  • The 1st oneliner is what i'm looking for! – sof Nov 22 '19 at 21:13
  • 1
    Of the three, I would recommend the *third* most strongly. – chepner Nov 22 '19 at 21:15
  • The 1st one seems more flexible to me but introduces new vars to the outer scope even unwanted. The 3rd need still define `f` elsewhere. – sof Nov 22 '19 at 21:25
  • PEP-572 discusses the rationale for `y` being defined in the containing scope, unlike `x` which is restricted to the comprehension. All three need to define `f` somewhere else. My main opposition to the first one is the asymmetrical expressions; I would have preferred something like `[let y = f(x) in (g(y), h(y)) for x in mylist]`, though adding new keywords to Python is much harder than new operators. – chepner Nov 22 '19 at 21:29
  • The 1st is the application of `f`, the 3rd the definition then need lambda expression if oneliner is preferred. – sof Nov 22 '19 at 21:34
  • What lambda expression? – chepner Nov 22 '19 at 21:37
  • E.g. `[(g(y:=x+1),h(y)) for x in mylist]` vs `[(g(y), h(y)) for y in map(lambda x: x+1, mylist)]` – sof Nov 22 '19 at 21:50
  • And, does the 3rd still suffer from double iterating over the list? – sof Nov 22 '19 at 21:53
  • Ah, ok. I'd still prefer either 2 or 3 here to avoid the asymmetry of the tuple expression. – chepner Nov 22 '19 at 21:54
  • 1
    The third does not, as `map` doesn't do anything until you actually request an item from it. Think of `map` as applying a wrapper around `mylist`; each time you request an item, it passes through `f` first. – chepner Nov 22 '19 at 21:55
  • Any reference to `map` as the wrapper here? It's already lengthy, thx a lot! – sof Nov 22 '19 at 21:58
  • 1
    That's just what an instance of the [`map`](https://docs.python.org/3/library/functions.html#map) class does; `map(f, xs)` is essentially the same as `(f(x) for x in xs)`. – chepner Nov 22 '19 at 22:00
1

I would argue that map is more appropriate for applying an operation on every element of a list.

mylist = map(f, mylist)
mylist = list(map(lambda x: (g(x), g(x)), mylist))
Woohoojin
  • 674
  • 1
  • 4
  • 19
  • 1
    Guido wanted to ditch `map` so I'm not sure how this is definitively "pythonic" – roganjosh Nov 22 '19 at 20:36
  • 1
    Guido also wanted to ditch `lambda`; he's not a fan of functional techniques in general (as witnessed by `reduce`'s demotion from built-in to the `functools` module). – chepner Nov 22 '19 at 20:46
  • But list comprehensions *are* a functional technique! – kaya3 Nov 22 '19 at 20:46
  • 1
    @kaya3 Ironic, isn't it? :) But even in Haskell (where Python borrowed it from), it is syntactic sugar used to *hide* the application of functions. – chepner Nov 22 '19 at 20:48
1

Since Python doesn't have "let ... in ..." expressions, the cleanest way is to write a function and call it.

def apply_g_h(x):
    y = f(x)
    return g(y), h(y)

mylist = [ apply_g_h(x) for x in mylist ]

If you really prefer it as a one-liner, then a lambda function can serve the same purpose:

mylist = [ (lambda y: (g(y), h(y)))(f(x)) for x in mylist ]

This works because a "let" expression let x = e1 in e2 is equivalent to (lambda x: e2)(e1). I've had to do this occasionally when limited to only writing expressions (to be passed to eval), but it's not very readable, so I think the first solution is much better.

kaya3
  • 47,440
  • 4
  • 68
  • 97
0

Some other questions are linked here (such as python how to declare variable in for comprehension without iterating) so let me answer a slightly more general question.

How to bind a local variable within a for comprehension without iteration. The trick is to create a singleton list and iterate over it such as for b in [u(a)] in the following.

One might find this ugly and cryptic, but it is the most concise way I have found.

[x for a in f(x) for b in [u(a)] if v(b) for x in w(b)]
Jim Newton
  • 594
  • 3
  • 16