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に置き替えたとしても、 「生成」と「要素アクセス」の基本的な機能しか使っていないのであれば両者の書き方は変わらないので、定義さえ書き換えればよい。

programming/python/packages/namedtuple.txt · 最終更新: 2019/12/24 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0