【Python入門】dataclass①

Python

1.デコレータ

関数に「追加の機能」を後付けする仕組みをデコレータという。
コードの上に @xxx と書くことで、その関数・メソッドに追加処理を「挟み込む」ことができる。
以下よく使う標準デコレータ一覧。※他のものも掲載した一覧は別投稿でやる。

デコレータ説明使用例
@propertyゲッターを定義オブジェクトの属性アクセス
@x.setterセッターを定義属性への代入時処理
@x.deleterデリータを定義del 文で属性削除
@classmethodcls を受け取るクラスメソッド別コンストラクタ、クラス変数操作
@staticmethodself/cls を受け取らない静的メソッドユーティリティ関数をクラス内に置く

2.dataclass とは?

dataclasses モジュールの @dataclass デコレータを使うと、

  • __init__(コンストラクタ)
  • __repr__(デバック用。インスタンス内容を表示してくれる。)
  • __eq__(== 比較)

などを 自動で作ってくれる クラス用の仕組みです。

from dataclasses import dataclass

@dataclass
class Person:
    name: str #dataclassを使った表現。「属性名:データ型」で属性とデータ型を定義。
    age: int  #「self.age=age」等で作る必要がない。

p = Person("Alice", 20)
print(p)          # Person(name='Alice', age=20)
print(p.name)     # Alice

クラスに「データ(属性)」がメインのときに特に有効です。

3.カプセル化 + dataclass の基本形

◼ プロパティでバリデーションするパターン

from dataclasses import dataclass, field

@dataclass
class Person:
    _age: int = field(repr=False)  # repr に表示したくないので repr=False

    @property
    def age(self) -> int: #「関数()->型名」とすることで返却値の型を表す。※強制力無
        return self._age

    @age.setter
    def age(self, value: int) -> None: #「関数()->None」でなにも返さない。
        if value < 0:
            raise ValueError("年齢は0以上にしてください")
        self._age = value

p = Person(20)
print(p.age)   # 20
p.age = 30     # OK
# p.age = -5   # ValueError
  • field(repr=False) で print(p) したときに表示させない
  • 内部的には _age で持ちつつ、外からは age でアクセス

普通の class とやることは同じですが、データの宣言がスッキリします。

4.クラスメソッド + dataclass

from dataclasses import dataclass

@dataclass
class Product:
    price: int
    tax_rate: float = 0.1   # クラス共通のデフォルト税率

    def total(self) -> int:
        """税込価格を返すインスタンスメソッド"""
        return int(self.price * (1 + self.tax_rate))

    @classmethod
    def with_tax_rate(cls, price: int, tax_rate: float) -> "Product":
 #あるクラスのオブジェクトを返すときは「->"クラス名"」で表現する。今回は自クラス。
        """税率を指定して Product を生成するクラスメソッド"""
        return cls(price=price, tax_rate=tax_rate)

    @staticmethod
    def is_valid_price(price: int) -> bool:
        """価格として妥当かをチェックする静的メソッド"""
        return price >= 0

使い方:

# 通常の生成
p1 = Product(1000)
print(p1.total())          # 1100

# クラスメソッドで生成
p2 = Product.with_tax_rate(2000, 0.08)
print(p2.total())          # 2160

# 静的メソッド
print(Product.is_valid_price(100))   # True
print(Product.is_valid_price(-10))   # False

ポイント:

  • price: int のように 型ヒントを書くと同時にフィールド宣言になる
  • クラスメソッド・静的メソッドは 普通の class と同じように書ける
  • dataclass は「データ定義を簡潔にする」ものなので、ロジック部分(total, with_tax_rate, is_valid_price)はそのまま使える感じです

5.クラス変数 + クラスメソッド + dataclass

from dataclasses import dataclass, field

@dataclass
class Dog:
    name: str
    count: int = 0

    def __post_init__(self):
        # dataclass では __init__ の後に __post_init__ が呼ばれる
        Dog.count += 1

    @classmethod
    def how_many(cls) -> int:
        return cls.count

使い方:

d1 = Dog("Pochi")
d2 = Dog("Koro")
print(Dog.how_many())  # 2
  • dataclass で自動生成された __init__ の後に
    追加処理をしたいときは __post_init__ を使うのが定石です。

6.静的メソッド + dataclass

純粋な「ユーティリティ関数」をクラス内にまとめたい場合:

from dataclasses import dataclass

@dataclass
class MathUtil:
    """値は持たないけど、便利関数をまとめたクラス"""

    @staticmethod
    def add(a: int, b: int) -> int:
        return a + b

    @staticmethod
    def is_even(x: int) -> bool:
        return x % 2 == 0

使い方:

print(MathUtil.add(3, 5))     # 8
print(MathUtil.is_even(4))    # True

dataclass を付ける意味はほとんどないですが、
「同じカテゴリの関数をまとめる箱」として使えます。

7.frozen=True で「完全なカプセル化(実質不変)」に近づける

値を一切変えさせたくないデータには frozen=True を使えます。

from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    host: str
    port: int

cfg = Config("localhost", 8080)
# cfg.port = 9000  # エラーになる(変更不可)
  • 不変オブジェクト(イミュータブル) として扱える
  • 「設定情報」「ID など絶対変わらない値」に向いています

8.まとめ

  • @dataclass を使うと、データ中心のクラスが圧倒的に短くキレイに書ける
  • カプセル化は field() + @property で実現できる
  • クラスメソッド・静的メソッドは 普通の class と同じ書き方
  • 追加処理は __post_init__ に書く
  • frozen=True で「変更不可」な設定用クラスが作れる
タイトルとURLをコピーしました