Pythonで親クラスを継承した子クラスの数とインスタンス数を数える

# 概要

タイトルがわかりづらくて恐縮なのですが、以下の2つの数え方についてこの記事では紹介します。

- 親クラスを継承した子クラスの数を数えたい
- 親クラスに属する（継承した子クラスの）インスタンスの数を数えたい

もう少し別のバリエーションだと、

- 親クラスを継承したすべての子クラスのクラスメソッドを呼びたい
- 親クラスに属するすべてのインスタンスに対して指示を出したい

ということも実現可能です。

サンプルコードはGitHubに公開しています。

https://github.com/dorapon2000/dorapon2000-blog/tree/main/count_subclasses_instances


# 検証環境

- Python 3.10.0


# 親クラスを継承した子クラスの数を数えたい

親クラスがを継承する子クラスが2つあるとして、その2という数字を求めます。

```python
class Parent():
    _subclasses_count = 0

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        __class__._subclasses_count += 1

    @classmethod
    @property
    def subclasses_count(cls):
        return __class__._subclasses_count

class ChildA(Parent):
    pass

class ChildB(Parent):
    pass

def main():
    print(Parent.subclasses_count)  # 2

if __name__ == '__main__':
    main()

```


ポイントは[`__init_subclass__`](https://docs.python.org/ja/3/reference/datamodel.html#object.__init_subclass__)です。Python 3.6から使えます。これは、クラスが継承されるたびに毎回呼ばれる特殊メソッドで、この中でクラス変数の`_subclasses_count`をインクリメントしていきます。`__init_subclass__`のおかげで、継承先の子クラスではなにもしなくても子クラスの数を求めることが可能になりました。

`__init__subclass__`は子クラスのさらに子クラスにも効きます。

```python
class ChildA(Parent):
    pass

class ChildB(Parent):
    pass

class ChildAChildC(ChildA):
    pass

class ChildAChildD(ChildA):
    pass
```

このような場合は4が返ってきます。


本題と関係ないですが、`@classmethod`と`@property`の重ねがけはPython 3.9以降でないと使えない[^1]ため、Python 3.8以前ではメソッドから参照するか、直接参照する必要があります。

```python
    # Python 3.8以前の場合
    @classmethod
    def get_subclasses_count_v3_8(cls):
        return __class__._subclasses_count
```

# 親クラスに属するインスタンスの数を数えたい

親クラスを継承した子クラスのインスタンスが合計3つあるとして、その3という数字を求めます。

```python
class Parent():
    _instances_count = 0

    def __new__(cls):
        self = super().__new__(cls)
        __class__._instances_count += 1
        return self

    @classmethod
    @property
    def instances_count(cls):
        return __class__._instances_count

class ChildA(Parent):
    pass

class ChildB(Parent):
    pass

def main():
    child_a_1 = ChildA()
    child_a_2 = ChildA()
    child_b = ChildB()

    print(Parent.instances_count)  # 3

if __name__ == '__main__':
    main()
```

こちらのポイントは[`__new__`](https://docs.python.org/ja/3/reference/datamodel.html#object.__new__)です。インスタンスの作成方法を定義できる特殊メソッドで、`__init__`と似ていますが、それより前に呼ばれます。普通はインスタンスを作成してreturnするもののようですが、この中で`_instances_count`をインクリメントする処理をはさめます。

前節と同様に、子クラスの子クラス（つまり孫クラス）のインスタンスも数えられます。


# 親クラスを継承したすべての子クラスのクラスメソッドを呼びたい

各子クラスに定義しているクラスメソッドnotifyを呼びます。
[親クラスを継承した子クラスの数を数えたい](#親クラスを継承した子クラスの数を数えたい)と仕組みは同じなのでソースコードだけ貼ります。

```python
from abc import ABC, abstractclassmethod

class Parent(ABC):
    all_subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        __class__.all_subclasses.append(cls)

    @classmethod
    def notify_all_subclasses(cls):
        for subclass in __class__.all_subclasses:
            subclass.notify()

    @abstractclassmethod
    def notify(cls):
        pass

class ChildA(Parent):
    @classmethod
    def notify(cls):
        print(f'{__class__.__name__} subclass')

class ChildB(Parent):
    @classmethod
    def notify(cls):
        print(f'{__class__.__name__} subclass')

def main():
    Parent.notify_all_subclasses()

if __name__ == '__main__':
    main()
```


実行結果

```sh
❯ python subclass_notifier.py
ChildA subclass
ChildB subclass
```

# 親クラスに属するすべてのインスタンスに対して指示を出したい

各子クラスに定義しているインスタンスメソッドnotifyを呼びます。
こちらも[親クラスに属するインスタンスの数を数えたい](#親クラスに属するインスタンスの数を数えたい)と仕組みは同じなのでソースコードだけ貼ります。

```python
from abc import ABC, abstractmethod

class Parent(ABC):
    all_instances = []

    def __new__(cls):
        self = super().__new__(cls)
        __class__.all_instances.append(self)
        return self

    @classmethod
    def notify_all_instances(cls):
        for instance in __class__.all_instances:
            instance.notify()

    @abstractmethod
    def notify(self):
        pass

class ChildA(Parent):
    def notify(self):
        print(f'{__class__.__name__} instance')

class ChildB(Parent):
    def notify(self):
        print(f'{__class__.__name__} instance')

def main():
    child_a_1 = ChildA()
    child_a_2 = ChildA()
    child_b = ChildB()

    Parent.notify_all_instances()

if __name__ == '__main__':
    main()
```

実行結果。

```sh
❯ python instance_notifier.py
ChildA instance
ChildA instance
ChildB instance
```


# 参考

- [Effective Python 第2版 ―Pythonプログラムを改良する90項目 | Brett Slatkin, 石本 敦夫, 黒川 利明 |本 | 通販 | Amazon](https://www.amazon.co.jp/dp/4873119170/)
- [How to auto register a class when it's defined](https://stackoverflow.com/questions/5189232/how-to-auto-register-a-class-when-its-defined)
- [【Python】特殊メソッド一覧 - Qiita](https://qiita.com/NaHCO3/items/d189ef440a48e918dde0)


[^1]: https://docs.python.org/ja/3.9/library/functions.html#classmethod