5
\$\begingroup\$

I have a nested for-loop that populates a list with elements:

a = []
for i in range(1, limit+1):
    for j in range(1, limit+1):
        p = i + j + (i**2 + j**2)**0.5
        if p <= limit:
            a.append(p)

I could refactor it into list comprehension:

a = [i + j + (i**2 + j**2)**0.5
     for i in range(1, limit+1)
     for j in range(1, limit+1)
     if i + j + (i**2 + j**2)**0.5 <= limit]

But now the same complex expression is in both parts of it, which is unacceptable. Is there any way to create a list in a functional way, but more elegantly?

I guess in Lisp I would use recursion with let. How it is done in more functional languages like Clojure, Scala, Haskell?

In Racket it's possible to bind expressions inside a for/list comprehension. I've found one solution to my problem:

[k
 for i in range(1, limit+1)
 for j in range(1, limit+1)
 for k in [i + j + (i**2 + j**2)**0.5]
 k <= limit]

I'm not sure how pythonic it is.

\$\endgroup\$
  • \$\begingroup\$ Just pointing out that you can divide limit by 2 in the range function (since you square the numbers anyway) and you'll still get the full set of results. Also, I would write a transform function that handled the math and run a list comprehension over it. \$\endgroup\$ – kreativitea Dec 19 '12 at 18:56
3
\$\begingroup\$

In the general case, Jeff's answer is the way to go : generate then filter.

For your particular example, since the expression is increasing wrt both variables, you should stop as soon as you reach the limit.

def max_value(limit, i) :
   """return max positive value one variable can take, knowing the other"""
   if i >= limit :
      return 0
   return int(limit*(limit-2*i)/(2*(limit-i)))

def collect_within_limit(limit) :
   return [ i + j + (i**2 + j**2)**0.5
              for i in range(1,max_value(limit,1)+1)
              for j in range(1,max_value(limit,i)+1) ]

Now, providing this max_value is error prone and quite ad-hoc. We would want to keep the stopping condition based on the computed value. In your imperative solution, adding break when p>limit would do the job. Let's find a functional equivalent :

import itertools

def collect_one_slice(limit,i) :
   return itertools.takewhile(lambda x: x <= limit,
            (i + j + (i**2 + j**2)**0.5 for j in range(1,limit)))

def collect_all(limit) :
   return list(itertools.chain(*(collect_one_slice(limit, i)
                                        for i in range(1,limit))))
\$\endgroup\$
3
\$\begingroup\$

You can rewrite it with two separate comprehensions. One to generate the values and one to filter them out. You can use itertools.product to get the cartesian product.

a = [x for x in (i + j + (i**2 + j**2)**0.5
                 for i, j in itertools.product(range(1, limit+1), repeat=2))
     if x <= limit]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.