57

On Python, range(3) will return [0,1,2]. Is there an equivalent for multidimensional ranges?

range((3,2)) # [(0,0),(0,1),(1,0),(1,1),(2,0),(2,1)]

So, for example, looping though the tiles of a rectangular area on a tile-based game could be written as:

for x,y in range((3,2)):

Note I'm not asking for an implementation. I would like to know if this is a recognized pattern and if there is a built-in function on Python or it's standard/common libraries.

MaiaVictor
  • 48,582
  • 42
  • 134
  • 263

7 Answers7

76

In numpy, it's numpy.ndindex. Also have a look at numpy.ndenumerate.

E.g.

import numpy as np
for x, y in np.ndindex((3,2)):
    print(x, y)

This yields:

0 0
0 1
1 0
1 1
2 0
2 1
Jeff Hammond
  • 4,945
  • 3
  • 26
  • 45
Joe Kington
  • 258,645
  • 67
  • 583
  • 455
  • 6
    +1: The syntax for that is alarmingly similar to what the OP originally asked for. Well played! – Li-aung Yip Apr 11 '12 at 00:16
  • 1
    As Li-aung pointed this is alarmingly similar to what I asked for, so it is, undoubtedly, the best answer to the topic. – MaiaVictor Apr 14 '12 at 15:33
  • 2
    Li-aung Yip answer is great, too, and has some learning on it as it shows the cartesian product can be used for the same purpose. – MaiaVictor Nov 16 '12 at 00:31
  • This makes loads of things with Numpy Arrays gigantically more generic, soo powerful. – alan2here Feb 17 '21 at 20:34
39

You could use itertools.product():

>>> import itertools
>>> for (i,j,k) in itertools.product(xrange(3),xrange(3),xrange(3)):
...     print i,j,k

The multiple repeated xrange() statements could be expressed like so, if you want to scale this up to a ten-dimensional loop or something similarly ridiculous:

>>> for combination in itertools.product( xrange(3), repeat=10 ):
...     print combination

Which loops over ten variables, varying from (0,0,0,0,0,0,0,0,0,0) to (2,2,2,2,2,2,2,2,2,2).


In general itertools is an insanely awesome module. In the same way regexps are vastly more expressive than "plain" string methods, itertools is a very elegant way of expressing complex loops. You owe it to yourself to read the itertools module documentation. It will make your life more fun.

Li-aung Yip
  • 11,952
  • 5
  • 31
  • 49
  • just a tiny improvement over your last answer: `for c in product(*([xrange(5)]*3)): print c`: from (0,0,0) to (4,4,4) – egor83 Apr 10 '12 at 17:27
  • It's actually better to use `itertools.tee()` if you want exact replicas - I believe the underlying implementation is more efficient due to caching. – Li-aung Yip Apr 10 '12 at 17:29
  • @agf: good catch - evidently time for me to sleep. Edited to that effect. – Li-aung Yip Apr 10 '12 at 17:38
26

There actually is a simple syntax for this. You just need to have two fors:

>>> [(x,y) for x in range(3) for y in range(2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
dhg
  • 51,689
  • 8
  • 120
  • 144
  • 1
    This is good, but I would like to point that it can get a little verbose: for (x,y) in [(x,y) for x in range(3) for y in range(2)]: – MaiaVictor Apr 10 '12 at 17:34
8

That is the cartesian product of two lists therefore:

import itertools
for element in itertools.product(range(3),range(2)):
    print element

gives this output:

(0, 0)
(0, 1)
(1, 0)
(1, 1)
(2, 0)
(2, 1)
0x90
  • 37,093
  • 35
  • 149
  • 233
4

You can use product from itertools module.

itertools.product(range(3), range(2))
Praveen Gollakota
  • 33,984
  • 10
  • 60
  • 61
3

I would take a look at numpy.meshgrid:

http://docs.scipy.org/doc/numpy-1.6.0/reference/generated/numpy.meshgrid.html

which will give you the X and Y grid values at each position in a mesh/grid. Then you could do something like:

import numpy as np
X,Y = np.meshgrid(xrange(3),xrange(2))
zip(X.ravel(),Y.ravel()) 
#[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]

or

zip(X.ravel(order='F'),Y.ravel(order='F')) 
# [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
JoshAdel
  • 62,453
  • 24
  • 136
  • 135
1

Numpy's ndindex() works for the example you gave, but it doesn't serve all use cases. Unlike Python's built-in range(), which permits both an arbitrary start, stop, and step, numpy's np.ndindex() only accepts a stop. (The start is presumed to be (0,0,...), and the step is (1,1,...).)

Here's an implementation that acts more like the built-in range() function. That is, it permits arbitrary start/stop/step arguments, but it works on tuples instead of mere integers.

import sys
from itertools import product, starmap

# Python 2/3 compatibility
if sys.version_info.major < 3:
    from itertools import izip
else:
    izip = zip
    xrange = range

def ndrange(start, stop=None, step=None):
    if stop is None:
        stop = start
        start = (0,)*len(stop)

    if step is None:
        step = (1,)*len(stop)

    assert len(start) == len(stop) == len(step)

    for index in product(*starmap(xrange, izip(start, stop, step))):
        yield index

Example:

In [7]: for index in ndrange((1,2,3), (10,20,30), step=(5,10,15)):
   ...:     print(index)
   ...:
(1, 2, 3)
(1, 2, 18)
(1, 12, 3)
(1, 12, 18)
(6, 2, 3)
(6, 2, 18)
(6, 12, 3)
(6, 12, 18)
Stuart Berg
  • 15,600
  • 11
  • 62
  • 92