namedtuple
標準モジュール collections または typing 内にある機能。c++でいう構造体のような機能である。
「複数の値をまとめて持てる」「イミュータブル」というTupleの性質を持ちつつ、「属性に名前を付けられる」という機能が追加されている。
これにより、Listなどのようにどこかで勝手に変更されないという安心感を持ちつつ、可読性を高めて要素にアクセスできる。 しかもドットアクセスなので、IDEが対応していれば補完が効く。
# 標準Tupleの生成と要素アクセス tup1 = ('pi', 3.14, [0, 1], min) print(tup1[0]) # => pi print(tup1[1]) # => 3.14 # NamedTupleの生成と要素アクセス from collections import namedtuple # まず(クラス名, 各カラム名)を渡してクラスを定義する TestTuple = namedtuple('TestTuple', 'name value list, func') # それを使って生成する tup2 = TestTuple('pi', 3.14, [0, 1], min) print(tup2.name) # => pi print(tup2.value) # => 3.14
実態はクラス
ちなみに実態はクラスなので、速度が向上するとかは望めない(むしろdict等と比べて遅い)。
# ... print(type(tup2)) # => <class '__main__.TestTuple'>
つまり、namedtuple自体はクラスファクトリーである。
namedtupleという名称も、むしろTupleより「クラスに、属性を勝手に変更できない機能を追加したもの」といった方が近い気がする。
Pickle化の注意点
Pickle化する際には他のユーザ定義クラスと同様の手間がかかる。つまり、
- 保存時はグローバルスコープに定義されていないといけない
- 保存されるのはクラス名と属性名のみで、クラス構造自体は保存されない
- unpickle時、そのスコープから同名のクラスが見えないといけない
- しかし、同名であれば、クラス構造が異なっても読み込めてしまう
書き出すとき、グローバルスコープからnamedtupleインスタンスが見えてないとエラー
def f(): Test = namedtuple('Test', 'name value') tup = Test('pi', 3.14) return tup tup2 = f() # 関数内で定義したNamedTupleインスタンスを返値で得る print(tup2.name) # その場で使うことはできる with open('some.pickle', 'wb') as wh: # pickleするとエラー pickle.dump(tup2, wh) # => _pickle.PicklingError: Can't pickle <class '__main__.Test'>: attribute lookup Test on __main__ failed
グローバルでNamedTupleを定義するとOK(保存は関数内・外どちらでも)
Test = namedtuple('Test', 'name value') def f(): tup = Test('pi', 3.14) with open('some.pickle', 'wb') as wh: pickle.dump(tup, wh) f()
読み込むとき、そのスコープから同名のクラスが見えないとエラー
何も情報を与えないと、Testクラスが見えないと言われる。
with open('some.pickle', 'rb') as rh: tup = pickle.load(rh) # => AttributeError: Can't get attribute 'Test' on <module '__main__' from '(実行したpythonスクリプトパス)'>
importする必要がある。明示的にはスクリプト内で使われてないので、IDEの設定によっては最適化で自動で除かれてしまう点に注意。
from (保存したpythonスクリプト) import Test with open('some.pickle', 'rb') as rh: tup = pickle.load(rh)
そのスクリプト内で同じものを定義しても通る。
しかし、あまり同じ定義が複数ファイルに分かれるのは良くない上に、属性名は違うものを定義しても通ってしまうので、使わない方がよい。
Test = namedtuple('Test', 'name2 value2') # 属性名を変えたTestを定義 with open('some.pickle', 'rb') as rh: tup = pickle.load(rh) # 読み込めはする print(tup.name) # => pi print(tup.name2) # => AttributeError: 'Test' object has no attribute 'name2' # スクリプト内で新たに生成したTestと属性名が食い違ってしまう tup2 = Test('r2', 1.414) print(tup2.name2) # => r2 print(tup2.name) # => AttributeError: 'Test' object has no attribute 'name'
さらに、namedtupleでもないクラスでも通ってしまう。
class Test: def greet(self): print('Hello!') with open('some.pickle', 'rb') as rh: tup = pickle.load(rh) # 読み込めてしまう
JSON化
Pickle化あたりの挙動が困る場合、容量や、変換時の処理時間は多少かかるが、dictを仲介してJSONなどで保存するのも手。
import json Test = namedtuple('Test', 'name value') tup = Test('pi', 3.14) # NamedTuple -> JSON tup_json = json.dumps(tup._asdict()) print(tup_json) # => {"name": "pi", "value": 3.14} # JSON -> NamedTuple tup2 = Test(**json.loads(tup_json)) print(tup2) # => Test(name='pi', value=3.14)
型指定
typing.NamedTuple
で定義すれば、型も指定できる。
from typing import NamedTuple # collections.namedtupleと同じ感じで指定する方法 Test1 = NamedTuple('Test1', [('name', str), ('value', float)]) tup1 = Test1('pi', 3.14) print(tup1) # classを継承して指定する方法 class Test2(NamedTuple): name: str value: float tup2 = Test2('pi', 3.14) print(tup2)
DataClass
Python3.7以降では、DataClassデコレータが新しく使えるようになり、ほぼ同等の機能が実現できる。
一部だけミュータブルにしたり、インスタンスを直接比較する際に使う属性を決めたり、細かな設定も可能になっているため、こちらの方が使いやすいかも。 (どちらも糖衣構文なだけで、できあがるものは、いろいろ設定が付与されたクラスである点に違いは無い)
速度は、単に生成するだけならNamedTupleが速い(といっても誤差の範囲)が、アクセスなどその他諸々の速度はDataClassが速い、という記事あり。
展開代入はNamedTupleにできてDataClassにできないらしいので、それを使いたい場合はNamedTupleを採用する価値があるが、基本、DataClassに取って代わられそう。
# NamedTuple Test1 = namedtuple('Test', 'name value') tup1 = Test1('pi', 3.14) a, b = tup1 # OK # DataClass @dataclass class Test2: name: str value: float tup2 = Test2('pi', 3.14) a, b = tup2 # TypeError: cannot unpack non-iterable Test2 object a, b = vars(tup2).values() # OK
NamedTupleを使っていたところをDataClassに置き替えたとしても、 「生成」と「要素アクセス」の基本的な機能しか使っていないのであれば両者の書き方は変わらないので、定義さえ書き換えればよい。