'Is it possible to define function that creates an N-dimensional list of lists easily?

I have searched and found these questions: How to create a multi-dimensional list and N dimensional array in python which hint toward what I am looking for, but they seem to only make 2D arrays and not ND arrays.

Problem:

My problem is that I am able to create an n-dimensional list of lists for a known n, but I am not sure how it can be generalized to work with all values of n.

Consider this example:

def makeList(n):
    return [[[n for _ in range(n)] 
                for _ in range(n)] 
                for _ in range(n)]
print(makeList(3))

Output:

[[[3, 3, 3], 
  [3, 3, 3], 
  [3, 3, 3]], 
 [[3, 3, 3], 
  [3, 3, 3], 
  [3, 3, 3]], 
 [[3, 3, 3], 
  [3, 3, 3], 
  [3, 3, 3]]]

This will create a list of lists that is a 3x3x3 array of 3's which is the intended result, but if we use a different n:

print(makeList(2))

Output:

[[[2, 2], 
  [2, 2]], 
 [[2, 2], 
  [2, 2]]]

This will create a list of lists that is a 2x2x0 array of 2's and this is not the intended result. Instead the results should be a list of lists that is a 2x2 array of 2's.

Likewise if we set n = 4:

print(makeList(4))

This will give a list of lists that is 4x4x4 when it should give a 4x4x4x4 list of lists.

The main issue is that the number of for loops must change depending on the input, but obviously the code can't "come to life" and recode itself magically, hence my issue.

I am looking for a way to get this result that is simple. I am sure I could continue developing ideas for solutions, but I have not been able to think of anything that is concise.

What I have tried:

The first idea I thought of was to use recursion, and this my simple approach:

def makeList(n, output = []):
    if not output:
        output = [n for _ in range(n)]
    else:
        output = [output for _ in range(n)]
    if len(output) == n:
        return output
    else:
        return makeList(n, output)

This obviously will not work because if len(output) == n: will execute the first iteration since the length of the inner loops is equal to the length of the outermost loop. However, even if there was a condition that properly terminated the function, this solution would still not be ideal because I could run into maximum recursion errors with large values of n. Furthermore, even if these issues were resolved this code is still quite long as well as time consuming.

The other potential perspective I considered (and the solution that I found that works) is using a dict. My solution saves the intermediate lists of lists in a dict so it can be used for the next iteration:

def makeList(n):
    d = {str(i): None for i in range(n)}
    for i in range(n):
        if i == 0:
            d[str(i)] = [n for _ in range(n)]
        else:
            d[str(i)] = [d[str(i-1)] for _ in range(n)]
    return d[str(n-1)]

But again, this is very long and doesn't seem very pythonic.

Solution requirements:

The ideal solution would be much more concise than mine with an efficient time complexity that only uses built-in functions.

Other options that are close to these requirements would also be helpful as long as the spirit of the answer is trying to best meet the requirements.

Being concise is the most important aspect, however, which is why I am not fully happy with my current attempts.



Solution 1:[1]

Using tuple(n for _ in range(n)) to get the dimension that you want, and numpy to generate a multidimensional array:

import numpy as np

def make_array(n):
    return np.full(tuple(n for _ in range(n)), n)

for n in range(1,5):
    print(make_array(n))

Output:

[1]

[[2 2]
 [2 2]]

[[[3 3 3]
  [3 3 3]
  [3 3 3]]

 [[3 3 3]
  [3 3 3]
  [3 3 3]]

 [[3 3 3]
  [3 3 3]
  [3 3 3]]]

[[[[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]]


 [[[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]]


 [[[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]]


 [[[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]

  [[4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]
   [4 4 4 4]]]]

Solution 2:[2]

Your BEST plan is to use numpy for this. You can create an arbitrary array of all zeros with np.zeros( (4,4,4) ), or an array of 1s with np.ones( (4,4,4) ). If you're going to be working with arrays very much at all, you will certainly want to use numpy.

Solution 3:[3]

I've sketched out a recursive function that might suit:

def nd_list(n, x=None):
    if x is None:
        x = n
    if x == 1:
        return [n] * n
    return [nd_list(n, x-1)] * n

In two lines using no external libraries!

def nd_list(x, z):
    return [z] * z if x == 1 else [nd_list(x-1, z)] * z

In two lines (just) and without the list of lists surprising behaviour:

def nd_list(x, z):
    return [0 for _ in range(z)] if x == 1 else [nd_list(x-1, z) for _ in range(z)]

Solution 4:[4]

Using python support for pattern matching

def nlist(shape: tuple[int, ...], fill_with=None):
    match shape:
        case (n,):
            return [fill_with] * n
        case (n, *rest):
            return [nlist(rest, fill_with) for _ in range(n)]
        case _:
            raise ValueError(f'Invalid value shape={shape}')


def makeList(n):
    return nlist((n,)*n, n)

Solution 5:[5]

I mean, I like numpy and all, but if I want to do general dynamic programming I don't want to have to pip install a massive library. Would also suck hard for technical interviews requiring complex DP.

Here's a simple, readable function that takes advantage of the built-in itertools module in Python (it uses repeat) and the copy module for making deep copies of lists (otherwise, you get that "surprising" list behavior where modifying one entry modifies many). It's also pretty sane in terms of the API: you can call it with a tuple of dimensions (like numpy's array's shape field):

from itertools import repeat
from copy import deepcopy

def ndarray(shape, val=None):
    base = val
    for dim in reversed(shape):
        base = list(map(deepcopy, repeat(base, times=dim)))
    return base

This is what is made:

>>> import pprint
>>> pprint.pprint(ndarray((2, 3, 4), val=1))
[[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]],
 [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]]]

For your original question, you could easily wrap makeList(n) around this:

def makeList(n, val=None):
    return ndarray([n]*n, val=val)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Stef
Solution 2 Tim Roberts
Solution 3
Solution 4 frndmg
Solution 5