Python におけるクラス機能の理解

PythonBeginner
オンラインで実践に進む

はじめに

この実験(Lab)では、Python における主要なオブジェクト指向プログラミング(OOP)の概念について実践的な理解を深めます。まず、カプセル化(encapsulation)から始め、データとメソッドをクラス内にバンドルする方法、およびプライベート属性を使用してデータへのアクセスを制御する方法を学びます。

次に、継承(inheritance)を実装してクラス間の関係を構築し、コードの再利用性を促進します。また、異なるクラスのオブジェクトを統一的に扱うことを可能にするポリモーフィズム(polymorphism)についても探ります。最後に、super()メソッドを使用して親クラスのメソッドを効果的に呼び出す方法を学び、複数の親クラスから継承できるマルチプルインヘリタンス(multiple inheritance)を実践します。

これは、学習と実践を支援するための段階的な指示を提供するガイド付き実験(Guided Lab)です。各ステップを注意深く実行し、実践的な経験を積んでください。過去のデータによると、これは初心者レベルの実験であり、完了率は100%です。学習者からは100%の肯定的なレビュー評価を得ています。

基本クラスを用いたカプセル化の探求

このステップでは、OOP の核となる原則である**カプセル化(encapsulation)**を探求します。カプセル化とは、データ(属性)とそのデータを操作するメソッドを単一のユニットであるクラス内にバンドルする行為を指します。また、オブジェクトの内部状態への直接アクセスを制限することで、意図しないデータ変更を防ぐのに役立ちます。

Python では、「プライベート」な属性であることを示すために命名規則を使用します。属性名の前にアンダースコアを一つ付けること(例:_name)は、その属性が内部使用を意図していることを示唆します。これは厳密に強制されるわけではありませんが、開発者が尊重する強力な慣習です。

まず、構造をどのように構築できるかを確認するために、DogクラスとCatクラスという 2 つの独立したクラスを作成します。

最初に、WebIDE の左側にあるファイルエクスプローラーでファイル animal_classes.py を見つけます。これを開き、以下の Python コードを追加してください。このコードは、プライベートな _name 属性と、それと対話するためのメソッドを持つ Dog クラスと Cat クラスを定義しています。

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## 単一アンダースコアのプレフィックスは「プライベート」属性を示します。
        self._name = name

    ## プライベート属性の値を取得するためのパブリックメソッド。
    def get_name(self):
        return self._name

    ## プライベート属性の値を設定するためのパブリックメソッド。
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

class Cat:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## このブロックはスクリプトが直接実行された場合にのみ実行されます。
if __name__ == "__main__":
    ## Dog クラスのインスタンスを作成
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## セッターメソッドを使用して犬の名前を変更
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## Cat クラスのインスタンスを作成
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

コードを追加した後、ファイルを保存します。

次に、カプセル化がどのように機能するかを確認するために、スクリプトを実行してみましょう。WebIDE でターミナルを開き、次のコマンドを実行します。

python animal_classes.py

以下の出力が表示されます。これは、パブリックな get_name メソッドと set_name メソッドを通じて、プライベートな _name 属性と対話していることを示しています。

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

継承とポリモーフィズムの実装

前のステップで、DogクラスとCatクラスが多くの同一コード(__init__get_nameset_name)を共有していることに気づいたかもしれません。これは**継承(inheritance)**を使用する絶好の機会です。継承により、新しいクラス(子クラスまたはサブクラス)は既存のクラス(親クラスまたはスーパークラス)から属性とメソッドを継承でき、コードの再利用が促進されます。

また、「多くの形」を意味する**ポリモーフィズム(polymorphism)**も導入します。OOP においては、異なるクラスが同じメソッド呼び出しに対してそれぞれ独自の応答をする能力を指します。

コードをリファクタリングしましょう。共通のコードを保持するために親クラス Animal を作成し、DogCat がそこから継承するようにします。それぞれで異なる say メソッドは、ポリモーフィズムを実証します。

animal_classes.py ファイルを開き、その内容全体を以下のコードに置き換えてください。

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog は Animal を継承します
class Dog(Animal):
    ## これは Animal クラスの say() メソッドをオーバーライドします
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat は Animal を継承します
class Cat(Animal):
    ## これも say() メソッドをオーバーライドします
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- Calling say() on each object ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- Demonstrating Polymorphism ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

ファイルを保存します。DogクラスとCatクラスがはるかにシンプルになっていることに注目してください。これらはAnimalから__init__get_nameset_nameメソッドを継承しています。それぞれが独自のバージョンのsayメソッドを提供しており、これはメソッドのオーバーライディング(method overriding)の例です。

それでは、ターミナルから更新されたスクリプトを実行します。

python animal_classes.py

出力は次のようになります。

--- Calling say() on each object ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- Demonstrating Polymorphism ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

関数 make_animal_speak は、say メソッドを持つ任意のオブジェクトを受け取ります。異なる型のオブジェクト(AnimalDogCat)を渡しても、それぞれのオブジェクトが独自のやり方で say アクションを実行する方法を知っているため、正しく機能します。これがポリモーフィズムの力です。

super() メソッドを使用して機能を拡張する

子クラスが親クラスのメソッドをオーバーライドする場合、単に置き換えるだけでなく、親のメソッドを拡張したいことがあります。super() 関数は、子クラス内から親クラスのメソッドを呼び出す方法を提供します。

これは __init__ メソッドで非常によく使われます。子クラスは、親クラスが実行する初期化に加えて、独自の初期化ステップを実行する必要があることがよくあります。

Dog クラスと Cat クラスに固有の属性を追加しましょう。Dog には age を、Cat には color を持たせます。親クラス Animal__init__ メソッドが呼び出されて _name 属性が設定されるように、super() を使用します。

animal_classes.py ファイルの内容を以下のコードに置き換えて変更してください。

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## 親の __init__ メソッドを呼び出して 'name' 属性を処理します
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## super() を使用して親の say() メソッドを呼び出すこともできます
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## 親の __init__ メソッドを呼び出します
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

ファイルを保存します。このバージョンでは、Dog.__init__Cat.__init__ はまず super().__init__(name) を呼び出します。これにより Animal.__init__ 内のコードが実行され、_name 属性が設定されます。その後、独自の具体的な初期化(self.age = age および self.color = color)に進みます。

ターミナルからスクリプトを実行します。

python animal_classes.py

出力は、__init__ の呼び出しチェーンと拡張された say メソッドを示しています。

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

多重継承の演習

Python では、クラスが複数の親クラスから継承することを許可されています。これは**多重継承(multiple inheritance)**と呼ばれます。これは異なるソースからの機能を組み合わせる強力なツールになり得ますが、特に同じ名前のメソッドがある場合に Python がどの親のメソッドを使用するかを決定する方法において、複雑さも生じさせます。

この検索順序は**メソッド解決順序(Method Resolution Order, MRO)**と呼ばれます。Python は、一貫性があり予測可能な MRO を決定するために、C3 線形化(C3 linearization)と呼ばれるアルゴリズムを使用します。

新しい例でこれを探求しましょう。ファイルエクスプローラーから multiple_inheritance.py ファイルを開き、以下のコードを追加してください。

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child は A の後に B を継承します
class Child_AB(ParentA, ParentB):
    pass

## Child は B の後に A を継承します
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Investigating Child_AB (ParentA, ParentB) ---")
    child1.speak()
    child1.common_method()
    ## .mro() メソッドはメソッド解決順序を示します
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Investigating Child_BA (ParentB, ParentA) ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

ファイルを保存します。ここで、Child_ABParentA の後に ParentB を継承します。Child_BA は逆順に継承します。メソッドが呼び出されると、Python は MRO によって指定された順序でそれを検索します。

ターミナルからスクリプトを実行します。

python multiple_inheritance.py

以下の出力が表示されます。

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

出力から、以下のことが観察できます。

  • child1.speak()Child_AB の MRO で ParentA が最初に来るため、ParentA のメソッドを呼び出します。
  • child2.speak()Child_BA の MRO で ParentB が最初に来るため、ParentB のメソッドを呼び出します。
  • child2.common_method() は、Python が親クラスをチェックする前に Child_BA で直接定義されているバージョンを見つけるため、それを呼び出します。

多重継承のシナリオで動作を予測するためには、MRO を理解することが極めて重要です。

まとめ

この実験(Lab)では、Python におけるオブジェクト指向プログラミングの 4 つの基本的な概念について実践的な経験を積みました。

まず、慣習的にプライベート属性を使用してクラスデータを保護し、アクセス用にパブリックメソッドを提供することで、**カプセル化(encapsulation)から始めました。次に、コードをリファクタリングして継承(inheritance)**を使用し、親の Animal クラスを作成することで、Dog および Cat サブクラスでのコードの重複を削減しました。

継承を実装する過程で、Dog オブジェクトと Cat オブジェクトが同じ say() メソッドの呼び出しに対して異なる応答をすることから、ポリモーフィズム(polymorphism)が機能しているのを確認しました。また、特に __init__ メソッド内で、親クラスの機能を呼び出し拡張するためにsuper() メソッドの使用方法を学びました。最後に、**多重継承(multiple inheritance)**と、どの親メソッドが呼び出されるかを決定する上でのメソッド解決順序(MRO)の重要性について探求しました。