dataclass は、Pythonで主にデータを格納するためのクラスで、C言語などでは構造体に相当するようなデータ構造を、かんたんに定義できるようになっています。
たとえば、次の Person
は、名前と年齢を格納するdataclassです。
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
person1 = Person(name="パーソン太郎", age=20)
person2 = Person(name="パーソン次郎", age=30)
Python 3.10では、dataclass
に slots
引数が追加され、スロット を使ったクラスを定義できるようになりました(bpo-42269)。
スロットとは?¶
通常、クラスのインスタンスは、メンバー変数の名前と値を __dict__
という名前の辞書オブジェクトに格納します。
>>> class Foo:
... pass
...
>>> foo = Foo()
>>> foo.attr1 = "いぬ"
>>> foo.attr2 = "ねこ"
>>>
>>> print(foo.__dict__)
{'attr1': 'いぬ', 'attr2': 'ねこ'}
しかし、クラス定義中に __slots__
という名前で、使用する属性名のシーケンスを指定すると、__dict__
辞書が作成されなくなります。
>>> class Bar:
... __slots__ = ('attr1', 'attr2')
...
>>> bar = Bar()
>>> bar.attr1 = "うし"
>>> bar.attr2 = "うま"
>>> bar.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Bar' object has no attribute '__dict__'
スロットのメリット¶
通常、クラスのインスタンスを一つ作成すると、__dict__
に使用する辞書が一つ、新しく作成されます。
辞書オブジェクトはパフォーマンスと柔軟性重視なデータ構造で、あらかじめ余計にメモリを確保して、データの追加や削除を高速に行えるような仕組みになっています。しかし、dataclass
のように、決まった形式のデータを大量に保持するようなクラスでは、めったに属性の追加や削除は行いませんから、このために確保したメモリは無駄になってしまいます。
そこで、__slots__
を指定すると、インスタンスの属性値を辞書ではなく、スロットという配列形式で格納するようになり、辞書オブジェクトの作成に必要な処理時間と、メモリ使用量を削減できます。一方、__slots__
を使ったインスタンスで属性を参照する場合、数%程度速度が低下します。
dataclassのスロット¶
__slots__
形式の dataclass
は、次のように slots=True
を指定して定義します。
from dataclasses import dataclass
@dataclass(slots=True)
class Person2:
name: str
age: int
person1 = Person2(name="パーソン太郎", age=20)
person2 = Person2(name="パーソン次郎", age=30)
実際に実行時間とメモリ使用量を測定してみましょう。
__slots__
を使用しない場合は次のとおりです。
>>> import time,tracemalloc
>>> tracemalloc.start()
>>> f = time.time()
>>> persons = [Person(str(i), i) for i in range(1000000)]
>>> print(time.time()-f)
2.6621129512786865
>>> current, peak = tracemalloc.get_traced_memory()
>>> print(current, peak)
(243329878, 243330262)
処理時間は2.7秒、メモリ使用量は243MB程ですね。
__slots__
を使用するとこんな感じになります。
>>> f = time.time()
>>> persons = [Person2(str(i), i) for i in range(1000000)]
>>> print(time.time()-f)
1.7276349544525146
>>> current, peak = tracemalloc.get_traced_memory()
>>> print(current, peak)
(139330518, 139330902)
処理時間は1.7秒、メモリ使用量は139MBまで削減できました。
また、__dict__
辞書 を持たないインスタンスでは、あたらしい属性に値を代入できません。代入すると、次のようなエラーとなります。
>>> person = Person2("名前", "年齢")
>>> person.addr = "じゅうしょ"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person2' object has no attribute 'addr'
この例の Person
のようなクラスでは動的にあたらしく属性を追加することはあまりありませんので、特に問題にはなりません。むしろ、属性名間違いによるBug防止に役立つでしょう。