いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Pythonメモ-57 (サブクラスで __init__ を定義する場合の注意点) (class, super, __init__, Never code a method that just delegates to the superclass)

概要

たまに間違えてバグ作ってしまったりしてるので、忘れないようメモメモ。

python で サブクラスの __init__ 定義する場合に

super().__init__()

としていないと、親クラスの__init__() が呼ばれないよって話しです。

他の言語の場合 (例えば C#)

C#の場合

using System;

namespace ConsoleApplication1
{
    internal class BaseClass
    {
        internal int BaseValue { get; }

        public BaseClass()
        {
            BaseValue = 10;
        }
    }

    internal class DerivedClass : BaseClass
    {
        internal int DerivedValue { get; }

        public DerivedClass()
        {
            DerivedValue = 20;
        }
    }

    internal class Program
    {
        public static void Main(string[] args)
        {
            var x = new DerivedClass();
            Console.WriteLine($"Base={x.BaseValue}, Derived={x.DerivedValue}");
        }
    }
}

と記述して、実行すると

Base=10, Derived=20

って表示されます。つまり、サブクラス側で明示的に親クラスのコンストラクタを

呼んでなくても、この場合は暗黙で デフォルトコンストラクタ が呼ばれます。

python の場合

python の場合、__init__メソッドは厳密にはコンストラクタではないのですが

まあ、大体同じものだということで。以下のようなクラスを定義してみて

class B:
    """
    ベースクラス
    """

    def __init__(self) -> None:
        self.x = 10


class C(B):
    """
    サブクラス1

    自分の __init__ を定義していないバージョン
    """
    pass

使ってみると

        obj1 = C()
        pr('C.x', obj1.x)

以下のように表示されます。

C.x=10

予想通りの動作。次に

class D(B):
    """
    サブクラス2

    自分の __init__ を定義しているが
    super().__init__() を呼んでいないバージョン
    """

    # noinspection PyMissingConstructor
    def __init__(self):
        self.y = 20

を定義して

            obj2 = D()
            pr('D.x', obj2.x)
            pr('D.y', obj2.y)

ってすると

AttributeError("'D' object has no attribute 'x'",)

となります。サブクラス側が __init__ を定義しているので

親クラス側の __init__ がオーバーライドされます。 python は素直な言語なので

普通に上書きされます。なので、この場合 親クラスの__init__が呼ばれない となります。

で、以下のようにするのが正しいやり方。

class E(B):
    """
    サブクラス3

    自分の __init__ を定義していて
    super().__init__() も呼んでいるバージョン
    """

    def __init__(self):
        super().__init__()
        self.y = 20

呼び出すと

        obj3 = E()
        pr('E.x', obj3.x)
        pr('E.y', obj3.y)

ちゃんと表示されます。

E.x=10
E.y=20

まとめ

てことで、サブクラスで __init__定義するときは、必ず

super().__init__()

を呼び出しするべしってなります。

PyCharmのような統合環境使っている場合は自動で挿入してくれていたり

呼び出しを書いていなかった場合は警告が出たりしますので、呼び出し忘れることは

ほぼ無いのですが、エディタで書いてたりすると抜けてたりします。(しましたw

実行すると意味の分からないエラーパターンになるので焦ります。。

ちなみに、このパターンは __init__だけじゃなくて、どのメソッドでも当てはまります。

サンプル

"""
Python のクラスについてのサンプルです。

サブクラス側で __init__ を定義した場合の注意点について。
"""
from common.commoncls import SampleBase
from common.commonfunc import pr, hr


class B:
    """
    ベースクラス
    """

    def __init__(self) -> None:
        self.x = 10


class C(B):
    """
    サブクラス1

    自分の __init__ を定義していないバージョン
    """
    pass


class D(B):
    """
    サブクラス2

    自分の __init__ を定義しているが
    super().__init__() を呼んでいないバージョン
    """

    # noinspection PyMissingConstructor
    def __init__(self):
        self.y = 20


class E(B):
    """
    サブクラス3

    自分の __init__ を定義していて
    super().__init__() も呼んでいるバージョン
    """

    def __init__(self):
        super().__init__()
        self.y = 20


class Sample(SampleBase):
    """
    サンプルとなるクラスです。
    """

    def exec(self):
        """
        処理を実行します。
        """
        # ------------------------------------------------------------
        # (1) サブクラス側が __init__ を定義していない場合
        #     既定で、 super().__init__() が呼ばれた状態となる
        # ------------------------------------------------------------
        obj1 = C()
        pr('C.x', obj1.x)

        hr()

        # ------------------------------------------------------------
        # (2) サブクラス側が __init__ を定義しているが super().__init__() を
        #     呼んでいない場合、親クラスの __init__ は呼ばれない。
        #
        # 他の言語に慣れている場合、デフォルトのコンストラクタが呼ばれるのが暗黙的なので
        # よく間違えてしまう。注意が必要。
        #
        # PyCharmで作業している場合、IDE側が警告を出してくれるので気付ける。
        # ------------------------------------------------------------
        try:
            obj2 = D()
            pr('D.x', obj2.x)
            pr('D.y', obj2.y)
        except AttributeError as e:
            pr('D', e)

        hr()

        # ------------------------------------------------------------
        # (3) サブクラス側が __init__ を定義していて super().__init__() を
        #     呼んでいる場合、親クラスの __init__ も呼ばれる。
        # ------------------------------------------------------------
        obj3 = E()
        pr('E.x', obj3.x)
        pr('E.y', obj3.y)


def go() -> None:
    """
    サンプルを実行します。
    """
    obj = Sample()
    obj.exec()


if __name__ == '__main__':
    go()

try-python/cls03.py at master · devlights/try-python · GitHub

補足

この点については、書籍「Python in a nutshell」でも記載されていました。

Python in a Nutshell

Python in a Nutshell

書籍内では、

単純に親クラスの処理をデリゲートするだけのメソッドを書いてはいけません。
(Never code a method that just delegates to the superclass)

つまり、以下のようなものは定義したらダメだよってことですね。

class Derived(Base):
    def __init__(self):
        super().__init__()

過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。