見出し画像

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

  • クラスデコレータは、メタクラスを使わなくてもクラスの振る舞いや属性を簡潔に変更・拡張できる手法。

  • 静的メソッド、クラスメソッド、プロパティなど多様な要素を一括してデコレートし、デバッグログの自動付加や属性注入などを実現可能。

  • メタクラスよりも可読性や保守性を損ないにくく、シンプルかつ強力なメタプログラミングのアプローチとして有効。

Pythonにおけるメタプログラミングは、常にメタクラスを必要とするわけではありません。実際、多くの場合はクラスデコレータの方が、クラスを定義するときに修正や拡張を行う上で分かりやすく、扱いやすい方法となります。メタクラスは強力である一方、コードを理解しにくくしてしまう可能性があります。これに対してクラスデコレータは、関数デコレータの機能的なスタイルに近く、多くのユースケースで直感的です。以下では、クラスデコレータの仕組みを、単純な属性注入からメソッド、静的メソッド、クラスメソッド、さらにはプロパティに至るまで、どのように取り扱うかを解説していきます。


メタクラスが過剰に思えるケース

メタクラスは確かに強力ですが、可読性やメンテナンス性を損ないがちです。もしクラスに属性やメソッドをいくつか追加・生成するだけで十分なら、メタクラスではなくクラスデコレータを検討してください。デコレータもすでにメタプログラミングの一種です。オブジェクト(ここではクラス)を引数に受け取り、必要に応じて修正し、(場合によっては)全く別のクラスを返すことで、定義後のクラスを変身させることができます。それでもクラス定義はシンプルなまま保たれます。


クラスデコレータの基本

Pythonにおけるクラスデコレータは、関数デコレータと同様の文法を用います:

def my_class_decorator(cls):
    # クラス (cls) に対して何らかの調整を行う
    return cls

@my_class_decorator
class MyClass:
    ...

これは以下の処理に展開されます:

class MyClass:
    ...
MyClass = my_class_decorator(MyClass)

唯一の違いは、`my_class_decorator` が受け取るのが関数オブジェクトではなく、クラスオブジェクトだという点です。多くの場合、このデコレータは同じクラスを返しつつ、その辞書(属性)を少し修正するだけです。例えば、最も単純なケースではデータ属性を注入します:

def savings_account(cls):
    cls.account_type = 'Savings'
    return cls

@savings_account
class BankAccount:
    pass

このとき、`BankAccount` は作成された直後に `savings_account(BankAccount)` が呼び出され、`account_type = 'Savings'` がクラスに追加されます。確認するには:

BankAccount.account_type
# 'Savings'

クラスデコレータのパラメータ化

クラスデコレータは、関数デコレータと同様、引数を取らせることが可能です。利率(APR)を設定したい場合を考えてみます:

def apr(rate):
    def inner_decorator(cls):
        cls.apr = rate
        return cls
    return inner_decorator

@apr(0.02)
class SavingsAccount:
    pass

@apr(0.0)
class CheckingAccount:
    pass

これにより、各クラスが異なるパラメータを持つ装飾を受けられます。これをメタクラスで行うこともできますが、追加の名前付き引数をクラス定義に指定する必要があり、通常はデコレータを使うほうがシンプルです。


クラスへのメソッド注入

静的なデータ属性と同様に、新しいメソッドを注入することも可能です。例えば:

def hello(cls):
    cls.hello = lambda self: f"{self} says hello!"
    return cls

@hello
class Person:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name

p = Person('Guido')
p.hello()  # 'Guido says hello!'

このデコレータはクラス辞書に `hello` 関数を追加します。インスタンスからアクセスするとバウンドメソッドになります。メタクラスを使わなくてもこうしたカスタマイズが可能です!


クラス内のすべての呼び出し可能オブジェクトをデコレートする

より強力な使い方として、クラス内の すべての メソッドにデコレータを自動付与する例が考えられます。例えば、すべての呼び出しをログに残したいとしましょう。まずは関数デコレータを用意します:

from functools import wraps

def func_logger(fn):
    @wraps(fn)
    def inner(*args, **kwargs):
        result = fn(*args, **kwargs)
        print(f"log: {fn.__qualname__}({args}, {kwargs}) = {result}")
        return result
    return inner

次に、クラス内の要素を走査して、見つけた呼び出し可能オブジェクトを片っ端からデコレートするクラスデコレータを用意します:

def class_logger(cls):
    for name, obj in vars(cls).items():
        if callable(obj):
            print(f"decorating: {cls}, {name}")
            setattr(cls, name, func_logger(obj))
    return cls

@class_logger
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def greet(self):
        return f"Hello, my name is {self.name} and I am {self.age}"

注意:静的メソッドやクラスメソッドは、その正体が `staticmethod` や `classmethod` の デスクリプタ であるため、`callable(...)` が `True` を返さないことがあります。もしそれらを含めてデコレートしたい場合は、追加の論理を加えて `obj.func を取り出し、再び `@staticmethod` や `@classmethod` で包み直す必要があります。プロパティについても同じ要領でデコレータを適用します。


静的メソッド、クラスメソッド、プロパティへの対応

静的メソッドとクラスメソッド

`@staticmethod` と `@classmethod` はオリジナルの関数をデスクリプタオブジェクトで包むため、そのままでは関数として認識されません。そこで:

  1. `isinstance(obj, staticmethod)` または `isinstance(obj, classmethod)` を使い判別。

  2. `obj.func で元の関数を取り出す。

  3. その関数をデコレートする。

  4. 戻ってきた関数を再度 `staticmethod(...)` または `classmethod(...)` で包む。

def class_logger(cls):
    for name, obj in vars(cls).items():
        if isinstance(obj, staticmethod):
            original = obj.__func__
            decorated = func_logger(original)
            setattr(cls, name, staticmethod(decorated))
        elif isinstance(obj, classmethod):
            original = obj.__func__
            decorated = func_logger(original)
            setattr(cls, name, classmethod(decorated))
        elif callable(obj):
            setattr(cls, name, func_logger(obj))
    return cls

プロパティ

プロパティもまたデスクリプタです。`property` オブジェクトには `fget`、`fset`、`fdel` があり、これらは直接上書きできません。そこで新しいプロパティを生成する必要があります。以下は簡易例です:

def class_logger(cls):
    for name, obj in vars(cls).items():
        if isinstance(obj, property):
            # fget, fset, fdel をデコレート
            if obj.fget:
                fget = func_logger(obj.fget)
                obj = obj.getter(fget)
            if obj.fset:
                fset = func_logger(obj.fset)
                obj = obj.setter(fset)
            if obj.fdel:
                fdel = func_logger(obj.fdel)
                obj = obj.deleter(fdel)
            setattr(cls, name, obj)
        # 静的メソッドやクラスメソッド、通常のメソッドを処理
    return cls

これで、プロパティのゲッターやセッター、デリータによるアクセスもログに取り込まれます。


過剰なデコレートを避ける

`callable(...)` が `True` を返す全要素をそのままデコレートすると、クラスとして定義しているものや call メソッドを持つオブジェクトまで誤ってデコレートしてしまう場合があります。そうした状況を回避するために、`inspect` モジュールを使ってより厳密にチェックする方法があります。

import inspect

def class_logger(cls):
    for name, obj in vars(cls).items():
        # staticmethod, classmethod, property を先に処理
        elif inspect.isroutine(obj):
            # inspect が "ルーチン" と判断したものだけデコレート
            setattr(cls, name, func_logger(obj))
    return cls

こうすることで、ネストされたクラスやカスタムのコール可能オブジェクトなど、“想定外の呼び出し可能” なものを飛ばせます。


メタクラスに比べて親しみやすいアプローチ

ここまでの例は、注入する属性、既存メソッドの置き換え、またはメソッドの自動ラップなど、多くの場面でクラスデコレータがメタクラスと同等の効果を、はるかに軽い仕組みで実現できることを示しています。結果としては “魔法”的な変化が起こりますが、どのようにクラスを包み替えているかが関数的に明示されるぶん、メタクラスほど読み手を混乱させません。


まとめ

  • クラスデコレータは、メタクラスよりもシンプル なことが多い。

  • クラスが生成された後 に実行されるため、Pythonはすでにクラスボディや辞書を準備済み。

  • 属性やメソッド、プロパティを動的に修正・追加 できる。

  • 発展させれば、ほとんどあらゆる要素を変形・装飾できる。

もちろん使いすぎには注意が必要です。クラスデコレータが増えすぎると、メタクラス並みにコードが混乱する場合もあります。ただし、ほんの数個のクラスに属性やメソッドを注入する程度であれば、クラスデコレータはドライ(DRY)で簡潔な解決策となるでしょう。Pythonの動的特性を活かしつつ、メタクラスのような深い複雑性を避けられます。

要するに、シンプルな解決策で十分なら、そちらを選ぶ方が将来的に保守しやすいでしょう。クラスデコレータが目的を満たすなら、それがPythonのメタプログラミングにおける柔軟性と明瞭さのちょうどよい落としどころと言えます。


「超温和なパイソン」へ

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