[解決!Python]データクラスのフィールドの初期化をカスタマイズするには解決!Python

dataclassesモジュールが提供するfield関数や__post_init__メソッドを使って、データクラスのインスタンスの初期化をより細かく制御する方法を紹介する。

» 2023年06月27日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「解決!Python」のインデックス

連載目次

from dataclasses import dataclass, field

# フィールドのデフォルト値を指定しない
@dataclass
class Person:
    name: str
    height: float
    weight: float

p = Person('no name', 150.0, 50.0# 全てのフィールドの初期値を指定する
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"

p = Person()  # TypeError:初期値の指定は省略できない

# field関数を使ってデフォルト値を指定する
@dataclass
class Person:
    name: str = field(default='no name')
    height: float = 150.0
    weight: float = 50.0

p = Person()
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"

# repr関数での文字列化に特定のフィールドを含めない
@dataclass
class Person:
    name: str = field(default='no name')
    height: float = field(default=150.0, repr=False)
    weight: float = field(default=50.0, repr=False)

p = Person()
repr(p)  # "Person(name='no name')"

# 同値性の比較対象から特定のフィールドを除外する
@dataclass
class Person:
    name: str = field(default='no name', compare=False)
    height: float = 150.0
    weight: float = 50.0

p0 = Person('kawasaki')
repr(p0)  # "Person(name='kawasaki', height=150.0, weight=50.0)"

p1 = Person('isshiki')
repr(p1)  # "Person(name='isshiki', height=150.0, weight=50.0)"

print(p0 == p1)  # True:nameフィールドは比較対象ではない

# 自動生成される__init__メソッドの初期化に特定のフィールドを含めない
@dataclass
class Person:
    name: str = field(default='no name', init=False)
    height: float = 150.0
    weight: float = 50.0

p = Person()
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"

# __init__メソッドに渡されなかったフィールドはクラス変数
# __init__メソッドに渡されたフィールドはインスタンス変数
Person.name = 'noname'
Person.height = 160.0
repr(p)  # "Person(name='noname', height=150.0, weight=50.0)"

# 初期化されずデフォルト値もないフィールドはオブジェクトに含まれなくなる
@dataclass
class Person:
    name: str = field(init=False)
    height: float = 150.0
    weight: float = 50.0

p = Person()  # AttributeError

# リストなどミュータブルなフィールドの初期化にはdefault_factoryを指定する
@dataclass
class Person:
    name: str = 'no name'
    height: float = 150.0
    weight: float = 50.0
    favor_list: list[str] = field(default_factory=list)

    def add(self, item):
        self.favor_list.append(item)

p0 = Person()
p0.add('apple')
repr(p0)

p1 = Person()
p1.add('banana')
repr(p1)

# クラス変数としてリストを持たせると複数のオブジェクトがそれを共有してしまう
class C:
    favor_list = []

    def add(self, item):
        self.favor_list.append(item)

c0 = C()
c0.add('apple')
print(c0.favor_list)  # ['apple']

c1 = C()
c1.add('banana')
print(c0.favor_list)  # ['apple', 'banana']

# __init__メソッドの呼び出し後に、__post_init__メソッドで独自の初期化を行う
@dataclass
class Person:
    name: str = 'no name'
    height: float = field(default=150.0, repr=False)
    weight: float = 50.0
    id: int = field(default=0, init=False)

    def __post_init__(self):
        self.__class__.id += 1
        self.id = self.__class__.id

p0 = Person()
repr(p0)  # "Person(name='no name', weight=50.0, id=1)"

p1 = Person()
repr(p1)  # "Person(name='no name', weight=50.0, id=2)"


パラメーター 説明
default このフィールドのデフォルト値を指定する
default_factory 引数なしで呼び出せる関数を指定。その戻り値がこのフィールドのデフォルト値となる
init 自動生成される__init__メソッドでの初期化にこのフィールドを含めるかどうかを指定。デフォルト値はTrue(含める)
repr 自動生成される__repr__メソッドでの文字列化にこのフィールドを含めるかどうかを指定。デフォルト値はTrue(含める)
hash 自動生成される__hash__メソッドでのハッシュ値の計算にこのフィールドを含めるかどうかを指定する。デフォルト値はNoneで、この場合はパラメーターcompareの値によって含めるかどうかが決まる
compare 自動生成される比較メソッド(__eq__メソッドなど)での計算にこのフィールドを含めるかどうかを指定する。デフォルト値はTrue(含める)
metadata マッピングオブジェクトかNoneを指定する(Noneの場合は空の辞書が指定されたものと見なされる)。サードパーティーでの拡張機構として使われることが前提となっているのでデータクラス自体では使われない
kw_only このフィールドの初期化をキーワード専用とするかどうかを指定。デフォルト値はdataclasses.MISSINGで、キーワード専用とはならない
dataclasses.field関数のパラメーター

dataclasses.dataclassデコレーター

 dataclassesモジュールが提供するdataclassデコレーターを使うと、クラス定義時に__init__メソッドなどの特殊メソッドを自動生成できるようになる(詳細については本連載の「データクラスを定義するには」を参照されたい)。

 以下はデータクラスを定義しているところだ。

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    height: float
    weight: float

p = Person('no name', 150.0, 50.0# 全てのフィールドの初期値を指定する
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"

p = Person()  # TypeError:初期値の指定は省略できない


 ここで型アノテーションを使って宣言している3つのフィールド「name」「height」「weight」は実際にはクラス変数となっている。ここでは初期値を与えていないので、Personクラスのインスタンスを生成する際にはこれらのフィールドの値を与える必要がある。

 もちろん、事前にデフォルト値を与えることも可能だ。

@dataclass
class Person:
    name: str = 'no name'
    height: float = 150.0
    weight: float = 50.0


 しかし、dataclassesモジュールが提供するfield関数を使うと、各フィールドがどのような特徴を持つかを細かく指定できる。以下は簡単なfield関数の使用例である。

@dataclass
class Person:
    name: str = field(default='no name')
    height: float = 150.0
    weight: float = 50.0

p = Person()
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"


 この例では、nameフィールドのデフォルト値をdefaultパラメーターで指定している。なぜdefaultパラメーターが必要かというと、「name: str =」に続けてデフォルト値を書くところにfield関数呼び出しがあるので、このように指定するしかないからだ。実際には、上のコードでやっていることは以下と変わらない。

@dataclass
class Person:
    name: str = 'no name'
    height: float = 150.0
    weight: float = 50.0

p = Person()
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"


 つまり、field関数にはデフォルト値の指定以外にもやれることがあるということだ。実際、field関数のパラメーターによって、各フィールドについての特性を決められる。

パラメーター 説明
default このフィールドのデフォルト値を指定する
default_factory 引数なしで呼び出せる関数を指定。その戻り値がこのフィールドのデフォルト値となる
init 自動生成される__init__メソッドでの初期化にこのフィールドを含めるかどうかを指定。デフォルト値はTrue(含める)
repr 自動生成される__repr__メソッドでの文字列化にこのフィールドを含めるかどうかを指定。デフォルト値はTrue(含める)
hash 自動生成される__hash__メソッドでのハッシュ値の計算にこのフィールドを含めるかどうかを指定する。デフォルト値はNoneで、この場合はパラメーターcompareの値によって含めるかどうかが決まる
compare 自動生成される比較メソッド(__eq__メソッドなど)での計算にこのフィールドを含めるかどうかを指定する。デフォルト値はTrue(含める)
metadata マッピングオブジェクトかNoneを指定する(Noneの場合は空の辞書が指定されたものと見なされる)。サードパーティーでの拡張機構として使われることが前提となっているのでデータクラス自体では使われない
kw_only このフィールドの初期化をキーワード専用とするかどうかを指定。デフォルト値はdataclasses.MISSINGで、キーワード専用とはならない
dataclasses.field関数のパラメーター

 表の一番上にあるdefaultは既に見たように、フィールドのデフォルト値を指定するものだ。

 次のdefault_factoryもフィールドのデフォルト値を指定するものだが、このパラメーターには引数なしで呼び出せる関数を指定する。データクラスのインスタンス生成時に指定した関数が呼び出され、その戻り値がフィールドのデフォルト値となる。

 init/repr/hash/comapreはdetaclassデコレーターによって自動生成される特殊メソッドでの処理(インスタンス変数の初期化、文字列化、ハッシュ値の計算)で対応するフィールドを使用するかどうかを指定するものだ。

 metadataはそのフィールドに関するメタデータを作成する。本稿では深く取り上げないので簡単な例だけ示しておこう。

from dataclasses import fields  # fields関数はデータクラスのフィールドを列挙する

@dataclass
class Person:
    name: str = field(default='no name')
    height: float = field(default=150.0, metadata={'foo': 'FOO'})
    weight: float = 50.0

p = Person()
for f in fields(p):
      print(f.metadata)
# 出力結果:
#{}
#{'foo': 'FOO'}
#{}


 metadataには上のコード例のようにマッピングオブジェクトを渡す。この例ではheightフィールドにのみメタデータを設定した。dataclasses.fields関数を引数に指定したデータクラスオブジェクトのフィールドを列挙できる。そこで上の例ではフィールドのmetadata属性を出力している。ただし、これはサードパーティーによる使用を考えてのものとなっているようだ。

 kw_onlyがTrueとしたフィールドはそのデータクラスのインスタンスを生成する際に、キーワード引数の形でしか引数を渡せなくなる。

 以下では上記のパラメーターの幾つかの使い方を例示する。

repr関数での文字列化に特定のフィールドを含めない

 データクラスを定義するとデフォルトでは__repr__特殊メソッドが生成される。このメソッドでインスタンスを文字列化する際に特定のフィールドをその対象に含めるかどうかを指定するのがreprパラメーターだ。以下に例を示す。

@dataclass
class Person:
    name: str = field(default='no name')
    height: float = field(default=150.0, repr=False)
    weight: float = field(default=50.0, repr=False)

p = Person()
repr(p)  # "Person(name='no name')"


 この例ではheightフィールドとweightフィールドについては「repr=False」としているので、これらはrepr関数での文字列化からは除外される。このインスタンスを生成して、repr関数に渡すと確かにこれらのフィールドの値が含まれていないことが分かる。

同値性の比較対象から特定のフィールドを除外する

 compareフィールドは、2つのインスタンスの同値性を比較する際に、特定のフィールドを比較対象に含めるかどうかを指定する。以下に例を示す。

@dataclass
class Person:
    name: str = field(default='no name', compare=False)
    height: float = 150.0
    weight: float = 50.0

p0 = Person('kawasaki')
repr(p0)  # "Person(name='kawasaki', height=150.0, weight=50.0)"

p1 = Person('isshiki')
repr(p1)  # "Person(name='isshiki', height=150.0, weight=50.0)"

print(p0 == p1)  # True:nameフィールドは比較対象ではない


 この例ではnameフィールドについて「compare=False」としている。つまり、名前は同値性の比較では使われないということだ。そこで、nameフィールドの値が異なり、他の2つのフィールドの値が同じ2つのオブジェクトを作成して、それらを比較したところ、確かに同じであると判断された。

自動生成される__init__メソッドの初期化に特定のフィールドを含めない

 initパラメーターは、自動生成される__init__メソッドでの初期化にそのフィールドを含めるかどうかを指定する。以下に例を示す。

@dataclass
class Person:
    name: str = field(default='no name', init=False)
    height: float = 150.0
    weight: float = 50.0

p = Person()
repr(p)  # "Person(name='no name', height=150.0, weight=50.0)"


 この例ではnameフィールドについては「default='no name'」として初期値を与える一方で、「init=False」として初期化の対象からは外している。取りあえず、このインスタンスを作成すると、3つのフィールドにはそれぞれ値が設定されているように見える。

 重要なのは、上のデータクラスの定義では3つのフィールドはあくまでも「クラス変数」であるという点だ。自動生成される__init__メソッドでは、インスタンス変数の初期化を行う。これにより、「self.フィールド名」としたときにクラス変数ではなく、インスタンス変数が参照されるようになる。そして、「init=False」としたフィールドでは、インスタンスが持つ属性(インスタンス変数)の初期化が省略されるということだ。

 この状態でPersonクラスが持つクラス変数の値を変更し、次に生成したインスタンスであるpをrepr関数で文字列化してみよう。

# __init__メソッドに渡されなかったフィールドはクラス変数
# __init__メソッドに渡されたフィールドはインスタンス変数
Person.name = 'noname'
Person.height = 160.0
repr(p)  # "Person(name='noname', height=150.0, weight=50.0)"


 このように、「init=False」としてインスタンス変数の初期化を省略したnameフィールドは変更後のクラス変数の値を持ち、その一方で、heightフィールドはインスタンス変数として初期化されているので、同じ名前のクラス変数を変更してもその変更がインスタンスに及んでいないことが分かる。

 なお、defaultパラメーターでデフォルト値の指定をせず、さらに__init__メソッドでの初期化の対象からも除外したフィールドはオブジェクトに含まれなくなる。以下に例を示す。

@dataclass
class Person:
    name: str = field(init=False)
    height: float = 150.0
    weight: float = 50.0

p = Person()  # AttributeError


リストなどミュータブルなフィールドの初期化にはdefault_factoryを指定する

 上でも見たが、データクラスの定義の時点では各フィールドはクラス変数として宣言されている。そのフィールドがリストのようなミュータブルなオブジェクトの場合、これは問題となることがある。データクラスではないが、以下の例を見てほしい。

class C:
    favor_list = []

    def add(self, item):
        self.favor_list.append(item)

c0 = C()
c0.add('apple')
print(c0.favor_list)  # ['apple']

c1 = C()
c1.add('banana')
print(c0.favor_list)  # ['apple', 'banana']


 クラスCはfavor_listをクラス変数として持っている。そして、このクラスのインスタンスを生成すると、このリストが複数のインスタンス間で共有されてしまうのだ。上の例ではc0とc1という2つのオブジェクトでは、それぞれが好きなものを追加しているが、これらが共有されるのが正しいときもあれば、そうではないときもある。通常のクラス定義であれば、__init__メソッドでの初期化時にインスタンスごとに所有する必要があるものはそのように初期化をすればよいが、データクラスでは__init__メソッドを自動生成できることがメリットの1つであり、1つのフィールドを独自に初期化するためだけに__init__メソッドの自動生成を省略するのはうれしくはない。

 こうしたフィールドの初期化にはdefault_factoryパラメーターで初期化に使用する関数を指定できる。以下に例を示す。

@dataclass
class Person:
    name: str = 'no name'
    height: float = 150.0
    weight: float = 50.0
    favor_list: list[str] = field(default_factory=list)

    def add(self, item):
        self.favor_list.append(item)

p0 = Person()
p0.add('apple')
repr(p0)

p1 = Person()
p1.add('banana')
repr(p1)


 この例ではdefault_factoryにlist関数を指定することで、インスタンスごとにfavor_listが空のリストで初期化されるようにしている。そのため、複数のインスタンスでそれぞれに好きなものを追加しても、それらが共有されることがなくなっている。

__init__メソッドの呼び出し後に、__post_init__メソッドで独自の初期化を行う

 最後に__init__メソッドでの初期化の後に、独自にフィールドの初期化を行うための方法である__post_init__メソッドを紹介する。このメソッドは__init__メソッドでの初期化に続けて呼び出される。

 ここでは、インスタンスにIDを割り振る例を示す。

@dataclass
class Person:
    name: str = 'no name'
    height: float = field(default=150.0, repr=False)
    weight: float = 50.0
    id: int = field(default=0, init=False)

    def __post_init__(self):
        self.__class__.id += 1
        self.id = self.__class__.id

p0 = Person()
repr(p0)  # "Person(name='no name', weight=50.0, id=1)"

p1 = Person()
repr(p1)  # "Person(name='no name', weight=50.0, id=2)"


 この例では、idフィールドには初期値を与え、__init__メソッドでの初期化から除外している。その一方で、__post_init__メソッドを定義して、その中でクラス変数であるID(self.__class__.id)の値を1増加された後で、それをインスタンス変数のID(self.id)に代入している。こうすることで、特定の値に固定されないデフォルト値をフィールドに与えられる。これを使えば、リストの初期化をインスタンスごとに独自に行うことも可能だろう。

「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。