ダックタイピング

ポリモーフィズムのアプローチの違い

Javaなどの静的型付け言語と、Pythonなどの動的型付け言語では、ポリモーフィズムを実現するためのアプローチが大きく異なります。 Javaのような静的型付け言語では、クラスの型が安全であることをコンパイル時に検証します。 そのため、異なるオブジェクトを同一視してポリモーフィズムを実現するには、共通のインターフェースや抽象クラスを明示的に継承する必要があります。 これを公称型と呼びます。 これに対し、Pythonのような動的型付け言語では、オブジェクトの具体的なクラスや継承関係は重要視されません。 オブジェクトが「どのようなメソッドや属性を持っているか」という実行時の振る舞いだけに基づいて判断されます。 この特徴は「もしそれが鴨のように歩き、鴨のように鳴くならば、それは鴨である」という言葉に由来して、ダックタイピングと呼ばれます。 ダックタイピングを用いると、共通の親クラスを継承していなくても、必要なメソッドさえ持っていればどのようなオブジェクトでも透過的に扱えます。

ダックタイピングの実例

Pythonの標準ライブラリでは、ファイルのような振る舞いをするオブジェクトであるファイルライクオブジェクトでダックタイピングが広く採用されています。 静的型付け言語の考え方に従い、共通の抽象クラスを明示的に継承してポリモーフィズムを実現する素朴なプログラムは以下のようになります。

nominal_writer.py

from abc import ABC, abstractmethod

class Writable(ABC):
    @abstractmethod
    def write(self, text):
        pass

class ConsoleWriter(Writable):
    def write(self, text):
        print(text)

def log_message(writer, message):
    writer.write(message)

writer = ConsoleWriter()
log_message(writer, "Hello, World!")

プログラムの実行結果は以下のようになります。

実行結果

Hello, World!

このプログラムは、Javaのような「共通の親クラスを継承して型を合わせる」設計を示しています。 しかし、Pythonは動的型付け言語であり、実行時に引数の型チェックを行いません。 そのため、実際には Writable を継承していないクラスであっても、write メソッドさえ持っていれば log_message 関数に渡して実行できてしまいます。 このように、Pythonでは明示的な継承に頼らず、必要なメソッドの有無だけでオブジェクトを扱うダックタイピングが自然と機能します。 継承関係を定義せず、ダックタイピングを前提として記述したプログラムは以下のようになります。

duck_writer.py

import io

class ConsoleWriter:
    def write(self, text):
        print(text)

class CustomLogger:
    def __init__(self):
        self.history = []

    def write(self, text):
        self.history.append(text)

def log_message(writer, message):
    writer.write(message)

console_writer = ConsoleWriter()
log_message(console_writer, "画面に出力します。")

custom_logger = CustomLogger()
log_message(custom_logger, "ログに保存します。")

string_buffer = io.StringIO()
log_message(string_buffer, "メモリ上のバッファに出力します。")

print(custom_logger.history)
print(string_buffer.getvalue())

プログラムの実行結果は以下のようになります。

実行結果

画面に出力します。
ログに保存します。
['ログに保存します。']
メモリ上のバッファに出力します。

このプログラムでは、ConsoleWriterCustomLogger、および標準ライブラリの io.StringIO は互いに継承関係を持っていません。 しかし、すべてのオブジェクトが write メソッドを持っているため、log_message 関数はそれらをファイルライクオブジェクトとして透過的に受け取って処理できます。 これがダックタイピングの持つ強力な柔軟性です。

演習問題

演習1

emit(self, record) メソッドを持つ ConsoleLogger クラスを作成しなさい。 このメソッドは、受け取った文字列の先頭に [CONSOLE] という接頭辞を付与して標準出力に表示するものとします。

演習2

ログをメモリ上に保存する MemoryLogger クラスを作成しなさい。 このクラスはログ記録を保持する records リストを持ち、emit(self, record) メソッドにより受け取った文字列の先頭に [MEMORY] という接頭辞を付与してリストに追加するものとします。

演習3

登録された複数のロガーにメッセージを一斉配信する LoggerManager クラスを作成しなさい。 このクラスは add_logger(self, logger) メソッドで任意のロガーを追加し、broadcast(self, message) メソッドで登録済みのすべてのロガーの emit メソッドを順に呼び出すものとします。 特定の親クラスの継承を要求せず、ダックタイピングを利用してロガーを受け取るように実装しなさい。

解答例

演習問題のプログラムの実装例は以下の通りである。

実装の方針として、まずログの出力先がそれぞれ異なる ConsoleLogger クラスおよび MemoryLogger クラスを定義し、共通のインターフェースとなる emit メソッドをそれぞれ実装する。 次に複数のロガーを管理する LoggerManager クラスを定義し、登録されたすべてのロガーに対して透過的に emit メソッドを呼び出す処理を記述する。 最後にそれぞれのロガーを管理クラスに登録してメッセージを配信し、標準出力への表示およびメモリへの保存が正しく実行されることを確認する。

logger_duck.py

class ConsoleLogger:
    def emit(self, record):
        print(f"[CONSOLE] {record}")

class MemoryLogger:
    def __init__(self):
        self.records = []

    def emit(self, record):
        self.records.append(f"[MEMORY] {record}")

class LoggerManager:
    def __init__(self):
        self.loggers = []

    def add_logger(self, logger):
        self.loggers.append(logger)

    def broadcast(self, message):
        for logger in self.loggers:
            logger.emit(message)

# 動作検証
manager = LoggerManager()
console = ConsoleLogger()
memory = MemoryLogger()

manager.add_logger(console)
manager.add_logger(memory)

manager.broadcast("システムを起動しました。")
manager.broadcast("警告が発生しました。")

print("メモリに保存されたログ:")
print(memory.records)

プログラムの実行結果は以下のようになります。

実行結果

[CONSOLE] システムを起動しました。
[CONSOLE] 警告が発生しました。
メモリに保存されたログ:
['[MEMORY] システムを起動しました。', '[MEMORY] 警告が発生しました。']

results matching ""

    No results matching ""