見出し画像

Python 3: Deep Dive (Part 4 - OOP): デコレータクラス (セクション14-4/15)

  • デコレータクラスでは `call` と `get` を実装し、インスタンスメソッドにも正しく適用できるようにする。

  • クラスデコレータは簡単かつスタック可能だが、サブクラスに継承されない点がメタクラスとの大きな違い。

  • Python 3.6以降では、メタクラスの `new` に追加パラメータをキーワード引数で渡してクラス作成を拡張できる。

Python のメタプログラミングツールボックスには、関数デコレータ、クラスデコレータ、メタクラス、さらには クラス を使ってデコレータを作成する方法など、さまざまなテクニックが含まれています。これらのトピックは最初は混乱を招くかもしれません。以下では、デコレータクラス(クラスデコレータと混同しないように注意)をどう使うか、メタクラス vs クラスデコレータ を比較して違いを確認する方法、そして Python 3.6 以降でサポートされているメタクラスに追加パラメータを渡す方法を見ていきます。


クラスを使って関数をデコレートする

ほとんどの人がデコレータを学ぶときは「関数を引数として受け取り、ラップした関数を返す関数」として覚えます。しかし、クラスもデコレータとして機能できます。やることはシンプルです:

  1. デコレート対象の関数をコンストラクタ(`init`)で受け取り、保持するクラスを書く。

  2. クラスをインスタンス化して関数を渡したときに呼び出し可能なオブジェクト(“デコレート済み”)を返すよう、`call` を実装する。

例えば、こちらが 関数 デコレータとしてのシンプルなロガーです:

from functools import wraps

def logger(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        print(f'Log: {fn.__name__} called.')
        return fn(*args, **kwargs)
    return wrapped

これを クラス に書き直すと:

class Logger:
    def __init__(self, fn):
        self.fn = fn   # デコレート対象の関数を保持
    
    def __call__(self, *args, **kwargs):
        print(f'Log: {self.fn.__name__} called.')
        return self.fn(*args, **kwargs)

どちらのアプローチでも、スタンドアロンの関数をデコレートする場合は似たように動作します:

@Logger
def say_hello():
    pass

say_hello()  
# Log: say_hello called.

しかし、大きな違いとして、デコレート後の関数はもはや関数ではなく、`Logger` のインスタンスになっている点があります。これは、例えば `type(say_hello)` が `Logger` になるなど、イントロスペクションを行う場合や、クラス内のメソッドをデコレートするときに影響することがあります。


インスタンスメソッドにデコレータクラスを適用するための工夫

上記の `Logger` でクラス内メソッドをデコレートしようとすると、例えば:

class Person:
    def __init__(self, name):
        self.name = name
    
    @Logger
    def say_hello(self):
        return f'{self.name} says hello!'

次のようなエラーがよく起こります:

TypeError: say_hello() missing 1 required positional argument: 'self'

なぜでしょうか? Python でメソッドが `self` をバインドする仕組みはデスクリプタプロトコルに依存しています。通常の関数は `get` メソッドを持ち、インスタンスから呼ばれたときにバウンドメソッド(bound method)を返します。しかし私たちのデコレータクラスインスタンスには `get` が実装されておらず、バウンドメソッドにはならないため、Python はただの呼び出し可能オブジェクトとして扱い、`self` を渡しません。

これを修正する 方法は簡単で、`get` を実装して明示的にバインドメソッドを返すようにします:

from types import MethodType

class Logger:
    def __init__(self, fn):
        self.fn = fn
    
    def __call__(self, *args, **kwargs):
        print(f'Log: {self.fn.__name__} called.')
        return self.fn(*args, **kwargs)
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        # この Logger オブジェクト(呼び出し可能)を instance にバインドする
        return MethodType(self, instance)

これにより、デコレートされたメソッドは正しいバウンドメソッドとして機能します。インスタンスから呼ばれたときは、`instance` が第一引数として渡されるようになります。


メタクラス vs クラスデコレータ

メタクラスクラスデコレータ の両方とも、クラスの生成やクラスロジックに対して “フック” を提供できます。それぞれ異なる形で強力です:

$$
\begin{array}{|c|c|c|} \hline
 & クラスデコレータ & メタクラス \\ \hline
理解のしやすさ & 通常はよりシンプルで直感的、“関数的” & 初学者には難解 \\ \hline
スタック可能性 & 複数のクラスデコレータを容易に重ね掛けできる & 1 つのメタクラスしか指定できない(ただし継承による拡張は可) \\ \hline
継承 & サブクラスにデコレータは自動では継承されない & サブクラスは親のメタクラスを継承 \\ \hline
柔軟性 & 継承チェーンの各クラスを別々のデコレータでデコレートできる & 1 つのメタクラスアプローチが継承階層全体で一貫性をもつ \\ \hline
多重継承 & 特に直接的な問題はない & 親クラスが異なるメタクラスを使っていると衝突を起こす可能性がある \\ \hline
\end{array}
$$

クラスデコレータ は、クラス属性を追加・変更したり、メソッドをラップしたり、一般的な変換を行う程度なら十分です。また、複数のデコレータをまとめて適用するのも簡単です。
しかし、すべてのサブクラスに対して自動的に挙動を継承させたい場合や、クラスオブジェクトそのものの生成過程に深くフックしたい場合は、メタクラス が適しています。

例: 継承における違い

  • クラスデコレータ の場合:

  @class_logger
  class Person:
      def __init__(self, name, age):
          self.name = name
          self.age = age
      def greet(self):
          return f'Hello, {self.name}!'
  
  class Student(Person):
      def __init__(self, name, age, student_number):
          super().__init__(name, age)
          self.student_number = student_number
      def study(self):
          return f'{self.name} studies...'

ここで `Student` は自動的にはデコレートされず、もう一度 `@class_logger` を書く必要があります。

  • メタクラス の場合:

  class ClassLogger(type):
      def __new__(mcls, name, bases, cls_dict):
          new_cls = super().__new__(mcls, name, bases, cls_dict)
          # ... たとえば全ての呼び出し可能オブジェクトをデコレート ...
          return new_cls
  
  class Person(metaclass=ClassLogger):
      ...
  
  class Student(Person):
      ...

`Student` は同じメタクラスを自動的に継承し、“ログ出力” のような振る舞いを余分に指定なしで使えます。


メタクラスにパラメータを渡す(new)

Python 3.6 以降では、クラス定義時にメタクラスの `new` メソッドへ 追加の名前付きパラメータ を渡せます。通常、Python は次のように呼び出します:

metaclass.__new__(metaclass, name, bases, cls_dict)

しかし、このシグネチャを拡張可能です:

class MyMeta(type):
    def __new__(mcls, name, bases, cls_dict, arg1, arg2, arg3=None):
        print(arg1, arg2, arg3)
        return super().__new__(mcls, name, bases, cls_dict)

class MyClass(metaclass=MyMeta, arg1=10, arg2=20, arg3=30):
    pass
# 10 20 30

追加の引数はすべて名前付き(キーワード)で渡す必要があります。位置引数は `class MyClass(...):` の継承元クラスを指定するために使われるからです。

実用的な例

クラスレベルの属性を動的に追加できる例を示します:

class AutoAttrib(type):
    def __new__(mcls, name, bases, cls_dict, **kwargs):
        new_cls = super().__new__(mcls, name, bases, cls_dict)
        # キーワード引数をクラスの属性として追加する
        for attr_name, attr_value in kwargs.items():
            setattr(new_cls, attr_name, attr_value)
        return new_cls

class Account(metaclass=AutoAttrib, account_type='Savings', apr=0.5):
    pass

print(vars(Account))
# {
#   '__module__': '__main__',
#   '__dict__': <attribute '__dict__' of 'Account' objects>,
#   '__weakref__': <attribute '__weakref__' of 'Account' objects>,
#   '__doc__': None,
#   'account_type': 'Savings',
#   'apr': 0.5
# }

まとめ

  1. デコレータクラス: クラスインスタンスを使ってデコレータを書くための便利な方法。クラス内メソッドをデコレートしたい場合は、`call` だけでなく `get` も実装しましょう。

  2. メタクラス vs クラスデコレータ: 継承ベースの一貫性や、クラス作成時の深いフックが必要ならメタクラス。単純な変換や複数のデコレータを重ね掛けしたいならクラスデコレータ。

  3. メタクラスパラメータ: Python 3.6+ では、`type.new` に追加の名前付き引数を渡せるので、クラス作成ロジックに追加データを埋め込めます。

デコレータクラス、クラスデコレータ、メタクラスのいずれを使うにしても、それぞれの挙動や利点・使いどころ、どう組み合わせられるかを理解することが重要です。これらの要素に慣れるほど、Python の柔軟性を活かして、よりエレガントで再利用可能かつ表現力の高いコードが書けるようになるでしょう。

楽しいコーディングを、そしてメタプログラミングの冒険がより明快になりますように!

「超温和なパイソン」へ

いいなと思ったら応援しよう!