numpy配列に文字列を格納した場合の型と挙動

 numpy配列に文字列を格納した場合、どう扱われるのか知らなかったので、調査してみました。
 まず基本。
>>> import numpy as np
>>> a = np.array(["a", "b"])
>>> a
array(['a', 'b'], dtype='<U1')
>>> type(a[0])
<class 'numpy.str_'>
 配列そのものは「<U1」なる型になります。要素を取り出すとnumpy.str_という型です。要素の型は別にどうでも良いのですが、中身の実体は何なのでしょうか。
 ドキュメントにはイマイチ親切な説明がありませんが、こちらなどに一応書いてあります。
The basic string format consists of 3 parts: a character describing the byteorder of the data (: big-endian, |: not-relevant), a character code giving the basic type of the array, and an integer providing the number of bytes the type uses.
(中略)
U Unicode (fixed-length sequence of Py_UNICODE)
 リトルエンディアンの長さ1(固定長)のユニコード文字列のようです。
 固定長ということなので、いろいろやってみます。
>>> import sys
>>> a = np.array(["a"]*1000)
>>> sys.getsizeof(a)
4096
>>> a = np.array(["a"]*10000)
>>> sys.getsizeof(a)
40096
 96バイトのおまけが付いてきますが、基本的には1要素あたり4byte=32bitで表現するようです。内部的にUCS4で表現されているから、ということだと思います。
 つまり、ASCIIもマルチバイトも同じ大きさで表現できます。
>>> a = np.array(["あ"]*10000)
>>> sys.getsizeof(a)
40096
 固定長フォーマットなので、文字数を増やすとお察しの通りの現象が発生します。更にお気付きの通り、一番長さが大きい要素に引っ張られます。
>>> a = np.array(["aあ"]*10000)
>>> sys.getsizeof(a)
80096
>>> a = np.array(["a"]*9999 + ["aあ"]*1)
>>> sys.getsizeof(a)
80096
>>> a = np.array(["a"]*9999 + ["aiueoあいうえお"]*1)
>>> sys.getsizeof(a)
400096
 メモリ効率は悪いです。まあ、numpyというのはそもそも固定データサイズ前提の設計になっているので、仕方ありません。
 型のことはわかったので、いろいろな操作をしてみます。
 まず、indexingを試してみましょう。
>>> a = np.array(["ab", "cd", "ef"])
>>> a[0, 1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: too many indices for array
>>> a[0][1]
'b'
 C言語のcharの2次元配列みたいな実装になっていることは容易に想像できるのですが、numpy配列としてはndim==1なので、都合よく要素を取り出せたりはしません。また、代入してmutableな文字列として使うというのも不可能なようです。
 通常の演算などが行えるかを確認します。
 復習しておくと、素のpythonの文字列では
  • 文字列同士の加算
  • 文字列とintの掛け算
 のみが行えるはずです。
>>> a = np.array(["a", "b", "c"])
>>> b = np.array(["あ", "い", "う"])
>>> a + b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ufunc 'add' did not contain a loop with signature matching types dtype('<U1') dtype('<U1') dtype('<U1')
>>> a * 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ufunc 'multiply' did not contain a loop with signature matching types dtype('<U3') dtype('<U3') dtype('<U3')
 numpyの内部表現そのままの型だと演算は受け付けてくれないようです。残念。
 object型(要するにただのstrということだと思う)に変換すれば、いけるようです。
>>> a.astype(object) + b.astype(object)
array(['aあ', 'bい', 'cう'], dtype=object)
>>> a.astype(object) * 3
array(['aaa', 'bbb', 'ccc'], dtype=object)
 ということなので、全体を通して最初からobject型で配列を作っておけば良いと思いました。逆に、numpyの内部表現で持っておくメリットはあるのでしょうか・・・。