「同じ名前のメソッドを持つクラスをきれいにそろえたい」「使う人に“このメソッドは必ず作ってね”と約束したい」。 そんなときに役立つのが、Pythonの抽象クラス(Abstract Base Class: ABC)と抽象メソッドです。
難しそうに聞こえるかもしれませんが、考え方はシンプルです。抽象クラスは“設計図”、抽象メソッドは“必ず実装してほしい項目”だと思ってください。
「なぜわざわざそんな回り道を?」と思うかもしれません。
理由は2つ。
1つ目は、実装の抜け漏れを防ぐこと。
2つ目は、クラス間で共通のインターフェース(同じメソッド名・同じ使い方)をそろえられることです。
結果として、読みやすく、差し替えやすく、テストしやすいコードになります。
今回は抽象クラスと抽象メソッドについて詳しく解説します。
Pythonではabc
モジュールを使って抽象クラスを作ります。
目印としてABC
を継承し、抽象メソッドには@abstractmethod
デコレータを付けます。
なお、Pythonでは「抽象メソッドが残っているクラスはインスタンス化できない」という仕様です。
逆にいうと、ABC
を継承していても抽象メソッドがなければ、通常のクラスとしてインスタンス化できます。
「インターフェースとしての約束を作りたい」「継承先に必ず実装してほしいメソッドがある」――そんなときに抽象クラスを選びましょう。
以下は、図形の「面積」と「周囲の長さ」を計算する設計です。 共通の約束を Shape 抽象クラスが定め、具体的な計算は Circle(円)と Rectangle(長方形)が担当します。
from abc import ABC, abstractmethod
# 抽象クラスの定義
class Shape(ABC):
@abstractmethod
def area(self):
"""面積を計算するメソッド"""
pass
@abstractmethod
def perimeter(self):
"""周囲の長さを計算するメソッド"""
pass
# CircleクラスはShapeクラスを継承
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * (self.radius ** 2)
def perimeter(self):
return 2 * 3.14 * self.radius
# RectangleクラスはShapeクラスを継承
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# インスタンスを作成
circle = Circle(5)
rectangle = Rectangle(4, 6)
# 面積と周囲の長さを表示
print(f"Circle Area: {circle.area()}")
print(f"Circle Perimeter: {circle.perimeter()}")
print(f"Rectangle Area: {rectangle.area()}")
print(f"Rectangle Perimeter: {rectangle.perimeter()}")
まず、Shape
は“図形なら必ず持つべき操作”を定義しています。ここではarea()
とperimeter()
がそれにあたります。
どちらも@abstractmethod
が付いているため、Shape
自身はインスタンス化できません。「図形は面積と周囲の長さを計算できるべし」という約束だけを宣言している、まさに設計図の役目です。
次に、Circle
とRectangle
がその約束を具体化します。Circle
は半径から円の面積と周長を計算し、Rectangle
は幅と高さから値を出します。どちらのクラスも、area()
とperimeter()
という“同じ名前・同じ意味”のメソッドを備えているため、呼び出し側はクラスの種類を意識せずに同じ書き方で扱えます。この「同じメソッド名で、違う中身が動く」性質はポリモーフィズム(多態性)と呼ばれ、オブジェクト指向ではとても重要です。
試しに、Shape()
を直接インスタンス化しようとするとどうなるでしょう?Pythonは「抽象メソッドが未実装だよ」と教えてくれます。これにより、必要なメソッドの作り忘れを早期に検知できます。
「本当にインスタンス化できないの?」と気になったら、次の短い実験をしてみましょう。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
try:
s = Shape() # ここでエラー
except TypeError as e:
print(type(e).__name__, ":", e)
また、サブクラスで実装を一つでも忘れると、やはりインスタンス化の時点でエラーになります。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class BadCircle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14 * self.r ** 2
# perimeter を実装し忘れ!
try:
bad = BadCircle(3) # ここで TypeError
except TypeError as e:
print(type(e).__name__, ":", e)
この動作は、抽象クラスが「実装の抜け漏れ」を自動で見張ってくれることを意味します。安心ですね。
抽象クラスは必ずこのメソッドを実装してくださいという約束を強制できるため、チーム開発や大きめのプロジェクトで特に威力を発揮します。
「それぞれ自由に実装していいよ」だけだと、開発者ごとにメソッド名や呼び出し方がバラバラになりがちです。 そうなると、呼び出し側のコードがクラスごとに条件分岐だらけになってしまい、保守が難しくなります。
そこで抽象クラスを使うと、インターフェースを揃えつつ抜け漏れを防止できます。 実際に役立つのは次のような場面です。
connect()
, send()
, close()
といった共通メソッドを強制したいとき。update()
, render()
のようにフレームごとの処理を統一したいとき。load()
, transform()
, save()
を必ず用意する設計にしたいとき。こうした「共通のインターフェース」を揃えると、差し替えがスムーズになります。たとえば、Circle
からRectangle
に変えても、呼び出し側のshape.area()
という書き方は変わりません。テストコードも再利用しやすくなります。
抽象クラスは「同じメソッド名で異なる実装を呼び分ける」ポリモーフィズムを支えます。
呼び出し側はshape.area()
だけを知っていればよく、中身の実装(円か四角か)は意識しなくて済みます。
また、Python 3.8以降では「プロトコル」という“振る舞いベースのインターフェース”も利用できます。
継承ではなく「このメソッドを持っているならOK」という考え方で、用途に応じて抽象クラスと使い分けます(プロトコルは別セクションで解説します)。
今回学習した内容をまとめます。
abc.ABC
を継承し、@abstractmethod
で“必ず実装すべきメソッド”を示します。 「自分の設計で“約束”を強くしたい」と感じたら、まずは小さな抽象クラスを一つ作ってみましょう。最初は面倒に見えても、プロジェクトが大きくなるほど、その効果を実感できるはずです。