Python3.10の新機能 Python 3.10の新機能(その10) Dataclassでslotsが利用可能に

(その1) パターンマッチ(その2) with文のネスト(その3) エラーメッセージの改善(その4) | 演算子によるユニオン型の指定(その5) パラメータ仕様変数(その6) 明示的な型エイリアス(その7) ユーザ定義型ガード(その8) OpenSSL 1.1.1が必須に(その9) zip()関数に引数 strict を追加(その10) Dataclassでslotsが利用可能に(その11) その他の変更

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では、dataclassslots 引数が追加され、スロット を使ったクラスを定義できるようになりました(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防止に役立つでしょう。

Copyright © 2001-2021 python.jp Privacy Policy python_japan
Amazon.co.jpアソシエイト
Amazonで他のPython書籍を検索