ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

Python - dataclass:データを格納するためのクラス

初めに

 dataclassはデータを格納するためのクラスです。通常のクラスとの違いや基本的な使い方をまとめています。

前提知識

 dataclassの説明に通常のclassの話をします。classについて先に学びたい方は下のリンク先をご覧ください。

fineworks-fine.hatenablog.com

dataclass

dataclassとは

 dataclassとは、データを格納するためのクラスです。dataclassesモジュールで提供されます。

dataclassとclassの比較

 同じ内容のPersonクラスを通常のclassとdataclassで作成してみます。

(通常のclass)

class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age

  def __repr__(self):
    return f"Person(name={self.name}, age={self.age})"

  def __eq__(self, other):
    return self.name == other.name and self.age == other.age

(dataclass)

from dataclasses import dataclass

@dataclass
class Person:
  name: str
  age: int

 このコードを見てわかる通り、dataclassには次のような有用性があります。

  • コードを簡潔に書くことができる
  • __init____repr__など特殊メソッドが自動生成される
  • アノテーションを使用できる

※型アノテーション:変数や関数の引数・戻り値に期待されるデータ型を明示的に記述すること

使い方

基本的な使い方
from dataclasses import dataclass

@dataclass
class Person:
  name: str
  age: int

person = Person("山田", 23)
print(f"{person.name}, {person.age}")  # 出力 : 山田, 23

 dataclassesモジュールからdataclassをインポートし、使用します。dataclassを作成するには、classの上に@dataclassデコレータを記述します。

デフォルト値

 フィールドには、デフォルト値を設定することもできます。

from dataclasses import dataclass

@dataclass
class Person:
  name: str
  age: int = 20  # デフォルト値を設定

# オブジェクトの生成
person1 = Person("田中")
person2 = Person("中澤", 32)

# 確認
print(f"{person1.name} は {person1.age} 歳")  # 田中 は 20 歳
print(f"{person2.name} は {person2.age} 歳")  # 中澤 は 32 歳

 引数を記述しない場合はデフォルト値が参照され、引数を記述した場合はその値が参照されます。

 注意点として、デフォルト値を設定するフィールドは、デフォルト値を設定しないフィールドの後に記述する必要があります。

from dataclasses import dataclass

@dataclass
class Person:
  name: str = "田中"
  age: int
  # これはエラー:デフォルト値を設定しているフィールドを前に記述されている

 ミュータブルな型(list、dict、set)の場合は注意が必要です。下の例のように参照元が同じになってしまい、意図していない処理をしてしまう可能性があります。

(例:二つのオブジェクトで参照元が同じ)

from dataclasses import dataclass

@dataclass
class Person:
  friends: list = []  # デフォルト値として空のリストを設定

# オブジェクトの生成
person1 = Person()
person2 = Person()

# person1.friendsとperson2.friendsは同じオブジェクトを参照
person1.friends.append("John")
print(person2.friends)  # ['John']と出力

 これを解決するには、field()関数を用います。

from dataclasses import dataclass, field

@dataclass
class Person:
  friends: list[str] = field(default_factory=list)

# オブジェクトの生成
person1 = Person()
person2 = Person()

# person1.friendsとperson2.friendsは異なるオブジェクト
person1.friends.append("John")
print(person1.friends)  # 出力 : ['John']
print(person2.friends)  # 出力 : []

 今回は、listのデフォルト値を設定するため、field(default_factory=list)としています。辞書の場合はfield(default_factory=dict)、集合の場合はfield(default_factory=set)とします。

 また、中身があるものをデフォルト値にする場合、次のように中身を返す関数をdefault_factoryに指定します。

from dataclasses import dataclass, field
from typing import Dict

def get_default_address() -> Dict[str, str]:
  return {"city": "東京", "prefecture": "東京都"}

@dataclass
class Person:
  address: Dict[str, str] = field(default_factory=get_default_address)

# オブジェクトの生成
person1 = Person()

# addressフィールドは新しいオブジェクトとして生成される
print(person1.address)  # {'city': '東京', 'prefecture': '東京都'}

 関数は無名関数(lambdaを用いて記述する)を用いてもよいです。

@dataclass
class Person:
  address: Dict[str, str] = field(default_factory=lambda: {"city": "東京", "prefecture": "東京都"})

fineworks-fine.hatenablog.com

==による処理

 classの場合とdataclassの場合で==による処理が異なります。

比較する対象
class オブジェクトの識別子(メモリ上のアドレス)を比較
dataclass フィールドの値を比較

 classの場合、オブジェクトの識別子を比較するため、クラス内のフィールドの値が全く同じ場合でもインスタンスが別であれば、Falseとなります。
 dataclassの場合、デフォルトで__eq__()メソッドが自動生成されるため、フィールドの値を比較します。すべてのフィールドが等しい場合、Trueとなります。

class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age

person1 = Person("John", 20)
person2 = Person("John", 20)

# オブジェクトの識別子を比較
print(person1 == person2)  # False
from dataclasses import dataclass

@dataclass
class Person:
  name: str
  age: int

person1 = Person("John", 20)
person2 = Person("John", 20)

# フィールドの値を比較
print(person1 == person2)  # True
frozon

 dataclassの引数には、frozonがあります。これはデフォルトではFalseになっていますが、Trueにすることで、読み込み専用のインスタンスを生成することができます。

from dataclasses import dataclass, field

@dataclass(frozen=True)
class Test:
  subject: str
  point: int

# オブジェクトの生成
test1 = Test("国語", 88)

test1.subject = "数学"      #エラー : dataclasses.FrozenInstanceError: cannot assign to field 'subject'
asdict()、astuple()

 asdict()関数はデータクラスを辞書に、astuple()関数はデータクラスをタプルに変換します。

asdict()関数)

from dataclasses import dataclass, asdict

@dataclass
class Person:
  name: str
  age: int

person = Person("John", 20)

# カスタムdict_factoryを使用
person_dict = asdict(person)

# 辞書の内容を確認
print(person_dict)  # {'name': 'John', 'age': 20}

astuple()関数)

from dataclasses import dataclass, astuple

@dataclass
class Person:
  name: str
  age: int

person = Person("John", 20)

# データクラスをタプルに変換
person_tuple = astuple(person)

# タプルの内容を確認
print(person_tuple)  # ('John', 20)