Pythonで継承を禁止したクラスを作る
これはなに
Pythonで具象クラスを継承禁止にする方法ないかな~と思って調べたときのメモ。意外とない。
結論
- Python3.6以降のバージョン : 方法2か方法3がシンプルで良さそう
- Python3.8以降のバージョン : 方法2か方法3に加えて、方法5を併用したい
- Python3.6以前のバージョン : 方法4が使える
前提
下記のように、抽象クラスとしてShape
クラスを実装し、Shape
クラスを継承したRectangle
クラスを実装する。
import abc
class Shape(abc.ABC):
@abc.abstractmethod
def calculate_area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
super().__init__()
self._width = width
self._height = height
def calculate_area(self) -> float:
return self._width * self._height
このRectangle
クラスを継承できないようにする。具体的には、下記のようなSquare
クラスを作成できないようにする。
class Square(Rectangle):
def __init__(self, side: float):
super().__init__(side, side)
self._side = side
def calculate_area(self) -> float:
return self._side**2
__init_subclass__
を使う
方法1 |継承を禁止したいクラスの__init_subclass__
メソッドをオーバーライドし、例外を発生させる。この方法はPython3.6以降で使える。
class Rectangle(Shape):
+ def __init_subclass__(cls, **kwargs: Any):
+ super().__init_subclass__(**kwargs)
+ raise TypeError("Rectangleクラスの継承は禁止されています。")
+
def __init__(self, width: float, height: float):
super().__init__()
self._width = width
self._height = height
def calculate_area(self) -> float:
return self._width * self._height
__init_subclass__
は、サブクラスが作成されるたびに呼び出される特殊メソッドである。これを使い、サブクラスが作成されたときに例外を発生させることで、継承を禁止できる。
上記のRectangle
クラスをclass Square(Rectangle)
のように継承しようとすると、下記のように例外が発生する。
$ /usr/local/bin/python /workspaces/python-playground/src/main.py
Traceback (most recent call last):
File "/workspaces/python-playground/src/main.py", line 25, in <module>
class Square(Rectangle):
File "<frozen abc>", line 106, in __new__
File "/workspaces/python-playground/src/main.py", line 14, in __init_subclass__
raise TypeError("Rectangleクラスの継承は禁止されています。")
TypeError: Rectangleクラスの継承は禁止されています。
__init_subclass__
を書き換えたクラスを継承する
方法2 | 方法1のように__init_subclass__
メソッドを書き換えればよいため、次のように__init_subclass__
を書き換えたクラスを継承しても、同じ効果が得られる。
+ from typing import Any
+ class NonInheritableClass:
+ def __init_subclass__(cls, **kwargs: Any):
+ super().__init_subclass__(**kwargs)
+ raise TypeError(f"{cls.__name__}クラスの継承は禁止されています。")
- class Rectangle(Shape):
+ class Rectangle(Shape, NonInheritableClass):
def __init__(self, width: float, height: float):
super().__init__()
self._width = width
self._height = height
def calculate_area(self) -> float:
return self._width * self._height
この方法でも、方法1と同様に、Rectangle
クラスを継承しようとすると例外が発生する。
$ /usr/local/bin/python /workspaces/python-playground/src/main.py
Traceback (most recent call last):
File "/workspaces/python-playground/src/main.py", line 17, in <module>
class Rectangle(Shape, NonInheritableClass):
File "<frozen abc>", line 106, in __new__
File "/workspaces/python-playground/src/main.py", line 8, in __init_subclass__
raise TypeError(f"{cls.__name__}クラスの継承は禁止されています。")
TypeError: Rectangleクラスの継承は禁止されています。
個人的には、方法1よりもこの方法のほうが可読性と再利用性が高いと思う。
final_class
パッケージを使う
方法3 | final_class
パッケージを使うと、継承を禁止したいクラスをデコレータで修飾することで、継承を禁止できる。pip
でインストールできる。このパッケージはPython3.6以降で使える。
pip install final-class
使い方は、下記のようにfinal
デコレータを使うだけである。
+ from final_class import final
+ @final
class Rectangle(Shape):
def __init__(self, width: float, height: float):
super().__init__()
self._width = width
self._height = height
def calculate_area(self) -> float:
return self._width * self._height
このパッケージのソースコードを読むとわかるが、内部的には__init_subclass__
メソッドでエラーを発生させている。
この方法もシンプルで良い方法だと思う。
方法4 | メタクラスを使う
Python3.6以前のバージョンでは、__init_subclass__
メソッドが使えない。その場合は、メタクラスを使う方法がとれる。下記のように、NonInheritableMeta
というメタクラスを作成し、Rectangle
クラスにmetaclass
引数で指定する。
+ class NonInheritableMeta(abc.ABCMeta):
+ def __init__(
+ cls,
+ __name: str,
+ __bases: tuple[type, ...],
+ __namespace: dict[str, Any],
+ ):
+ super().__init__(__name, __bases, __namespace)
+ for b in __bases:
+ if isinstance(b, NonInheritableMeta):
+ raise TypeError(f"{b.__name__}クラスの継承は禁止されています。")
- class Rectangle(Shape):
+ class Rectangle(Shape, metaclass=NonInheritableMeta):
def __init__(self, width: float, height: float):
super().__init__()
self._width = width
self._height = height
def calculate_area(self) -> float:
return self._width * self._height
この方法でも、次のようにRectangle
クラスの継承時に例外が発生する。
$ /usr/local/bin/python /workspaces/python-playground/src/main.py
Traceback (most recent call last):
File "/workspaces/python-playground/src/main.py", line 34, in <module>
class Square(Rectangle):
File "/workspaces/python-playground/src/main.py", line 15, in __init__
raise TypeError(f"{b.__name__}クラスの継承は禁止されています。")
TypeError: Rectangleクラスの継承は禁止されています。
__init_subclass__
を使う方法と比べると、メタクラスを用いる方法は少し複雑である。そのため、__init_subclass__
を使う方法が使えない場合にのみ有効な方法だと思う。
final
デコレータを使う
方法5 | Python3.8以降では、final
デコレータを使うことで、mypy
などの静的解析ツールでエラーメッセージを出せる。
import abc
+ from typing import final
+ @final
class Rectangle(Shape):
def __init__(self, width: float, height: float):
super().__init__()
self._width = width
self._height = height
def calculate_area(self) -> float:
return self._width * self._height
上記のコードだと、VS CodeのPylance
なら次のエラーメッセージが出る。
基底クラス "Rectangle" は final とマークされており、サブクラス化できません
この方法は、これまで紹介した方法とは異なり、静的解析でエラーメッセージを出せるという利点がある。しかし、この方法は実行時にはエラーを発生させない。そのため、実行はできてしまうという欠点がある。継承を禁止したいなら、この方法と他の方法を併用するのがよいと思う。
まとめ
Pythonで継承を禁止したい場合、方法2か方法3で__init_subclass__
メソッドを書き換える方法がもっともシンプルで良いと思う。Python3.8以降のバージョンでは、final
デコレータと__init_subclass__
メソッドを併用して、静的解析でもエラーメッセージを出せるとより良さそうである。
Discussion