Python クラスの特殊メソッドを探求する

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

はじめに

この実験(Lab)では、Python の特殊メソッド、しばしば二重アンダースコア(double-underscore)の名前から「ダンダー(dunder)」メソッドと呼ばれるものについて探求します。これらのメソッドが、クラスやオブジェクトの動作をどのようにカスタマイズできるかを実践的に理解します。

インスタンスの生成を制御するための __new__ メソッドと、オブジェクトの破棄のための __del__ メソッドについて学びます。また、メモリ使用量を最適化し属性を制限するための __slots__ の使用方法や、__call__ メソッドを使ってクラスインスタンスを関数のように呼び出し可能にする方法についても確認します。実践的な例を通して、より効率的で表現力豊かな Python コードの書き方を習得します。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 100%です。学習者から 100% の好評価を得ています。

__new__ メソッドの理解と使用

このステップでは、__new__ メソッドを探求します。__init__ はオブジェクトが作成された後にその属性を初期化するためによく使用されますが、__new__ はそもそもインスタンスを作成するメソッドです。これは __init__ よりも先に呼び出されます。

主な違いは以下の通りです。

  • __new__ は、クラス(cls)を最初の引数として受け取る静的メソッド(static method)です。クラスの新しいインスタンスを作成し、それを返す責任があります。
  • __init__ は、インスタンス(self)を最初の引数として受け取るインスタンスメソッドです。新しく作成されたオブジェクトを初期化し、何も返しません。

通常、object クラスのデフォルト実装で十分なため、__new__ をオーバーライドする必要はありません。しかし、シングルトンパターン(Singleton pattern)の実装や、イミュータブル(immutable)な型のインスタンスを作成するような高度なケースでは有用です。

__new__ がどのように機能するかを見てみましょう。インスタンス作成時にメッセージを出力する Dog クラスを作成します。

まず、IDE の左側にあるファイルエクスプローラーから dog_cat.py ファイルを開いてください。

dog_cat.py ファイルに以下のコードを追加します。このコードは、基底クラスである Animal と、__new__ メソッドをオーバーライドするサブクラス Dog を定義しています。

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name
        print(f'Initializing {self._name} in Animal.')

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    ## 最初の引数は cls であり、クラス自体を参照します。
    ## コンストラクタに渡されるすべての引数も受け取る必要があります。
    def __new__(cls, name, age):
        print('A new Dog instance is being created.')
        ## 親クラスの __new__ メソッドを呼び出してインスタンスを作成します。
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print(f'Initializing {name} in Dog.')
        ## 親クラスの __init__ を呼び出して name を設定します。
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

## Dog インスタンスを作成
print("Creating a Dog object...")
d = Dog('Buddy', 5)
print("Dog object created.")
print(f"Dog's name: {d._name}, Age: {d.age}")

ファイルを保存します(Ctrl+S または Cmd+S)。

次に、IDE でターミナルを開きます(メニューの Terminal > New Terminal を使用できます)。スクリプトを実行して、メソッド呼び出しの順序を観察してください。

python ~/project/dog_cat.py

以下の出力が表示されます。__new__ がインスタンスを作成するために最初に呼び出され、その後 __init__ メソッド群がそれを初期化するために呼び出されていることに注目してください。

Creating a Dog object...
A new Dog instance is being created.
Initializing Buddy in Dog.
Initializing Buddy in Animal.
Dog object created.
Dog's name: Buddy, Age: 5

これは、__new__ がオブジェクトの生成を制御し、__init__ がその後にオブジェクトを設定することを示しています。

__del__ メソッドの実装とテスト

このステップでは、__del__ メソッドについて学びます。このメソッドはファイナライザ(finalizer)またはデストラクタ(destructor)と呼ばれます。オブジェクトの参照カウントがゼロになったとき、つまり Python のガベージコレクタによって破棄されようとしているときに呼び出されます。ネットワーク接続やファイルハンドルを閉じるなど、クリーンアップタスクによく使用されます。

del ステートメントを使用してオブジェクトへの参照を削除できます。最後の参照がなくなると、__del__ が自動的に呼び出されます。

オブジェクトがいつ破棄されるかを確認するために、Dog クラスに __del__ メソッドを追加してみましょう。

再度 dog_cat.py ファイルを開きます。ファイル全体の内容を以下のコードに置き換えてください。このバージョンでは、モジュールがインポートされたときにインスタンスが作成されるのを避けるため、Dog インスタンスを作成するコードは削除され、Dog クラスに __del__ メソッドが追加されています。

## File Name: dog_cat.py

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

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    def __new__(cls, name, age):
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

    ## このメソッドを Dog クラスに追加します
    def __del__(self):
        print(f'The Dog object {self._name} is being deleted.')

dog_cat.py ファイルを保存します。

次に、この動作をテストするための別のスクリプトを作成します。ファイルエクスプローラーから test_del.py ファイルを開きます。

test_del.py に以下のコードを追加します。このスクリプトは 2 つの Dog インスタンスを作成し、そのうちの 1 つを明示的に削除します。

## File Name: test_del.py

from dog_cat import Dog
import time

print("Creating two Dog objects: d1 and d2.")
d1 = Dog('Tom', 3)
d2 = Dog('John', 5)

print("\nDeleting reference to d1...")
del d1
print("Reference to d1 deleted.")

## ガベージコレクタは即座に実行されない場合があります。
## 実行時間を確保するために短い遅延を追加します。
time.sleep(1)

print("\nScript is about to end. d2 will be deleted automatically.")

ファイルを保存します。次に、ターミナルで test_del.py スクリプトを実行します。

python ~/project/test_del.py

出力を観察してください。del d1 が呼び出された後、Tom__del__ メッセージが表示されます。d2 オブジェクトはスクリプト終了時にガベージコレクトされるため、John のメッセージは最後に出力されます。

Creating two Dog objects: d1 and d2.

Deleting reference to d1...
The Dog object Tom is being deleted.
Reference to d1 deleted.

Script is about to end. d2 will be deleted automatically.
The Dog object John is being deleted.

注意: ガベージコレクションの正確なタイミングは異なる場合があります。__del__ は、del が使用された直後ではなく、オブジェクトが収集されたときに呼び出されます。

__slots__ による属性の制御

このステップでは、__slots__ について学びます。デフォルトでは、Python はインスタンス属性を __dict__ と呼ばれる特別な辞書に格納します。これにより、いつでもオブジェクトに新しい属性を追加できます。しかし、この柔軟性は余分なメモリを消費します。

クラスで __slots__ 属性を定義することにより、インスタンスが持ちうる属性の固定リストを指定できます。これには主に 2 つの効果があります。

  1. メモリ節約: Python はインスタンスごとに __dict__ を使用する代わりに、よりコンパクトな内部構造を使用するため、特に多数のオブジェクトを作成する場合に、メモリ使用量を大幅に削減できます。
  2. 属性の制限: __slots__ にリストされていない属性をインスタンスに追加できなくなります。これは、タイプミスを防ぎ、厳密なオブジェクト構造を強制するのに役立ちます。

__slots__ がどのように機能するかを見るための例を作成しましょう。ファイルエクスプローラーから slots_example.py ファイルを開きます。

slots_example.py に以下のコードを追加します。

## File Name: slots_example.py

class Player:
    ## __slots__ を使用して許可される属性を定義します
    __slots__ = ('name', 'level')

    def __init__(self, name, level):
        self.name = name
        self.level = level

## Player のインスタンスを作成
p1 = Player('Hero', 10)

## 許可された属性にアクセス
print(f"Player name: {p1.name}")
print(f"Player level: {p1.level}")

## 次に、__slots__ に含まれていない新しい属性を追加してみます
print("\nTrying to add a 'score' attribute...")
try:
    p1.score = 100
    print(f"Player score: {p1.score}")
except AttributeError as e:
    print(f"Caught an error: {e}")

## また、インスタンスが __dict__ 属性を持っているか確認します
print("\nChecking for __dict__...")
try:
    print(p1.__dict__)
except AttributeError as e:
    print(f"Caught an error: {e}")

ファイルを保存します。次に、ターミナルで slots_example.py スクリプトを実行します。

python ~/project/slots_example.py

出力は、namelevel への代入は可能ですが、score への値の代入を試みると AttributeError が発生することを示しています。また、インスタンスが __dict__ を持っていないことも確認できます。

Player name: Hero
Player level: 10

Trying to add a 'score' attribute...
Caught an error: 'Player' object has no attribute 'score'

Checking for __dict__...
Caught an error: 'Player' object has no attribute '__dict__'

これは、__slots__ がどのように固定された属性セットを強制し、インスタンス辞書を排除することでメモリを最適化できるかを示しています。

__call__ によるインスタンスの呼び出し可能化

このステップでは、__call__ メソッドを探求します。Python では、関数のように括弧 () を使って「呼び出す」ことができるオブジェクトは、呼び出し可能(callable)オブジェクトとして知られています。関数やメソッドは本質的に呼び出し可能です。

デフォルトでは、クラスのインスタンスは呼び出し可能ではありません。しかし、クラス内に特殊メソッド __call__ を定義すると、そのインスタンスは呼び出し可能になります。インスタンスを関数のように呼び出すと、その __call__ メソッド内のコードが実行されます。これは、関数のように振る舞いつつ、独自の内部状態を保持できるオブジェクトを作成する際に役立ちます。

インスタンスを呼び出し可能にするクラスを作成してみましょう。ファイルエクスプローラーから callable_instance.py ファイルを開きます。

callable_instance.py に以下のコードを追加します。

## File Name: callable_instance.py

class Greeter:
    def __init__(self, greeting):
        ## この状態はインスタンスと共に保存されます
        self.greeting = greeting
        print(f'Greeter initialized with "{self.greeting}"')

    ## インスタンスを呼び出し可能にするために __call__ メソッドを定義します
    def __call__(self, name):
        ## このコードはインスタンスが呼び出されたときに実行されます
        print(f"{self.greeting}, {name}!")

## Greeter のインスタンスを作成
hello_greeter = Greeter("Hello")

## 組み込みの callable() 関数を使用してインスタンスが呼び出し可能か確認します
print(f"Is hello_greeter callable? {callable(hello_greeter)}")

## 次に、インスタンスを関数のように呼び出します
print("\nCalling the instance:")
hello_greeter("Alice")
hello_greeter("Bob")

## 異なる状態で別のインスタンスを作成します
goodbye_greeter = Greeter("Goodbye")
print("\nCalling the second instance:")
goodbye_greeter("Charlie")

ファイルを保存します。次に、ターミナルで callable_instance.py スクリプトを実行します。

python ~/project/callable_instance.py

出力は、hello_greeter インスタンスが実際に呼び出し可能であることを示しています。呼び出すたびに、初期化時に設定された状態 (self.greeting) を使用して __call__ メソッドが実行されます。

Greeter initialized with "Hello"
Is hello_greeter callable? True

Calling the instance:
Hello, Alice!
Hello, Bob!
Greeter initialized with "Goodbye"

Calling the second instance:
Goodbye, Charlie!

これは、__call__ がどのようにして状態を持つ関数のようなオブジェクトを作成できるかを示しており、これはオブジェクト指向プログラミングにおける強力な機能です。

まとめ

この実験(Lab)では、Python のいくつかの強力な特殊メソッドを探求しました。インスタンス生成プロセスを制御するために __new__ を使用する方法を学び、__init__ が呼び出される前のフックを得ました。オブジェクトがガベージコレクションされる際に実行されるクリーンアップロジックを定義するために __del__ メソッドを実装しました。また、インスタンスの __dict__ の作成を防ぐことでメモリを最適化し、厳密な属性モデルを強制するために __slots__ を使用しました。最後に、__call__ メソッドを実装することで、オブジェクトに関数のように振る舞わせました。これらのダンダ―メソッド(dunder methods)を習得することで、より柔軟で効率的、かつ Python らしいクラスを書くことができます。