62

In solid mechanics, I often use Python and write code that looks like the following:

for i in range(3):
    for j in range(3):
        for k in range(3):
            for l in range(3):
                # do stuff

I do this really often that I start to wonder whether there is a more concise way to do this. The drawback of the current code is: if I comply with PEP8, then I cannot exceed the 79-character-limit per line, and there is not too much space left, especially if this is again in a function of a class.

Mazdak
  • 105,000
  • 18
  • 159
  • 188
Yuxiang Wang
  • 8,095
  • 13
  • 64
  • 95
  • Are you only iterating over ranges? Then there's a shorter (although not necessarily more readable) way. – L3viathan Feb 07 '15 at 13:05
  • 5
    If an algorithm is O(n^4), then it is O(n^4). No way around that. To get around the 79 chars limit, consider splitting them into functions. That will do wonders for both readability and testability. – UltraInstinct Feb 07 '15 at 13:06
  • 6
    Well... deep nested looping is not a very nice way of programming... so I think you should worry more about avoiding deep nested looping than about PEP8. – sarveshseri Feb 07 '15 at 13:07
  • 4
    use vectorized numpy operations such as `numpy.einsum()`, see [Fast tensor rotation with NumPy](http://stackoverflow.com/q/4962606/4279) – jfs Feb 08 '15 at 16:17
  • Duplicate? This definitely seems like the better question... – Zizouz212 May 26 '15 at 20:48

4 Answers4

121

By using nested for-loops you're basically trying to create what's known as the (Cartesian) product of the input iterables, which is what the product function, from itertools module, is for.

>>> list(product(range(3),repeat=4))
[(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 1, 0), (0, 0, 1, 1),
 (0, 0, 1, 2), (0, 0, 2, 0), (0, 0, 2, 1), (0, 0, 2, 2), (0, 1, 0, 0),
...

And in your code you can do :

for i,j,k,l in product(range(3),repeat=4):
    #do stuff

Based on python documentation, "This function is roughly equivalent to the following code, except that the actual implementation does not build up intermediate results in memory:"

def product(*args, repeat=1):
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
    pools = [tuple(pool) for pool in args] * repeat
    result = [[]]
    for pool in pools:
        result = [x+[y] for x in result for y in pool]
    for prod in result:
        yield tuple(prod)
Mazdak
  • 105,000
  • 18
  • 159
  • 188
  • 7
    IMHO, this is the way to go. At least if the loops are as basic as presented in the OP. This scheme can also handle ranges of different length (in fact it can handle arbitrary iterables): `product(range(3),range(4),['a','b','c'] ,some_other_iterable)`. If the nested loops are more complex than that (e.g. contain some if/else logic) defining your on generator function is the next best thing. Here I would prefer @SergeBallesta's variant over @L3viathan's generator because of simplicity and ease of understanding. – PeterE Feb 07 '15 at 16:33
  • @PeterE I think you can find the most simplicity in `for i,j,k,l in product(range(3),repeat=4)` also i add the equivalent function of `product` to answer you can check it out . – Mazdak Feb 07 '15 at 17:52
  • In hindsight I see that my comment could be misunderstood as a critic. That was not my intention. I wanted to express that I find your solution the best for the given problem. And I wanted to mention, that `product()` can still be used even if the ranges have differing length. – PeterE Feb 07 '15 at 21:51
  • @PeterE i get your mean and thanks for your attention , and note , i'll add it to answer too ;) – Mazdak Feb 07 '15 at 21:54
  • 2
    This is the only way to go in this kind of situation, just a bit of FP concepts and code quickly gets more elegant / concise. – Jean-Marie Comets Feb 10 '15 at 20:51
  • It should be noted that this is roughly 2.5 times slower than the long written version! Try `product(range(100), repeat=4)` against the written out version. product takes me 12 seconds, while the long version takes 5 seconds. – PascalVKooten Apr 04 '15 at 09:39
15

The idea to use itertools.product is a good one. Here's a more general approach that will support ranges of varying sizes.

from itertools import product

def product_of_ranges(*ns):
    for t in product(*map(range, ns)):
        yield t

for i, j, k in product_of_ranges(4, 2, 3):
    # do stuff
FMc
  • 41,963
  • 13
  • 79
  • 132
13

It won't be more concise as it will cost you a generator function, but at least you won't be bothered by PEP8 :

def tup4(n):
    for i in range(n):
        for j in range(n):
            for k in range(n):
                for l in range(n):
                    yield (i, j, k, l)

for (i, j, k, l) in tup4(3):
    # do your stuff

(in python 2.x you should use xrange instead of range in the generator function)

EDIT:

Above method should be fine when the depth of the pyramid is known. But you can also make a generic generator that way without any external module :

def tup(n, m):
    """ Generate all different tuples of size n consisting of integers < m """
    l = [ 0 for i in range(n)]
    def step(i):
        if i == n : raise StopIteration()
        l[i] += 1
        if l[i] == m:
            l[i] = 0
            step(i+ 1)
    while True:
        yield tuple(l)
        step(0)

for (l, k, j, i) in tup(4, 3):
    # do your stuff

(I used (l, k, j, i) because in above generator, first index varies first)

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
8

This is equivalent:

for c in range(3**4):
    i = c // 3**3 % 3
    j = c // 3**2 % 3
    k = c // 3**1 % 3
    l = c // 3**0 % 3
    print(i,j,k,l)

If you're doing this all the time, consider using a general generator for it:

def nestedLoop(n, l):
    return ((tuple((c//l**x%l for x in range(n-1,-1,-1)))) for c in range(l**n))

for (a,b,c,d) in nestedLoop(4,3):
    print(a,b,c,d)
L3viathan
  • 26,748
  • 2
  • 58
  • 81
  • `c // 1`? Regardless, good point...which I might have up-voted if you had at least shown how to turn this specific case into a generator. – martineau Feb 07 '15 at 14:42
  • I included `c // 1` (and the first modulo) for better understanding. – L3viathan Feb 07 '15 at 14:52
  • 1
    In that case, for illustrative purposes you probably should have also used `c // 3**3 % 3`, `c // 3**2 % 3`, etc. `;-)` – martineau Feb 07 '15 at 15:07
  • Good point, changed it to that. That should also make it clearer how to write a generator for it. – L3viathan Feb 07 '15 at 16:09
  • 1
    …and there's the generator. – L3viathan Feb 07 '15 at 16:14
  • +1 – also note you could go further and put that `for` loop in a "tuple comprehension" turning it into a much-coveted one-liner. – martineau Feb 07 '15 at 16:31
  • See edit. Not very readable now though. – L3viathan Feb 07 '15 at 17:41
  • 1
    Agreed...but that doesn't seems as important as shortness to most folks here. You could shorten it even more by `yield`ing a tuple instead of returning a generator expression, as well as by changing the `reversed(range(n))` to `range(n)[::-1]` (or `range(n,-1,-1)`). – martineau Feb 07 '15 at 18:18
  • 1
    @martineau The latter, since slicing will, I think, generate a list. – L3viathan Feb 07 '15 at 20:34
  • Excellent point re slicing – however now it's broke – needs to be `range(n-1,-1,-1)` (my mistake). – martineau Feb 07 '15 at 21:22
  • @Quincunx OT: That reminds me of my confusion: since I started to work in Java, I am always confused: is it now `this` or `self`? In both languages... – glglgl Feb 08 '15 at 11:49