静かなる名辞

pythonとプログラミングのこと

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念



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 (<: little-endian, >: 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)

The Array Interface — NumPy v1.16 Manual

 リトルエンディアンの長さ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ということだと思う)に変換すれば、いけるようです。

2つの文字列配列の要素同士を連結する(文字列配列の足し算) - Pythonのメモ帳

>>> a.astype(object) + b.astype(object)
array(['aあ', 'bい', 'cう'], dtype=object)
>>> a.astype(object) * 3
array(['aaa', 'bbb', 'ccc'], dtype=object)

 ということなので、全体を通して最初からobject型で配列を作っておけば良いと思いました。逆に、numpyの内部表現で持っておくメリットはあるのでしょうか・・・。