41

Is it possible to define a recursive list comprehension in Python?

Possibly a simplistic example, but something along the lines of:

nums = [1, 1, 2, 2, 3, 3, 4, 4]
willThisWork = [x for x in nums if x not in self] # self being the current comprehension

Is anything like this possible?

SilentGhost
  • 307,395
  • 66
  • 306
  • 293
Yuval Adam
  • 161,610
  • 92
  • 305
  • 395
  • 4
    If order is not an issue, `list(set(num))`. Otherwise, check `unique_everseen` in http://docs.python.org/library/itertools.html. – kennytm Apr 14 '10 at 15:05
  • leaky abstraction alert. comprehensions shouldn't be thought of as loops, in my opinion, even though they may be implemented as loops in cpython .. – wim Apr 16 '13 at 03:49

7 Answers7

34

No, there's no (documented, solid, stable, ...;-) way to refer to "the current comprehension". You could just use a loop:

res = []
for x in nums:
  if x not in res:
    res.append(x)

of course this is very costly (O(N squared)), so you can optimize it with an auxiliary set (I'm assuming that keeping the order of items in res congruent to that of the items in nums, otherwise set(nums) would do you;-)...:

res = []
aux = set()
for x in nums:
  if x not in aux:
    res.append(x)
    aux.add(x)

this is enormously faster for very long lists (O(N) instead of N squared).

Edit: in Python 2.5 or 2.6, vars()['_[1]'] might actually work in the role you want for self (for a non-nested listcomp)... which is why I qualified my statement by clarifying there's no documented, solid, stable way to access "the list being built up" -- that peculiar, undocumented "name" '_[1]' (deliberately chosen not to be a valid identifier;-) is the apex of "implementation artifacts" and any code relying on it deserves to be put out of its misery;-).

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
13

Starting Python 3.8, and the introduction of assignment expressions (PEP 572) (:= operator), which gives the possibility to name the result of an expression, we could reference items already seen by updating a variable within the list comprehension:

# items = [1, 1, 2, 2, 3, 3, 4, 4]
acc = []; [acc := acc + [x] for x in items if x not in acc]
# acc = [1, 2, 3, 4]

This:

  • Initializes a list acc which symbolizes the running list of elements already seen
  • For each item, this checks if it's already part of the acc list; and if not:
    • appends the item to acc (acc := acc + [x]) via an assignment expression
    • and at the same time uses the new value of acc as the mapped value for this item
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
10

Actually you can! This example with an explanation hopefully will illustrate how.

define recursive example to get a number only when it is 5 or more and if it isn't, increment it and call the 'check' function again. Repeat this process until it reaches 5 at which point return 5.

print [ (lambda f,v: v >= 5 and v or f(f,v+1))(lambda g,i: i >= 5 and i or g(g,i+1),i) for i in [1,2,3,4,5,6] ]

result:

[5, 5, 5, 5, 5, 6]
>>> 

essentially the two anonymous functions interact in this way:

let f(g,x) = {  
                 expression, terminal condition
                 g(g,x), non-terminal condition
             }

let g(f,x) = {  
                 expression, terminal condition
                 f(f,x), non-terminal condition
             }

make g,f the 'same' function except that in one or both add a clause where the parameter is modified so as to cause the terminal condition to be reached and then go f(g,x) in this way g becomes a copy of f making it like:

f(g,x) = {  
                 expression, terminal condition
                 {
                    expression, terminal condition,
                    g(g,x), non-terminal codition
                 }, non-terminal condition
             }

You need to do this because you can't access the the anonymous function itself upon being executed.

i.e

(lambda f,v: somehow call the function again inside itself )(_,_)

so in this example let A = the first function and B the second. We call A passing B as f and i as v. Now as B is essentially a copy of A and it's a parameter that has been passed you can now call B which is like calling A.

This generates the factorials in a list

print [ (lambda f,v: v == 0 and 1 or v*f(f,v-1))(lambda g,i: i == 0 and 1 or i*g(g,i-1),i) for i in [1,2,3,5,6,7] ]

[1, 2, 6, 120, 720, 5040]
>>> 
  • 2
    Cloning the lambda is unnecessary; you can use a generic proxy as the first lambda to allow any kind of second lambda to call itself. `(lambda f,arg: f(f,arg))(lambda self,v: .... , firstvalue)` – Sean Gugler Nov 03 '14 at 19:20
2

Not sure if this is what you want, but you can write nested list comprehensions:

xs = [[i for i in range(1,10) if i % j == 0] for j in range(2,5)]
assert xs == [[2, 4, 6, 8], [3, 6, 9], [4, 8]]

From your code example, you seem to want to simply eliminate duplicates, which you can do with sets:

xs = sorted(set([1, 1, 2, 2, 3, 3, 4, 4]))
assert xs == [1, 2, 3, 4]
Eli Courtwright
  • 186,300
  • 67
  • 213
  • 256
1

no. it won't work, there is no self to refer to while list comprehension is being executed.

And the main reason of course is that list comprehensions where not designed for this use.

SilentGhost
  • 307,395
  • 66
  • 306
  • 293
1

No.

But it looks like you are trying to make a list of the unique elements in nums.

You could use a set:

unique_items = set(nums)

Note that items in nums need to be hashable.

You can also do the following. Which is a close as I can get to your original idea. But this is not as efficient as creating a set.

unique_items = []
for i in nums:
    if i not in unique_items:
        unique_items.append(i)
Gary Kerr
  • 13,650
  • 4
  • 48
  • 51
1

Do this:

nums = [1, 1, 2, 2, 3, 3, 4, 4]
set_of_nums = set(nums)
unique_num_list = list(set_of_nums)

or even this:

unique_num_list = sorted(set_of_nums)
hughdbrown
  • 47,733
  • 20
  • 85
  • 108
  • The list comprehensions are unnecessary. `unique_num_list = list(set_of_nums)`. `sorted(set_of_nums)` returns a list. – Gary Kerr Apr 15 '10 at 09:19