25
a1=[1,2,3,4,5,6]  
b1=[[1,2,3], [4,5,6]]

If using np.shape list a1 will return (6,) and b1 will return (2, 3).

If Numpy is forbidden, how can I get the shape of list a1?

I am mainly confused about how can I let the python program know a1 is only one dimension. Is there any good method?

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
Yihong Guan
  • 267
  • 1
  • 3
  • 3

7 Answers7

27
>>>a = [1,2,3,4,5,6]
>>>print (len(a))
6

For one dimensional lists, the above method can be used. len(list_name) returns number of elements in the list.

>>>a = [[1,2,3],[4,5,6]]
>>>nrow = len(a)
>>>ncol = len(a[0])
>>>nrow
2
>>>ncol
3

The above gives the dimension of the list. len(a) returns number of rows. len(a[0]) returns number of rows in a[0] which is the number of columns.

Here's a link to original answer.

mcgusty
  • 1,354
  • 15
  • 21
10

The question clearly states 'without using numpy'. However, if anybody reached here looking for a solution without any condition, consider below. This solution will work for a balanced list.

b1=[[1,2,3], [4,5,6]]
np.asarray(b1).shape

(2, 3)

Regi Mathew
  • 2,603
  • 3
  • 24
  • 38
8

this is a recursive attempt at solving your problem. it will only work if all the lists on the same depth have the same length. otherwise it will raise a ValueError:

from collections.abc import Sequence


def get_shape(lst, shape=()):
    """
    returns the shape of nested lists similarly to numpy's shape.

    :param lst: the nested list
    :param shape: the shape up to the current recursion depth
    :return: the shape including the current depth
            (finally this will be the full depth)
    """

    if not isinstance(lst, Sequence):
        # base case
        return shape

    # peek ahead and assure all lists in the next depth
    # have the same length
    if isinstance(lst[0], Sequence):
        l = len(lst[0])
        if not all(len(item) == l for item in lst):
            msg = 'not all lists have the same length'
            raise ValueError(msg)

    shape += (len(lst), )
    
    # recurse
    shape = get_shape(lst[0], shape)

    return shape

given your input (and the inputs from the comments) these are the results:

a1=[1,2,3,4,5,6]
b1=[[1,2,3],[4,5,6]]

print(get_shape(a1))  # (6,)
print(get_shape(b1))  # (2, 3)
print(get_shape([[0,1], [2,3,4]]))  # raises ValueError
print(get_shape([[[1,2],[3,4]],[[5,6],[7,8]]]))  # (2, 2, 2)

not sure if the last result is what you wanted.


UPDATE

as pointed out in the comments by mkl the code above will not catch all the cases where the shape of the nested list is inconsistent; e.g. [[0, 1], [2, [3, 4]]] will not raise an error.

this is a shot at checking whether or not the shape is consistent (there might be a more efficient way to do this...)

from collections.abc import Sequence, Iterator
from itertools import tee, chain

def is_shape_consistent(lst: Iterator):
    """
    check if all the elements of a nested list have the same
    shape.

    first check the 'top level' of the given lst, then flatten
    it by one level and recursively check that.

    :param lst:
    :return:
    """

    lst0, lst1 = tee(lst, 2)

    try:
        item0 = next(lst0)
    except StopIteration:
        return True
    is_seq = isinstance(item0, Sequence)

    if not all(is_seq == isinstance(item, Sequence) for item in lst0):
        return False

    if not is_seq:
        return True

    return is_shape_consistent(chain(*lst1))

which could be used this way:

lst0 = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
lst1 = [[0, 1, 2], [3, [4, 5]], [7, [8, 9]]]

assert is_shape_consistent(iter(lst0))
assert not is_shape_consistent(iter(lst1))
mkl
  • 635
  • 1
  • 6
  • 16
hiro protagonist
  • 44,693
  • 14
  • 86
  • 111
  • 1
    Note that the check `if not all(len(item) == l for item in lst): raise` won't catch all inconsistencies. For example, `get_shape([[0, 1], [2, [3, 4]]])` returns `(2, 2)`, but `[[0, 1], [2, [3, 4]]]` doesn't have a well defined shape. – mkl Jan 22 '21 at 18:11
  • 1
    @mkl you might additionally count the total number of elements in the nested lists and check if it matches the product of the entries in shape... – hiro protagonist Jan 22 '21 at 18:36
  • 1
    If you take `[[0]]`, which has shape `(1, 1)`, you would have to only count the "dead ends", i.e. _non_-list elements. But then your condition fails on `[[0, 1, 2], [3, [4, 5]], [7, [8, 9]]]`, which has 9 elements and "shape" `(3, 3)`. – mkl Jan 22 '21 at 21:52
  • @mkl yes, as i wrote it i thought it must be possible to construct a list that defies my simple check... – hiro protagonist Jan 23 '21 at 07:29
  • 1
    @mkl added a function that should be able to verify that the shapes are consistent. not well tested... can it still be fooled? do you have an idea for a more efficient way? – hiro protagonist Jan 23 '21 at 08:00
  • That checks out, although I've made a small edit that handles empty lists. Note: I consider `[[]]` (the empty 0x0 matrix) to be shape-consistent (maybe I haven't given this enough thought). This case is not handled by `get_shape`. Also, what about `[[], []]`, etc.? – mkl Jan 24 '21 at 11:54
  • @mkl thanks for the edit! approved! (hmm... why is an edit on one of my posts not approved if i say it's ok?). and yes, i should consider empty lists as well. – hiro protagonist Jan 24 '21 at 12:41
3

Here is a great example from "Ten Essays on Fizz Buzz" book by Joel Grus by using recursion.

from typing import List, Tuple, Union


def shape(ndarray: Union[List, float]) -> Tuple[int, ...]:
    if isinstance(ndarray, list):
        # More dimensions, so make a recursive call
        outermost_size = len(ndarray)
        row_shape = shape(ndarray[0])
        return (outermost_size, *row_shape)
    else:
        # No more dimensions, so we're done
        return ()

Example:

three_d = [
    [[0, 0, 0], [1, 1, 1], [2, 2, 2]],
    [[0, 0, 0], [1, 1, 1], [2, 2, 2]],
    [[0, 0, 0], [1, 1, 1], [2, 2, 2]],
    [[0, 0, 0], [1, 1, 1], [2, 2, 2]],
    [[0, 0, 0], [1, 1, 1], [2, 2, 2]],
]

result = shape(three_d)
print(result)
>>> (5, 3, 3)
Vlad Bezden
  • 83,883
  • 25
  • 248
  • 179
1

Depending on the level of thoroughness required, I would recommend using tail recursion. Build up the shape from the innermost to the outermost list. That will allow you to check that all the sizes match up at every depth and index.

def shape(lst):
    def ishape(lst):
        shapes = [ishape(x) if isinstance(x, list) else [] for x in lst]
        shape = shapes[0]
        if shapes.count(shape) != len(shapes):
            raise ValueError('Ragged list')
        shape.append(len(lst))
        return shape
    return tuple(reversed(ishape(lst)))

Here is a demo on IDEOne: https://ideone.com/HJRwlC

shapes.count(shape) != len(shapes) is a neat trick to determine if all the shapes up to a given level are identical, taken from https://stackoverflow.com/a/3844948/2988730.

If your only goal is to determine whether the list is one dimensional or not, just run a single all on the outermost list:

is_1d = all(not isinstance(x, list) for x in lst)

OR

is_1d = not any(isinstance(x, list) for x in lst)
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
0

Following function keeps track of first item of each dimension of a list. It doesn't matter how many dimensions it has.

def list_shape(input):
    
shape = []
    a = len(input)
    shape.append(a)
    b = input[0]

    while a > 0:
        try:

            a = len(b)
            shape.append(a)
            b = b[0]

        except:
            break
        
    return shape

list1 = [[[123], [231]], [[345], [231]]]

print(list_shape(list1))

Output:

[2, 2, 1]

Note: Only works for symmetrical lists and numeral data within the list.

0

My proposal for the problem is the following relative complex code which delivers the maximum shape in form of a tuple. The method can be feeded by any kind of nested iterable object that contains a __len__ method. I use the itertools.zip_longest() method to calculate the shape.

import itertools

def get_max_shape(value):
    if hasattr(value, '__len__'): # add here `and not hasattr(value,'capitalize)` to exclude strings
        s0 = len(value)
        if s0 == 0:
            return ()
        if s0==1 and hasattr(value,'capitalize'): # handle string like objects 
            return ()
        s1 = 0
        sub_shape = []
        # via zip_longest we iterate over the longest sub_list item
        for v2 in itertools.zip_longest(*(v if hasattr(v, '__len__') else tuple() for v in value), fillvalue=0):
            s1 += 1  # calc the length of second dimension
            for v3 in v2:
                sub_shape_new = get_max_shape(v3)  # recursive measurement of lower dimensions
                # compare new shapes with old shapes (find max)
                for i, new in enumerate(sub_shape_new):
                    if i >= len(sub_shape):
                        # new items
                        sub_shape.extend(sub_shape_new[i:])
                    else:
                        # is max?
                        if sub_shape[i] < new:
                            sub_shape[i] = new
        if len(sub_shape):
            return (s0, s1) + tuple(sub_shape)
        elif s1:
            return (s0, s1)
        else:
            return (s0,)
    else:
        return ()

The execution delivers:

>>> print(get_max_shape([])
()
>>> print(get_max_shape([1, 2, 3])
(3,)
>>> print(get_max_shape([1, 2, [2, 3]]))
(3,2)
 >>> print(get_max_shape([[[2, 3, 4, 5], 1, 2], [3, [2, 3, 4], 5, 6]]))
(2, 4, 4)
>>> print(get_max_shape([[[2, 3, 4, [4, [5, 6]]], 1, 2], [3, [2, [3, 4, 5, 6, 7], 4], 5, 6]]))
(2, 4, 4, 5, 2)
>>> print(get_max_shape(['123456789', []])) # strings are also counted as lists
(2, 9)

The code might be improved but in general I think it works and it does not iterate over all items to get the dimensions it uses the length() method if possible.

B.R.
  • 234
  • 2
  • 7