Documentation Python

Pythonでは、既存のオブジェクトインスタンスに動的にメソッドを追加することが可能です。この記事では、様々な方法とその使い分けについて解説します。

メソッド追加方法の比較

方法用途self自動バインド推奨度
types.MethodTypeインスタンスメソッド追加
__get__()手動バインド
functools.partial引数固定×
直接代入シンプルな関数追加×
クラスへの追加全インスタンスに追加

types.MethodTypeを使用した方法(推奨)

標準ライブラリのtypes.MethodTypeを使うと、既存のインスタンスにメソッドを正しくバインドできます。

import types

class Dog:
    def __init__(self, name: str):
        self.name = name

    def bark(self) -> str:
        return f"{self.name}がワンワン!"

# 新しいメソッドとなる関数を定義
def wag_tail(self) -> str:
    return f"{self.name}が尻尾を振っている"

def sit(self) -> str:
    return f"{self.name}がお座りした"

# インスタンスを作成
dog1 = Dog("ポチ")
dog2 = Dog("タロウ")

# dog1にのみメソッドを追加
dog1.wag_tail = types.MethodType(wag_tail, dog1)
dog1.sit = types.MethodType(sit, dog1)

# 使用例
print(dog1.bark())      # ポチがワンワン!
print(dog1.wag_tail())  # ポチが尻尾を振っている
print(dog1.sit())       # ポチがお座りした

# dog2にはメソッドがない
print(dog2.bark())      # タロウがワンワン!
# dog2.wag_tail()       # AttributeError: 'Dog' object has no attribute 'wag_tail'

MethodTypeの仕組み

import types

def greet(self):
    return f"Hello, I am {self.name}"

class Person:
    def __init__(self, name):
        self.name = name

p = Person("Alice")

# MethodTypeは関数とインスタンスをバインドする
bound_method = types.MethodType(greet, p)

# バインドされたメソッドの情報を確認
print(bound_method.__self__)  # <Person object>(バインドされたインスタンス)
print(bound_method.__func__)  # <function greet>(元の関数)

# 呼び出し時にselfが自動的に渡される
print(bound_method())  # Hello, I am Alice

get()を使った方法

関数は記述子(descriptor)プロトコルを実装しているため、__get__()を直接使用できます。

class Calculator:
    def __init__(self, value: int):
        self.value = value

def double(self) -> int:
    return self.value * 2

def triple(self) -> int:
    return self.value * 3

calc = Calculator(10)

# __get__()でメソッドをバインド
calc.double = double.__get__(calc, Calculator)
calc.triple = triple.__get__(calc, Calculator)

print(calc.double())  # 20
print(calc.triple())  # 30

functools.partialを使う方法

functools.partialを使うと、引数を事前に固定した関数を作成できます。

from functools import partial

class DataProcessor:
    def __init__(self, data: list):
        self.data = data

def filter_data(self, threshold: int) -> list:
    return [x for x in self.data if x > threshold]

processor = DataProcessor([1, 5, 10, 15, 20])

# partialでインスタンスを固定
processor.filter_data = partial(filter_data, processor)

# 使用時はselfを渡さない
result = processor.filter_data(threshold=8)
print(result)  # [10, 15, 20]

# 注意: partialは厳密にはメソッドではない
print(type(processor.filter_data))  # <class 'functools.partial'>

クラスへのメソッド追加

特定のインスタンスではなく、クラス全体にメソッドを追加する場合は、クラス属性に直接代入します。

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

# 後からクラスにメソッドを追加
def speak(self) -> str:
    return f"{self.name}が鳴いた"

Animal.speak = speak

# すべてのインスタンスでメソッドが使える
cat = Animal("タマ")
dog = Animal("ポチ")

print(cat.speak())  # タマが鳴いた
print(dog.speak())  # ポチが鳴いた

実践的なユースケース

プラグインシステム

import types
from typing import Callable

class Plugin:
    """拡張可能なプラグインベースクラス"""

    def __init__(self, name: str):
        self.name = name
        self._hooks: dict[str, list] = {}

    def add_method(self, name: str, func: Callable) -> None:
        """メソッドを動的に追加"""
        setattr(self, name, types.MethodType(func, self))

    def remove_method(self, name: str) -> None:
        """メソッドを動的に削除"""
        if hasattr(self, name):
            delattr(self, name)

# プラグインの使用例
def custom_action(self, data: str) -> str:
    return f"[{self.name}] Processing: {data}"

def custom_validate(self, value: int) -> bool:
    return value > 0

plugin = Plugin("MyPlugin")

# メソッドを動的に追加
plugin.add_method("process", custom_action)
plugin.add_method("validate", custom_validate)

print(plugin.process("test data"))  # [MyPlugin] Processing: test data
print(plugin.validate(10))          # True

# メソッドを削除
plugin.remove_method("validate")

テスト用のモック

import types

class APIClient:
    """外部APIクライアント"""

    def __init__(self, base_url: str):
        self.base_url = base_url

    def fetch_data(self, endpoint: str) -> dict:
        # 実際のAPI呼び出し
        import requests
        response = requests.get(f"{self.base_url}/{endpoint}")
        return response.json()

# テスト用のモックメソッド
def mock_fetch_data(self, endpoint: str) -> dict:
    """APIをモック化"""
    return {
        "users": {"id": 1, "name": "Test User"},
        "posts": {"id": 1, "title": "Test Post"}
    }.get(endpoint, {})

# テストでの使用
client = APIClient("https://api.example.com")

# 本番メソッドをモックに置き換え
original_fetch = client.fetch_data
client.fetch_data = types.MethodType(mock_fetch_data, client)

# テスト実行
result = client.fetch_data("users")
print(result)  # {'id': 1, 'name': 'Test User'}

# 元に戻す
client.fetch_data = original_fetch

デコレータとの組み合わせ

import types
from functools import wraps

def add_logging(func):
    """ログを追加するデコレータ"""
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(self, *args, **kwargs)
        print(f"Result: {result}")
        return result
    return wrapper

class Service:
    def __init__(self, name: str):
        self.name = name

def process_data(self, value: int) -> int:
    return value * 2

# デコレートされたメソッドを追加
service = Service("DataService")
service.process = types.MethodType(add_logging(process_data), service)

service.process(10)
# 出力:
# Calling process_data with args=(10,), kwargs={}
# Result: 20

注意点とベストプラクティス

使用を避けるべきケース

# NG: 本番コードでの乱用
class User:
    def __init__(self, name):
        self.name = name

# 悪い例: 意図が不明確なモンキーパッチ
import types
user = User("Alice")
user.mysterious_method = types.MethodType(lambda self: "???", user)

推奨されるケース

ユースケース説明
プラグイン/拡張機能明示的な拡張ポイントとして設計
テスト用モックテストコードでの一時的な置き換え
デバッグ/調査開発時の一時的な機能追加
フレームワークORM等での動的なメソッド生成

代替案を検討する

# 継承を使う(推奨)
class Dog:
    def bark(self):
        return "ワンワン"

class SmartDog(Dog):
    def sit(self):
        return "お座り"

# ミックスインを使う(推奨)
class SittingMixin:
    def sit(self):
        return "お座り"

class TrainedDog(SittingMixin, Dog):
    pass

# 構成(Composition)を使う(推奨)
class TrickBag:
    def __init__(self, dog):
        self.dog = dog

    def sit(self):
        return f"{self.dog.name}がお座り"

まとめ

状況推奨方法
インスタンスにメソッド追加types.MethodType
クラス全体にメソッド追加クラス属性への代入
テスト用モックtypes.MethodType + コンテキストマネージャ
本番コード継承やミックスインを優先検討

動的なメソッド追加は強力な機能ですが、コードの可読性と保守性を考慮して適切な場面で使用することが重要です。

参考文献

円