Behavior of np.c_ with list and tuple arguments Behavior of np.c_ with list and tuple arguments numpy numpy

Behavior of np.c_ with list and tuple arguments


There are 2 common use cases for np.c_:

  • np.c_ can accept a sequence of 1D array-likes:

    In [98]: np.c_[[1,2],[3,4]]Out[98]: array([[1, 3],       [2, 4]])
  • or, np.c_ can accept a sequence of 2D array-likes:

    In [96]: np.c_[[[1,2],[3,4]], [[5,6],[7,8]]]Out[96]: array([[1, 2, 5, 6],       [3, 4, 7, 8]])

So np.c_ can be passed 1D array-likes or 2D array-likes. But that raises the question how is np.c_ supposed to recognize if the input is a single 2D array-like (e.g. [[1,2],[3,4]]) or a sequence of 1D array-likes (e.g. [1,2], [3,4])?

The developers made a design decision: If np.c_ is passed a tuple, the argument will be treated as a sequence of separate array-likes. If it is passed a non-tuple (such as a list), then that object will be consider a single array-like.

Thus, np.c_[[1,2], [3,4]] (which is equivalent to np.c_[([1,2], [3,4])]) will treat ([1,2], [3,4]) as two separate 1D arrays.

In [99]: np.c_[[1,2], [3,4]]Out[99]: array([[1, 3],       [2, 4]])

In contrast, np.c_[[[1,2], [3,4]]] will treat [[1,2], [3,4]] as a single 2D array.

In [100]: np.c_[[[1,2], [3,4]]]Out[100]: array([[1, 2],       [3, 4]])

So, for the examples you posted:

np.c_[[1,2]] treats [1,2] as a single 1D array-like, so it makes [1,2] into a column of a 2D array:

In [101]: np.c_[[1,2]]Out[101]: array([[1],       [2]])

np.c_[(1,2)] treats (1,2) as 2 separate array-likes, so it places each value into its own column:

In [102]: np.c_[(1,2)]Out[102]: array([[1, 2]])

np.c_[(1,2),] treats the tuple (1,2), (which is equivalent to ((1,2),)) as a sequence of one array-like, so that array-like is treated as a column:

In [103]: np.c_[(1,2),]Out[103]: array([[1],       [2]])

PS. Perhaps more than most packages, NumPy has a history of treating lists and tuples differently. That link discusses how lists and tuples are treated differenty when passed to np.array.


The first level on handling the argument comes from the Python interpreter, which translates a [...] into a call to __getitem__:

In [442]: class Foo():     ...:     def __getitem__(self,args):     ...:         print(args)     ...:        In [443]: Foo()['str']strIn [444]: Foo()[[1,2]][1, 2]In [445]: Foo()[[1,2],]([1, 2],)In [446]: Foo()[(1,2)](1, 2)In [447]: Foo()[(1,2),]((1, 2),)

np.c_ is an instance of np.lib.index_tricks.AxisConcatenator. It's __getitem__

    # handle matrix builder syntax    if isinstance(key, str):        ....        mymat = matrixlib.bmat(...)        return mymat    if not isinstance(key, tuple):        key = (key,)     ....    for k, item in enumerate(key):        ....

So except for the np.bmat compatible string, it turns all inputs into a tuple, and then iterates over the elements.

Any of the variations containing [1,2] is the same as ([1,2],), a single element tuple. (1,2) is two elements that will be concatenated. So is ([1,2],[3,4]).

Note that numpy indexing also distinguishes between lists and tuples (though with a few inconsistencies).

In [455]: x=np.arange(24).reshape(2,3,4)In [456]: x[0,1]               # tuple - index for each dimOut[456]: array([4, 5, 6, 7])In [457]: x[(0,1)]             # same tupleOut[457]: array([4, 5, 6, 7])In [458]: x[[0,1]]             # list - index for one dimOut[458]: array([[[ 0,  1,  2,  3],        [ 4,  5,  6,  7],        [ 8,  9, 10, 11]],       [[12, 13, 14, 15],        [16, 17, 18, 19],        [20, 21, 22, 23]]])In [459]: x[([0,1],)]          # same     ....