Menu
Coddy logo textTech

クラスデコレータ

CoddyのPythonジャーニー「Object Oriented Programming」セクションの一部 — レッスン 40/64。

クラスデコレータを使用すると、クラスを別の関数でラップすることで、クラスを変更したり拡張したりすることができます。これらは関数デコレータと同様に動作しますが、クラス全体に適用されます。

以下は、デコレーションのないシンプルなクラスです:

class Person:
    def __init__(self, name):
        self.name = name
        
    def greet(self):
        return f"Hello, my name is {self.name}"

新しいメソッドを追加するクラスデコレータを作成します:

def add_farewell(cls):
    def farewell(self):
        return f"Goodbye from {self.name}"
    
    cls.farewell = farewell  # クラスにメソッドを追加する
    return cls

デコレータをクラスに適用するには、@ を使用します:

@add_farewell
class EnhancedPerson:
    def __init__(self, name):
        self.name = name
        
    def greet(self):
        return f"Hello, my name is {self.name}"

これで、クラスには元のメソッドと追加されたメソッドの両方が含まれるようになりました:

person = EnhancedPerson("Alice")
print(person.greet())     # こんにちは、私の名前は Alice です
print(person.farewell())  # Alice からのさようなら

既存のメソッドをラップすることもできます。元のメソッドを保存しておくことで、ラッパー内から引き続き呼び出すことが可能です。これは、クラスが既に持っているメソッドに(トラッキングやロギングなどの)動作を追加したい場合に便利です。

def add_tracking(cls):
    original_greet = cls.greet  # 元のメソッドを保存する

    def tracked_greet(self):
        print(f"greet was called")   # 前に追加の動作
        return original_greet(self)  # 元のメソッドを呼び出す

    cls.greet = tracked_greet  # クラスのメソッドを置き換える
    return cls

@add_tracking
class TrackedPerson:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, my name is {self.name}"

greet を呼び出すと、最初にラッパーが実行され、次に元のメソッドに処理が委譲されます。

person = TrackedPerson("Alice")
print(person.greet())
# greet が呼び出されました
# Hello, my name is Alice

既存のメソッドをラップするための主な手順は次のとおりです:
1. 元のメソッドを保存します: original = cls.method
2. original(self) と追加のロジックを呼び出す新しい関数を定義します。
3. 新しい関数をクラスに再代入します: cls.method = new_function
4. 変更されたクラスを返します。

重要なポイント: クラスデコレータは、クラスを入力として受け取り、それを修正または拡張して、修正されたクラスを返します。これらはメソッドの追加、属性の変更、または既存の機能のラップを行うことができます。これらを適用するには、クラス定義の上に @decorator_name を使用します。これにより、継承を使わずにクラスの機能を拡張するクリーンな方法が提供されます。

challenge icon

チャレンジ

中級

このチャレンジでは、メソッドの呼び出し追跡機能を追加するクラスデコレータを実装します。

  • decorator.py - add_counter クラスデコレータを実装します
  • calculator.py - デコレータを使用するクラスが含まれています

driver.py ファイルには、さまざまなユースケース、継承パターン、およびエッジケースに対して実装を検証するための広範なテストシナリオが含まれています。このファイルを変更する必要はありませんが、内容を確認することで期待される動作を理解するのに役立ちます。

チートシート

クラスデコレータは、クラスを別の関数でラップすることで、クラスを修正または拡張します。これらはクラスを入力として受け取り、それを修正して、修正されたクラスを返します。

基本的なクラスデコレータの構文:

def decorator_name(cls):
    # Modify the class
    return cls

@decorator_name
class MyClass:
    pass

例 - クラスにメソッドを追加する:

def add_farewell(cls):
    def farewell(self):
        return f"Goodbye from {self.name}"
    
    cls.farewell = farewell  # Add the method to the class
    return cls

@add_farewell
class EnhancedPerson:
    def __init__(self, name):
        self.name = name
        
    def greet(self):
        return f"Hello, my name is {self.name}"

person = EnhancedPerson("Alice")
print(person.greet())     # Hello, my name is Alice
print(person.farewell())  # Goodbye from Alice

例 - 既存のメソッドをラップする(オリジナルを保存してから置き換える):

def loud_greet(cls):
    original_greet = cls.greet  # Store the original method

    def new_greet(self):
        result = original_greet(self)  # Call the original
        return result.upper()          # Enhance the result

    cls.greet = new_greet  # Replace with the wrapped version
    return cls

@loud_greet
class EnhancedPerson:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, my name is {self.name}"

person = EnhancedPerson("Alice")
print(person.greet())  # HELLO, MY NAME IS ALICE

既存のメソッドをラップするための重要なステップ:

original = cls.method_name — オリジナルのメソッドを保存する
def new_method(self): original(self) — ラッパーの中でそれを呼び出す
cls.method_name = new_method — クラスのメソッドを置き換える

クラスデコレータを使用すると、継承を使わずにメソッドの追加、属性の修正、または既存の機能のラップを行うことができます。

自分で試してみよう

from calculator import Calculator

# 包括的なテストケースハンドラー
test_case = input()

if test_case == "basic_functionality":
    calc = Calculator()
    print(calc.add(5, 3))  # 8を出力するはずです
    print(calc.add(2, 7))  # 9を出力するはずです
    print(calc.subtract(10, 4))  # 6を出力するはずです
    print(calc.call_counts)  # {'add': 2, 'subtract': 1}を出力するはずです

elif test_case == "multiple_instances":
    calc1 = Calculator()
    calc2 = Calculator()
    
    print(calc1.add(5, 3))  # 8を出力するはずです
    print(calc2.add(2, 7))  # 9を出力するはずです
    print(calc1.subtract(10, 4))  # 6を出力するはずです
    
    # 両方のインスタンスで同じcall_countsを共有する必要があります
    print(calc1.call_counts)  # {'add': 2, 'subtract': 1}を出力するはずです
    print(calc2.call_counts)  # {'add': 2, 'subtract': 1}を出力するはずです
    print(calc1.call_counts is calc2.call_counts)  # Trueを出力するはずです

elif test_case == "method_call_order":
    calc = Calculator()
    
    print(calc.add(1, 2))  # 3を出力するはずです
    print(calc.subtract(5, 2))  # 3を出力するはずです
    print(calc.add(10, 20))  # 30を出力するはずです
    print(calc.add(7, 3))  # 10を出力するはずです
    print(calc.call_counts)  # {'add': 3, 'subtract': 1}を出力するはずです

elif test_case == "large_values":
    calc = Calculator()
    
    print(calc.add(1000000, 2000000))  # 3000000を出力するはずです
    print(calc.subtract(5000000, 2000000))  # 3000000を出力するはずです
    print(calc.call_counts)  # {'add': 1, 'subtract': 1}を出力するはずです

elif test_case == "negative_values":
    calc = Calculator()
    
    print(calc.add(-5, -3))  # -8を出力するはずです
    print(calc.subtract(-10, -4))  # -6を出力するはずです
    print(calc.call_counts)  # {'add': 1, 'subtract': 1}を出力するはずです

elif test_case == "float_values":
    calc = Calculator()
    
    print(calc.add(5.5, 3.2))  # 8.7を出力するはずです
    print(calc.subtract(10.5, 4.2))  # 6.3を出力するはずです
    print(calc.call_counts)  # {'add': 1, 'subtract': 1}を出力するはずです

elif test_case == "zero_values":
    calc = Calculator()
    
    print(calc.add(0, 0))  # 0を出力するはずです
    print(calc.subtract(0, 0))  # 0を出力するはずです
    print(calc.call_counts)  # {'add': 1, 'subtract': 1}を出力するはずです

elif test_case == "counter_reset":
    calc = Calculator()
    
    print(calc.add(5, 3))  # 8を出力するはずです
    print(calc.call_counts)  # {'add': 1, 'subtract': 0}を出力するはずです
    
    # カウンターをリセット
    calc.call_counts = {"add": 0, "subtract": 0}
    
    print(calc.add(2, 7))  # 9を出力するはずです
    print(calc.call_counts)  # {'add': 0, 'subtract': 0}を出力するはずです

elif test_case == "counter_manipulation":
    calc = Calculator()
    
    print(calc.add(5, 3))  # 8を出力するはずです
    print(calc.call_counts)  # {'add': 1, 'subtract': 0}を出力するはずです
    
    # カウンターを直接操作
    calc.call_counts["add"] = 100
    
    print(calc.add(2, 7))  # 9を出力するはずです
    print(calc.call_counts)  # {'add': 101, 'subtract': 0}を出力するはずです

elif test_case == "performance_test":
    calc = Calculator()
    
    # 1000回の加算操作を実行
    for i in range(1000):
        calc.add(i, i+1)
    
    # 500回の減算操作を実行
    for i in range(500):
        calc.subtract(i+10, i)
    
    print(calc.call_counts)  # {'add': 1000, 'subtract': 500}を出力するはずです
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

Object Oriented Programmingのすべてのレッスン