使用Unicode字符美化打印NumPy数组

6

我最近注意到Python打印NumPy ndarrays的功能不够一致。例如,它会将水平的1D数组以水平方式打印:

import numpy as np
A1=np.array([1,2,3])
print(A1)
#--> [1 2 3]

但是一个带有冗余括号的1D水平数组:

A2=np.array([[1],[2],[3]])
print(A2)
#--> [[1]
#     [2]
#     [3]]

将一维垂直数组水平化:
A3=np.array([[1,2,3]])
print(A3)
#--> [[1 2 3]]

还有一个二维数组:

B=np.array([[11,12,13],[21,22,23],[31,32,32]])
print(B)
# --> [[11 12 13]
#      [21 22 23]
#      [31 32 32]]

第一维现在是垂直的。对于更高的维度来说,情况变得更糟,因为它们全部都是垂直打印的:

C=np.array([[[111,112],[121,122]],[[211,212],[221,222]]])
print(C)
#--> [[[111 112]
#      [121 122]]
#
#     [[211 212]
#      [221 222]]]

在我看来,一致的行为应该是水平打印偶数维度和垂直打印奇数维度。使用Unicode字符可以将其格式化得很好。我想知道是否有可能创建一个函数来打印上述数组:

A1 --> [1 2 3]
A2 --> ┌┌─┐┌─┐┌─┐┐
       │ 1  2  3 │
       └└─┘└─┘└─┘┘
A3 --> ┌┌─┐┐ # \u250c\u2500\u2510 1# \u25022 │
       │ 3 │
       └└─┘┘ # \u2514\u2500\u2518 
B -->  ┌┌──┐┌──┐┌──┐┐ 
       │ 11  21  31 │
       │ 12  22  32 │
       │ 13  23  33 │
       └└──┘└──┘└──┘┘ 

C -->  ┌┌─────────┐┌─────────┐┐
       │ [111 112]  [211 212] │
       │ [121 122]  [221 222] │
       └└─────────┘└─────────┘┘ 

我发现这个代码片段可以处理不同位数的数字。我尝试原型化了一个递归函数来实现上述概念:

 def npprint(A):
     assert isinstance(A, np.ndarray), "input of npprint must be array like"
     if A.ndim==1 :
         print(A)
     else:
         for i in range(A.shape[1]):
             npprint(A[:,i]) 

它对于A1A2A3B有点用,但对于C不起作用。我希望你能帮我知道如何使用npprint 来实现任意维度的numpy ndarrays的上述输出。请注意:在Jupyter环境中,可以在Markdown中使用LaTeX \mathtools, \underbracket\overbracket。Sympy的漂亮打印功能也是一个很好的起点,它可以使用ASCII、Unicode、LaTeX等格式。有人告诉我ndarrays的打印方式确实是一致的,但在我看来它有点奇怪和不直观。有一个灵活的漂亮打印函数可以帮助我们以不同的形式显示ndarrays。Sympy的开发者已经考虑到了我在这里提到的两个问题。他们的矩阵模块非常一致(A1A2是相同的),他们还有一个pprint函数,它做了类似的事情,我期望npprint也能实现这样的效果。如果您有兴趣,请查看此Jupyter Notebook

1
A2 的形状为 (3,1)。第一维垂直打印,第二维按列打印。C 是 (2,2,2),第一维以空格分隔块显示,其余维度与 2D B 相同,按行/列显示。请注意方括号的使用,它们与等价列表的嵌套匹配。 - hpaulj
2
A2没有多余的括号。A3也是如此。形状与A1不同。括号很重要。 - hpaulj
2
numpy 的显示是一致的。最后一个维度(最内层)总是列。倒数第二个是行。然后用空格、括号和缩进分隔块。然后是更高级别的分隔。在 2D 屏幕上显示 3D 及以上的数组总会有问题(这也适用于编写 CSV 文件)。但是,实际工作中的数组通常太大而无法完整显示,无论布局如何。 - hpaulj
1
ndarray 的数据存储方式与 list 完全不同。0维数组与数组标量并不完全相同,而它们也与Python标量不完全相同。参考链接:https://docs.scipy.org/doc/numpy-1.15.0/reference/arrays.html。 - hpaulj
1
与MATLAB的差异太多了,无法在评论中列出。但是有几个关键点 - MATLAB中的所有内容都是2D的。即使3D也是在2D之上的薄层。这就是为什么它省略了尾随单例维度(超过第二个维度)。尾随维度是最外层的(Fortran风格)。两者都将内部2个维度显示为行/列块。扁平化矩阵的大小为(n,1)(与numpy中的(n,)形状形成对比)。 - hpaulj
显示剩余4条评论
2个回答

8

对我来说,理解numpy数组与我心中想象的MATLAB矩阵或多维数学数组完全不同是一个相当大的启示。它们实际上是由同质和统一的Python嵌套列表组成。我也明白了,numpy数组的第一维是最深/内部方括号,水平打印,然后从那里开始打印第二个垂直维度,第三个垂直带有一个空格行...

无论如何,我认为拥有一个“ppring”函数(受Sympy命名约定的启发)可以帮助很多人。所以我要在这里放一个非常糟糕的实现,希望它能激发其他高级Python程序员提出更好的解决方案:

def pprint(A):
    if A.ndim==1:
        print(A)
    else:
        w = max([len(str(s)) for s in A]) 
        print(u'\u250c'+u'\u2500'*w+u'\u2510') 
        for AA in A:
            print(' ', end='')
            print('[', end='')
            for i,AAA in enumerate(AA[:-1]):
                w1=max([len(str(s)) for s in A[:,i]])
                print(str(AAA)+' '*(w1-len(str(AAA))+1),end='')
            w1=max([len(str(s)) for s in A[:,-1]])
            print(str(AA[-1])+' '*(w1-len(str(AA[-1]))),end='')
            print(']')
        print(u'\u2514'+u'\u2500'*w+u'\u2518')  

对于一维和二维数组,结果有些可接受:

B1=np.array([[111,122,133],[21,22,23],[31,32,33]])
pprint(B1)

#┌─────────────┐
# [111 122 133]
# [21  22  23 ]
# [31  32  33 ]
#└─────────────┘

这确实是一段非常糟糕的代码,它只适用于整数。希望其他人能提出更好的解决方案。

P.S.1. Eric Wieser已经为IPython/Jupiter实现了一个非常好的HTML原型,可以在此处查看:

enter image description here

您可以在numpy邮件列表这里关注讨论。

P.S.2. 我还在Reddit上发布了这个想法

P.S.3 我花了一些时间扩展了该代码以适用于三维数组:

def ndtotext(A, w=None, h=None):
    if A.ndim==1:
        if w == None :
            return str(A)
        else:
            s= '['
            for i,AA in enumerate(A[:-1]):
                s += str(AA)+' '*(max(w[i],len(str(AA)))-len(str(AA))+1)
            s += str(A[-1])+' '*(max(w[-1],len(str(A[-1])))-len(str(A[-1]))) +'] '
    elif A.ndim==2:
        w1 = [max([len(str(s)) for s in A[:,i]])  for i in range(A.shape[1])]
        w0 = sum(w1)+len(w1)+1
        s= u'\u250c'+u'\u2500'*w0+u'\u2510' +'\n'
        for AA in A:
            s += ' ' + ndtotext(AA, w=w1) +'\n'    
        s += u'\u2514'+u'\u2500'*w0+u'\u2518'
    elif A.ndim==3:
        h=A.shape[1]
        s1=u'\u250c' +'\n' + (u'\u2502'+'\n')*h + u'\u2514'+'\n'
        s2=u'\u2510' +'\n' + (u'\u2502'+'\n')*h + u'\u2518'+'\n'
        strings=[ndtotext(a)+'\n' for a in A]
        strings.append(s2)
        strings.insert(0,s1)
        s='\n'.join(''.join(pair) for pair in zip(*map(str.splitlines, strings)))
    return s

举个例子:

shape = 4, 3, 6
B2=np.arange(np.prod(shape)).reshape(shape)
print(B2)
print(ndtotext(B2))        


[[[ 0  1  2  3  4  5]
  [ 6  7  8  9 10 11]
  [12 13 14 15 16 17]]

 [[18 19 20 21 22 23]
  [24 25 26 27 28 29]
  [30 31 32 33 34 35]]

 [[36 37 38 39 40 41]
  [42 43 44 45 46 47]
  [48 49 50 51 52 53]]

 [[54 55 56 57 58 59]
  [60 61 62 63 64 65]
  [66 67 68 69 70 71]]]
┌┌───────────────────┐┌───────────────────┐┌───────────────────┐┌───────────────────┐┐
│ [0  1  2  3  4  5 ]  [18 19 20 21 22 23]  [36 37 38 39 40 41]  [54 55 56 57 58 59] │
│ [6  7  8  9  10 11]  [24 25 26 27 28 29]  [42 43 44 45 46 47]  [60 61 62 63 64 65] │
│ [12 13 14 15 16 17]  [30 31 32 33 34 35]  [48 49 50 51 52 53]  [66 67 68 69 70 71] │
└└───────────────────┘└───────────────────┘└───────────────────┘└───────────────────┘┘

1
这个功能没有记录,可能在任何时候都会消失,但如果您不关心这个问题,您可以使用fmt = numpy.core.arrayprint._get_format_function(A),然后在该示例中调用fmt替换str - 然后它也将适用于其他类型,而且您无需自己处理列间距。 - Eric
@Eric 非常感谢你提供的出色原型。除了我在Numpy邮件列表中提到的要点外,我还看到你将最新的维度垂直堆叠了。我认为按照偶数水平和奇数垂直的约定方式会更好。此外,在这里,我请Variable Inspector的开发者们看看是否可以向Jupyter Notebook扩展中添加类似的功能。 - Foad S. Farimani

0
在这些情况下,您最终维度的每个实例都打印在单独的一行上。这里没有任何不一致之处。
尝试使用各种形式:
a = np.random.rand(5, 4, 3)
print(a)

更改 a 中的维数(例如,通过添加由逗号分隔的更多整数)。 每次打印 a 时,您会发现打印对象中的每一行都将具有 k 值,其中 ka 形状中的最后一个整数。


感谢您的回复。我认为主要问题,正如我在OP的评论中也提到的那样,是Numpy ndarrays并不完全是多维数学数组,而是高级Python列表,它们基本上是指针和地址。numpy并不像MATLAB一样处理ndarrays。无论如何,漂亮地打印结果会很有帮助。Sympy具有一些uncode功能。numpy也可以有。 - Foad S. Farimani
2
@Foad,你为什么说NumPy数组不是多维数组?NumPy不是Matlab,对吧。 - duhaime
例如MATLAB省略了额外的括号,就像你在数学上期望的那样。似乎每次我们在一组值周围加上括号时,我们都会创建一个指向C数组的指针。 - Foad S. Farimani
@Foad,我不知道你所说的“额外”括号是什么意思——括号表示给定对象的维数;在numpy中没有额外的括号这种事情。元组也与numpy数组非常不同。很抱歉,但我不明白你的困惑。 - duhaime
这就是为什么它被称为混淆:)) 好吧,如果你尝试MATLAB / Octave / Scilab / Julia A1A2将给出相同的结果。至于元组,我的意思是“1,2,3...”这是一个有效的Python元组,但与讨论无关。最后,我发布这个问题的原因是要有漂亮的打印输出,而不管numpy如何处理ndarrays,具有不同形式的石化结果会有所帮助。 - Foad S. Farimani
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接