🚫

Pythonで継承を禁止したクラスを作る

2024/03/18に公開

これはなに

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

方法1 |__init_subclass__を使う

継承を禁止したいクラスの__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クラスの継承は禁止されています。

方法2 | __init_subclass__を書き換えたクラスを継承する

方法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よりもこの方法のほうが可読性と再利用性が高いと思う。

方法3 | final_classパッケージを使う

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__を使う方法が使えない場合にのみ有効な方法だと思う。

方法5 | finalデコレータを使う

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