Pythonのクラスとdataclassを理解する|属性・メソッド・データモデルの基本

「Pythonのクラスとdataclassを理解する|属性・メソッド・データモデルの基本」の内容を表す技術イラスト

部品名、寸法、圧力などを別々の変数で管理すると、データ同士の関係が分かりにくくなります。辞書でまとめる方法もありますが、キーの書き間違いを実行時まで見つけにくく、データに関係する計算処理も別の場所へ散らばりがちです。

クラスを使うと、関連するデータと処理を一つの型として表現できます。さらに、値を中心とする小さなデータモデルなら、標準ライブラリのdataclassesを使って定型コードを減らせます。

この記事では継承や複雑な設計パターンには進まず、小さな工学データを安全に表現する範囲へ絞ります。

目次

今日の到達点

この記事を読み終えると、次のことができるようになります。

  • オブジェクト、クラス、インスタンスの関係を説明できる
  • 属性とメソッドを持つクラスを定義できる
  • __init__()でインスタンスの初期状態を設定できる
  • selfがどのインスタンスを指すか理解できる
  • @dataclassで定型的な初期化や表示、比較を簡潔に書ける
  • 型ヒントと実行時の入力検証を区別できる
  • 辞書、関数、通常クラス、dataclassを目的に応じて選べる

オブジェクト・クラス・インスタンス

Pythonでは、数値、文字列、リスト、関数など、プログラムで扱う多くのものがオブジェクトです。オブジェクトは、値や状態と、それに対して行える操作を持ちます。

クラスは、新しい種類のオブジェクトを作るための定義です。クラスから実際に作られた個々のオブジェクトをインスタンスと呼びます。

たとえば「油圧シリンダ」というクラスを定義すると、寸法や圧力が異なる複数のインスタンスを作れます。

class HydraulicCylinder:
    pass


cylinder_a = HydraulicCylinder()
cylinder_b = HydraulicCylinder()

print(type(cylinder_a))
print(cylinder_a is cylinder_b)

実行結果です。

<class '__main__.HydraulicCylinder'>
False

cylinder_aとcylinder_bは同じクラスから作られていますが、別々のインスタンスです。

属性で状態を持たせる

属性は、インスタンスに関連付けられた値です。初期値の設定には、通常__init__()という特別なメソッドを使います。

class HydraulicCylinder:
    def __init__(self, name, bore_mm, pressure_mpa):
        self.name = name
        self.bore_mm = bore_mm
        self.pressure_mpa = pressure_mpa

インスタンスを作ると、Pythonが__init__()を呼び出します。

cylinder = HydraulicCylinder(
    "Cylinder A",
    80.0,
    14.0,
)

print(cylinder.name)
print(cylinder.bore_mm)
print(cylinder.pressure_mpa)

実行結果です。

Cylinder A
80.0
14.0

selfは、メソッドを呼び出しているインスタンス自身を受け取る引数です。self.bore_mmと書くことで、各インスタンスが自分の内径を持てます。

__init__()を「オブジェクトを新規作成する処理」と説明することがありますが、厳密には、すでに作られたインスタンスを初期化するメソッドです。入門段階では「インスタンス作成時に呼ばれ、初期状態を設定する」と理解すれば十分です。

メソッドで関連する計算を表す

メソッドは、クラス内に定義する関数です。シリンダの属性を使う計算をメソッドにすると、データと処理の関係が明確になります。

受圧面積と理論推力を次の式で求めます。\[ A=\frac{\pi D^2}{4} \]\[ F=pA \]

  • \(D\):内径 mm
  • \(A\):受圧面積 mm²
  • \(p\):圧力 MPa
  • \(F\):理論推力 N
  • \(1\ \mathrm{MPa}=1\ \mathrm{N/mm^2}\)
import math


class HydraulicCylinder:
    def __init__(self, name, bore_mm, pressure_mpa):
        self.name = name
        self.bore_mm = bore_mm
        self.pressure_mpa = pressure_mpa

    def theoretical_force_kn(self):
        area_mm2 = math.pi * self.bore_mm ** 2 / 4
        force_n = self.pressure_mpa * area_mm2
        return force_n / 1000


cylinder = HydraulicCylinder("Cylinder A", 80.0, 14.0)

print(cylinder.name)
print(f"{cylinder.theoretical_force_kn():.2f} kN")

実行結果です。

Cylinder A
70.37 kN

この値は摩擦、背圧、圧力損失、安全率を含まない理論値です。クラスへ計算をまとめても、物理的な前提の確認は別途必要です。

dataclassで定型コードを減らす

前のクラスでは、属性を初期化するために__init__()を手作業で書きました。値を保持することが中心のクラスでは、@dataclassを使うと簡潔に定義できます。

from dataclasses import dataclass
import math


@dataclass
class CylinderSpec:
    name: str
    bore_mm: float
    pressure_mpa: float = 14.0

    def theoretical_force_kn(self) -> float:
        area_mm2 = math.pi * self.bore_mm ** 2 / 4
        return self.pressure_mpa * area_mm2 / 1000

各行のname: strやbore_mm: floatはフィールドの名前と型ヒントです。pressure_mpaには既定値14.0を設定しています。

spec = CylinderSpec(
    name="Cylinder B",
    bore_mm=80.0,
)

print(spec)
print(f"{spec.theoretical_force_kn():.2f} kN")

実行結果です。

CylinderSpec(name='Cylinder B', bore_mm=80.0, pressure_mpa=14.0)
70.37 kN

単純な@dataclassは、フィールドをもとに__init__()、__repr__()、__eq__()などを既定で生成します。そのため、初期化処理を書かなくてもインスタンスを作れ、内容の分かる文字列表現も得られます。

同じクラスで、各フィールドの値が等しいインスタンスは、既定の比較で等しいと判定されます。

spec_a = CylinderSpec("Cylinder B", 80.0)
spec_b = CylinderSpec("Cylinder B", 80.0)

print(spec_a == spec_b)

実行結果です。

True

通常クラスが古く、dataclassが新しい上位互換という意味ではありません。独自の初期化や振る舞いが中心なら通常クラス、複数のフィールドを持つデータ表現が中心ならdataclassが有力です。

型ヒントは入力を自動検証しない

bore_mm: floatと書いても、Pythonが実行時に正数や有限値であることを自動確認するわけではありません。型ヒントは、コードを読む人や型検査ツールへの情報です。

データの制約は別に検証します。dataclassでは、生成された__init__()の後に呼ばれる__post_init__()を利用できます。

from dataclasses import dataclass
import math


@dataclass
class CylinderSpec:
    name: str
    bore_mm: float
    pressure_mpa: float

    def __post_init__(self) -> None:
        values = (self.bore_mm, self.pressure_mpa)

        if not all(math.isfinite(value) for value in values):
            raise ValueError(
                "bore_mm and pressure_mpa must be finite"
            )

        if self.bore_mm <= 0 or self.pressure_mpa <= 0:
            raise ValueError(
                "bore_mm and pressure_mpa must be positive"
            )

    def theoretical_force_kn(self) -> float:
        area_mm2 = math.pi * self.bore_mm ** 2 / 4
        return self.pressure_mpa * area_mm2 / 1000

不正な値からインスタンスを作ろうとすると、原因の分かる例外が発生します。

try:
    invalid_spec = CylinderSpec(
        name="Invalid",
        bore_mm=-50.0,
        pressure_mpa=7.0,
    )
except ValueError as error:
    print(f"Error: {error}")

実行結果です。

Error: bore_mm and pressure_mpa must be positive

検証をクラスへ置く利点は、どこからインスタンスを作っても同じ規則を適用できることです。ただし、ファイル読込時の形式確認など、クラスへ渡す前に行うべき検証もあります。

辞書やJSONへ変換する

dataclassのインスタンスは、そのままではJSON文字列ではありません。標準ライブラリのasdict()で辞書へ変換し、json.dumps()へ渡せます。

from dataclasses import asdict
import json


spec = CylinderSpec(
    name="Cylinder B",
    bore_mm=80.0,
    pressure_mpa=14.0,
)

spec_dict = asdict(spec)
payload = json.dumps(
    spec_dict,
    ensure_ascii=False,
    indent=2,
)

print(payload)

実行結果です。

{
  "name": "Cylinder B",
  "bore_mm": 80.0,
  "pressure_mpa": 14.0
}

JSONへ変換できたことは、単位や値の妥当性を保証しません。APIやファイルへ出力するときは、フィールド名、単位、必須項目、欠損値の表現を仕様として決めます。

辞書・関数・クラスの選び方

すべてのデータをクラスにする必要はありません。

選択肢向いている場面注意点
関数入力から結果を求める処理だけで、状態を持たない関連データが増えると引数が多くなりやすい
辞書JSONなど外部データを一時的・柔軟に扱うキーの打ち間違いや必須項目の不足に注意する
通常クラス独自の初期化や振る舞いが中心定型メソッドを自分で書く場合がある
dataclass名前付きフィールドを持つデータモデルが中心型ヒントだけでは値を検証しない

たとえば、推力を一度計算するだけなら関数で十分です。

import math


def calculate_force_kn(
    bore_mm: float,
    pressure_mpa: float,
) -> float:
    area_mm2 = math.pi * bore_mm ** 2 / 4
    return pressure_mpa * area_mm2 / 1000

部品ごとに名前、寸法、圧力をまとめ、検証や複数の計算も関連付けたいならクラスが役立ちます。構造が安定したデータを多く作るならdataclassが読みやすい選択肢です。

よくある失敗

クラスとインスタンスを混同する

class CylinderSpec:
    pass


print(CylinderSpec)

CylinderSpecはクラスそのものです。インスタンスを作るには括弧を付けます。

spec = CylinderSpec()

selfを書き忘れる

class Cylinder:
    def show_name():
        print("Cylinder")

インスタンスメソッドの第1引数には、慣例としてselfを書きます。

class Cylinder:
    def show_name(self):
        print("Cylinder")

dataclassなら検証不要だと思う

型注釈があっても、負の内径や無限大が自動的に拒否されるわけではありません。物理量の有限性、符号、上限、単位は用途に応じて検証します。

辞書をすべてクラスへ置き換える

外部JSONを読み込んで一度変換するだけなら、辞書のまま処理する方が単純なこともあります。再利用する規則や振る舞いがあるかを基準に判断します。

一つのクラスへ責務を詰め込む

入力、ファイル保存、計算、画面表示をすべて一つのクラスへ入れると変更しにくくなります。データとそれに密接な計算をクラスへまとめ、入出力は別の関数へ分けると見通しがよくなります。

練習問題

寸法データをdataclassで表す

部品名、長さmm、直径mmを持つShaftSpecを定義してください。

解答例です。

from dataclasses import dataclass


@dataclass
class ShaftSpec:
    name: str
    length_mm: float
    diameter_mm: float


shaft = ShaftSpec("Shaft A", 250.0, 30.0)
print(shaft)

実行結果です。

ShaftSpec(name='Shaft A', length_mm=250.0, diameter_mm=30.0)

断面積を求めるメソッドを追加する

ShaftSpecへ円形断面積をmm²で返すメソッドを追加してください。

解答例です。

from dataclasses import dataclass
import math


@dataclass
class ShaftSpec:
    name: str
    length_mm: float
    diameter_mm: float

    def area_mm2(self) -> float:
        return math.pi * self.diameter_mm ** 2 / 4


shaft = ShaftSpec("Shaft A", 250.0, 30.0)
print(f"{shaft.area_mm2():.2f} mm²")

実行結果です。

706.86 mm²

不正な直径を拒否する

diameter_mmが0以下ならValueErrorを発生させてください。

解答例です。

from dataclasses import dataclass


@dataclass
class ShaftSpec:
    name: str
    length_mm: float
    diameter_mm: float

    def __post_init__(self) -> None:
        if self.diameter_mm <= 0:
            raise ValueError("diameter_mm must be positive")

確認する観点は、インスタンス生成時に必ず同じ検証が行われることです。実務では長さや有限性も確認します。

まとめ

  • クラスは新しい種類のオブジェクトを定義する
  • クラスから作られた個々のオブジェクトがインスタンスである
  • 属性はインスタンスの状態、メソッドは関連する処理を表す
  • __init__()はインスタンス作成時の初期状態を設定する
  • selfはメソッドを呼び出しているインスタンスを指す
  • @dataclassは定型的な初期化、表示、比較を生成できる
  • dataclassにも独自メソッドを定義できる
  • 型ヒントは実行時の値検証を自動では行わない
  • __post_init__()で生成直後の検証を行える
  • 一度の計算なら関数、柔軟な外部データなら辞書、安定したデータと処理ならクラスを検討する

クラスを使う目的は、コードを難しくすることではありません。関連するデータと規則を同じ場所へまとめ、意味のある単位で再利用できるようにすることです。まずは小さなデータモデルから始め、辞書や関数で十分な場面では無理にクラスを増やさないことが大切です。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次