PythonのクラスとOOP:__init__、self、メソッド、継承、dataclass
Pythonのクラスは、データ(フィールド)と処理(メソッド)を1つにまとめた設計図です。クラスを呼び出すとインスタンス(オブジェクト)が作られ、__init__ がフィールドを設定し、self はそのオブジェクト自身を指します。継承を使うと親クラスの機能を引き継いで一部だけ変えられ、@dataclass デコレータを付けると、データ中心のクラスに __init__、__repr__、__eq__ を自動生成できます。
Pythonのクラスは設計図です。どんなデータを持ち、どんな操作ができるかを定義します。インスタンス(オブジェクト)は、その設計図から作られた個別の製品です。Pythonのオブジェクト指向は、3つの要素を中心に組み立てられています。メソッドを持つクラス、継承、そして __init__ や __repr__ といった特殊メソッドです。ここでは、支出管理という1つの例を通して、すべてを順に見ていきます。
辞書ではなくクラスを使う理由
データは辞書にも保存できます。例えば {"title": "コーヒー", "amount": 250} です。しかし辞書は、どのキーが正しいのかを知りません。expense.get("amout", 0) のようなタイプミスでもエラーにはならず、0 が返るだけです。レポートは静かに誤った値を表示し始めます。
クラスのインスタンスでは、フィールドがあらかじめ決まっています。存在しない属性にアクセスすると、すぐに AttributeError が発生します。現在のPythonは、正しい名前の候補も提示してくれます。さらにクラスは、データと、そのデータに対する処理を1か所にまとめます。支出オブジェクトは税込み金額を自分で計算でき、レポートは自分で合計を出せます。
Pythonでクラスを作る
クラスは class キーワードで定義します。名前は、PEP 8に従ってCamelCase(大文字で単語を区切る形式)で書きます。最も単純なクラスは次のとおりです。
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
coffee = Expense("コーヒー", 250)
taxi = Expense("タクシー", 300)
print(coffee.title, coffee.amount) # コーヒー 250
Expense("コーヒー", 250) の呼び出しで、新しいインスタンスが作られます。coffee と taxi は同じ型の別々のオブジェクトで、それぞれが自分の値を持っています。型を確かめるには isinstance(coffee, Expense) を使います。結果は True です。
コンストラクタ __init__:インスタンスの初期化
__init__ はしばしばクラスのコンストラクタと呼ばれますが、正確には初期化メソッドです。オブジェクトそのものを作るのは __new__ で、__init__ はその直後に呼ばれ、渡された引数をフィールドに設定します。戻り値は必要ありません(厳密には None を返す必要があります)。
__init__ の中で入力値を検査すると、プログラムの中に不正なオブジェクトが作られるのを防げます。
class Expense:
def __init__(self, title, amount):
if amount < 0:
raise ValueError("金額は負の値にできません")
self.title = title
self.amount = amount
selfとは何か
self は、メソッドが現在扱っているオブジェクトそのものへの参照です。coffee.with_tax() と書くと、Pythonは内部で Expense.with_tax(coffee) を呼び出し、オブジェクトを最初の引数として渡します。そのため、メソッドの定義では self を先頭に書きますが、呼び出し時には書きません。
self は予約語ではなく、慣習的な名前です。技術的には別の名前も使えますが、誰もそうしません。初心者がよくやるミスは、メソッドの定義で self を書き忘れることです。すると TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given のようなエラーになります。
メソッドと特殊メソッド
メソッドは、クラスの中で定義された関数です。self を通じてオブジェクトのフィールドを参照します。
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
def with_tax(self, rate=0.2):
return self.amount * (1 + rate)
def __repr__(self):
return f"Expense({self.title!r}, {self.amount})"
coffee = Expense("コーヒー", 250)
print(coffee.with_tax()) # 300.0
print(coffee) # Expense('コーヒー', 250)
__repr__ がなければ、表示は <__main__.Expense object at 0x7f...> のようになり、デバッグには役立ちません。前後に2つずつアンダースコアが付いたメソッドは特殊メソッド(dunderメソッド)と呼ばれ、Pythonが自動的に呼び出します。__str__ はユーザー向けの表示、__eq__ は == による比較、__len__ は len(obj) の動作を決めます。
@classmethodと@staticmethod
通常のメソッドはインスタンスに対して働きます。@classmethod を付けたメソッドは、最初の引数としてクラス自身(cls)を受け取ります。代替コンストラクタとしてよく使われます。@staticmethod は、オブジェクトもクラスも受け取りません。論理的にはそのクラスに属する、普通の関数です。
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
@classmethod
def from_csv(cls, line):
title, amount = line.split(";")
return cls(title, float(amount))
@staticmethod
def is_valid_amount(value):
return value >= 0
e = Expense.from_csv("昼食;520")
print(e.amount) # 520.0
print(Expense.is_valid_amount(-1)) # False
デストラクタ __del__
Pythonには __del__ メソッドがあり、ガベージコレクタがオブジェクトを破棄する直前に呼ばれます。ただし、呼ばれるタイミングは保証されないため、リソースの解放をこれに頼ってはいけません。ファイルや接続には、with によるコンテキストマネージャか、明示的な close() メソッドを使ってください。
クラス属性とインスタンス属性
self.name = ... で設定されるフィールドは、個々のオブジェクトに属します。クラス本体に直接書いた変数は、クラス属性であり、すべてのインスタンスで共有されます。定数をまとめて持つときに使います。
class Expense:
CURRENCY = "JPY" # クラス属性(定数)
def __init__(self, title, amount):
self.title = title # インスタンス属性
self.amount = amount
a = Expense("コーヒー", 250)
b = Expense("お茶", 150)
print(a.CURRENCY, Expense.CURRENCY) # JPY JPY
危険な落とし穴は、変更可能な値をクラス属性にすることです。クラス本体に rows = [] と書き、self.rows.append(...) で追加すると、リストがすべてのオブジェクトで共有されてしまいます。変更可能なフィールドは、必ず __init__ の中で作りましょう。
オブジェクトのフィールドは vars(obj) または obj.__dict__ で確認できます。文字列の名前で属性を取り出すには getattr(obj, "amount") を使います。
プロパティ:@property
プロパティは、外から見ると括弧なしの通常のフィールドのように読めるメソッドです。計算で求められる値に便利です。レポートの合計を別のフィールドとして保存すると、更新を忘れる恐れがありますが、プロパティにしておけば常に最新の値が得られます。
class Report:
def __init__(self):
self.rows = []
def add(self, expense):
self.rows.append(expense)
@property
def total(self):
return sum(e.amount for e in self.rows)
r = Report()
r.add(Expense("コーヒー", 250))
r.add(Expense("タクシー", 300))
print(r.total) # 550
書き込みも制御したい場合は、@total.setter でセッターを追加します。セッターがないと、r.total = 0 という代入は AttributeError になります。
カプセル化についても触れておきます。Pythonには、厳密な意味での非公開フィールドはありません。先頭に1つのアンダースコア(_cache)を付けるのは「外から触らないでください」という約束です。2つのアンダースコア(__secret)を付けると、名前が変換されます(ネームマングリング)。これにより、サブクラスが同じ名前のフィールドを誤って上書きするのを防げます。
Pythonの継承
継承を使うと、親クラスを土台にして子クラスを作れます。子は親のフィールドとメソッドを受け継ぎ、必要なものだけを上書きできます。親クラスは括弧の中に書きます。
class MonthlyReport(Report):
def __init__(self, month):
super().__init__() # 親の __init__ を呼ぶ
self.month = month
def add(self, expense): # メソッドの上書き
print(f"[{self.month}] + {expense}")
super().add(expense) # 親の動作を維持する
m = MonthlyReport("10月")
m.add(Expense("コーヒー", 250)) # [10月] + Expense('コーヒー', 250)
print(m.total) # 250 — プロパティも継承されている
super() は、親クラスのメソッドを呼び出すための代理オブジェクトを返します。子の __init__ で super().__init__() を書き忘れるのは典型的なミスです。すると rows フィールドが作られず、最初の add が AttributeError で失敗します。
継承関係は issubclass(MonthlyReport, Report) で確認できます。Python 3のすべてのクラスは、暗黙のうちに object を継承しています。だからこそ、__repr__ や __eq__ などの基本的な特殊メソッドが使えるのです。Pythonは多重継承(class C(A, B))にも対応しています。メソッドの探索順序は C.__mro__ で確認できます。
抽象クラス
抽象クラスはインターフェースを定義します。子クラスが必ず実装しなければならないメソッドを決めるのです。抽象クラスのインスタンスは作れません。この仕組みは abc モジュールが提供します。
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, report): ...
class CsvExporter(Exporter):
def export(self, report):
return "\n".join(f"{e.title};{e.amount}" for e in report.rows)
Exporter() # TypeError: Can't instantiate abstract class Exporter ...
抽象メソッドをすべて実装していない子クラスは、やはり抽象のままです。そのオブジェクトを作ろうとすると、同じエラーになります。
深い継承より、コンポジション(合成)
継承を覚えると、基本の支出、金額の支出、円建ての支出、というように5段階の階層を作りたくなります。Pythonでそのような書き方はあまりしません。継承は「〜である」(is-a)の関係を表し、コンポジションは「〜を持つ」(has-a)の関係を表します。レポートは支出のリストではありません。支出のリストを rows フィールドとして持っています。
実践的なルールは、別のオブジェクトの機能を使いたいだけなら、継承せずにフィールドとして持たせることです。データにはdataclassで十分なことが多く、ロジックは通常の関数に分けられます。
dataclass:定型コードのないクラス
主にデータを保持するクラスでは、__init__、__repr__、__eq__ を手で書くのは面倒です。dataclasses モジュールの @dataclass デコレータは、フィールドのアノテーションからこれらを自動生成します。
from dataclasses import dataclass, field
@dataclass
class Expense:
title: str
amount: float
tags: list[str] = field(default_factory=list)
def __post_init__(self):
if self.amount < 0:
raise ValueError("金額は負の値にできません")
e = Expense("コーヒー", 250)
print(e) # Expense(title='コーヒー', amount=250, tags=[])
print(e == Expense("コーヒー", 250)) # True
以前は __init__ の中に書いていた値の検査は、__post_init__ に移します。このメソッドは、生成された初期化メソッドの直後に呼ばれます。変更可能なデフォルト値は field(default_factory=list) で指定します。tags: list = [] と書くと、dataclass がこれを禁止し、ValueError を出します。
frozenとslots
frozen=True を指定すると、オブジェクトは変更できなくなります。slots=True(Python 3.10以降)を指定すると __slots__ が作られ、メモリを節約できます。凍結されたオブジェクトのフィールドを書き換えようとすると、FrozenInstanceError になります。値が変わったオブジェクトが必要なら、コピーを作ります。
from dataclasses import dataclass, replace
@dataclass(frozen=True, slots=True)
class Expense:
title: str
amount: float
e = Expense("コーヒー", 250)
# e.amount = 300 -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2) # Expense(title='コーヒー', amount=250) Expense(title='コーヒー', amount=300)
Python 3.13からは、同じことを汎用の copy.replace() 関数でもできます。凍結されたdataclassのオブジェクトは、__hash__ を持つので、集合に入れたり辞書のキーに使ったりできます。
新しい機能:Self、@override、__slots__
クラスの型付けには、役立つ道具が2つあります。typing.Self(Python 3.11、PEP 673)は「同じクラスのオブジェクトを返す」ことを表します。継承されたメソッドでも正しく動きます。@override デコレータ(Python 3.12、PEP 698)は、親クラスのメソッドを上書きすることを明示します。名前を打ち間違えた場合、mypyやpyrightが「基底クラスに該当するメソッドがない」と指摘してくれます。
from typing import Self, override
class Report:
def __init__(self):
self.rows = []
def add(self, e) -> Self:
self.rows.append(e)
return self # メソッドチェーンで呼べる
class MonthlyReport(Report):
@override
def add(self, e) -> Self:
return super().add(e)
Python 3.15では、限定的な制約が解消されました。tuple のサブクラスで空でない __slots__ が使えるようになり、どのクラスの __slots__ にも __dict__ と __weakref__ を指定できます。Python 3.14では、タプルを継承したクラスでこの宣言を書くと、TypeError: nonempty __slots__ not supported for subtype of 'tuple' が出ていました。
クラスでよくあるミス
| ミス | 症状 | 対処 |
|---|---|---|
メソッドで self を書き忘れる | TypeError: ... takes 0 positional arguments but 1 was given | メソッドの第1引数は必ず self |
__init__ で self. を付けずに代入する | 値が __init__ の後に残らない | self.amount = amount と書く |
| リストをクラス属性にする | データがオブジェクトの間で共有される | __init__ 内、または default_factory でリストを作る |
super().__init__() を呼ばない | 親のフィールドで AttributeError | 親の初期化メソッドを呼ぶ |
| プロパティを括弧付きで呼ぶ | TypeError: 'int' object is not callable | 括弧なしで r.total と読む |
まとめ
Pythonのオブジェクト指向は、シンプルなところから始まります。クラスはフィールドとメソッドを定義し、__init__ がフィールドを設定し、self は現在のオブジェクトを指します。継承と super() で機能を拡張し、抽象クラスでインターフェースを定め、@property で計算される値を提供します。データ中心のクラスには @dataclass を使い、深い継承階層よりコンポジションを選びましょう。クラスとdataclassは、講座の講義で支出管理ツールを例に実践的に扱っています。
よくある質問
クラスとインスタンスの違いは何ですか?
クラスは型の定義で、どんなフィールドとメソッドがあるかを決めます。インスタンスは、Expense("コーヒー", 250) のようにクラスを呼び出して作られる実体で、具体的な値を持ちます。
クラスに __init__ を書く必要はありますか?
必須ではありません。__init__ を書かなければ、object から継承したものが使われ、引数は受け取れません。dataclass の場合は、デコレータが自動生成します。
__str__ と __repr__ の違いは何ですか?
__repr__ は開発者やデバッガ向けの正確な表現で、__str__ は print やf文字列でユーザーに見せる表示です。__str__ がなければ、Pythonは __repr__ を使います。
dataclass と通常のクラスはどう使い分けますか?
主にデータを保持し、__init__、__repr__、比較が自動で欲しい場合は dataclass が向いています。初期化時の複雑なロジックや、データより振る舞いが重要な場合は、通常のクラスが適しています。
Pythonにプライベートなフィールドはありますか?
厳密な非公開はありません。先頭のアンダースコア1つは「外部で使わない」という慣習で、2つのアンダースコアはネームマングリングにより、サブクラスによる誤った上書きを防ぎます。