クラスデコレータについて学ぶ

Beginner

This tutorial is from open-source community. Access the source code

はじめに

この実験 (lab) では、Python のクラスデコレータについて学び、Python のディスクリプタを復習・拡張します。これらの概念を組み合わせることで、強力でクリーンなコード構造を作成できます。

この実験 (lab) では、以前のディスクリプタの概念を基盤とし、クラスデコレータを使用してそれらを拡張します。この組み合わせにより、検証機能を強化した、よりクリーンで保守性の高いコードを作成できます。修正するファイルは validate.pystructure.py です。

ディスクリプタによる型チェックの実装

このステップでは、型チェックにディスクリプタを使用する Stock クラスを作成します。しかし、まずディスクリプタとは何かを理解しましょう。ディスクリプタは Python の非常に強力な機能です。これにより、クラスの属性へのアクセス方法を制御できます。

ディスクリプタは、他のオブジェクトの属性へのアクセス方法を定義するオブジェクトです。これは、__get____set____delete__ のような特殊なメソッドを実装することによって行われます。これらのメソッドにより、ディスクリプタは属性の取得、設定、削除の方法を管理できます。ディスクリプタは、検証、型チェック、計算プロパティの実装に非常に役立ちます。例えば、ディスクリプタを使用して、属性が常に正の数または特定の形式の文字列であることを保証できます。

validate.py ファイルには、すでにバリデータクラス (StringPositiveIntegerPositiveFloat) が含まれています。これらのクラスを使用して、Stock クラスの属性を検証できます。

それでは、ディスクリプタを使用した Stock クラスを作成しましょう。

  1. まず、エディタで stock.py ファイルを開きます。

  2. ファイルが開いたら、プレースホルダーの内容を次のコードに置き換えます。

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    _fields = ('name', 'shares', 'price')
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

## _fields に基づいて __init__ メソッドを作成します
Stock.create_init()

このコードが何をするのかを分解してみましょう。_fields タプルは Stock クラスの属性を定義します。これらは、Stock オブジェクトが持つ属性の名前です。

namesharesprice 属性はディスクリプタオブジェクトとして定義されています。String() ディスクリプタは、name 属性が文字列であることを保証します。PositiveInteger() ディスクリプタは、shares 属性が正の整数であることを保証します。そして PositiveFloat() ディスクリプタは、price 属性が正の浮動小数点数であることを保証します。

cost プロパティは計算プロパティです。これは、株式数と 1 株あたりの価格に基づいて株式の総コストを計算します。

sell メソッドは、株式数を減らすために使用されます。売却する株式数を指定してこのメソッドを呼び出すと、その数が shares 属性から差し引かれます。

Stock.create_init() 行は、クラスの __init__ メソッドを動的に作成します。このメソッドにより、namesharesprice 属性の値を渡して Stock オブジェクトを作成できます。

  1. コードを追加したら、ファイルを保存します。これにより、変更が保存され、テストを実行する際に使用できるようになります。

  2. それでは、テストを実行して実装を確認しましょう。まず、次のコマンドを実行してディレクトリを ~/project ディレクトリに変更します。

cd ~/project

次に、次のコマンドを使用してテストを実行します。

python3 teststock.py

実装が正しければ、次のような出力が表示されるはずです。

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

この出力は、すべてのテストが合格したことを意味します。ディスクリプタは、各属性の型を正常に検証しています!

Python インタープリタで Stock オブジェクトを作成してみましょう。まず、~/project ディレクトリにいることを確認してください。次に、次のコマンドを実行します。

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

次の出力が表示されるはずです。

Stock('GOOG', 100, 490.1)
Cost: 49010.0

型チェックのためのディスクリプタを正常に実装しました!それでは、このコードをさらに改善しましょう。

検証のためのクラスデコレータの作成

前のステップでは、実装は機能しましたが、冗長性がありました。_fields タプルとディスクリプタ属性の両方を指定する必要がありました。これはあまり効率的ではなく、改善できます。Python では、クラスデコレータは、このプロセスを簡素化するのに役立つ強力なツールです。クラスデコレータは、クラスを引数として受け取り、それを何らかの方法で変更してから、変更されたクラスを返す関数です。クラスデコレータを使用することで、ディスクリプタからフィールド情報を自動的に抽出でき、コードがよりクリーンで保守しやすくなります。

コードを簡素化するためにクラスデコレータを作成しましょう。以下に、従う必要がある手順を示します。

  1. まず、エディタで structure.py ファイルを開きます。

  2. 次に、structure.py ファイルの先頭、インポート文の直後に次のコードを追加します。このコードはクラスデコレータを定義します。

from validate import Validator

def validate_attributes(cls):
    """
    Validator インスタンスを抽出し、_fields リストを自動的に構築する
    クラスデコレータ
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## バリデータ名に基づいて _fields を設定します
    cls._fields = [val.name for val in validators]

    ## 初期化メソッドを作成します
    cls.create_init()

    return cls

このデコレータが何をするのかを分解してみましょう。

  • まず、validators という空のリストを作成します。次に、vars(cls).items() を使用してクラスのすべての属性を反復処理します。属性が Validator クラスのインスタンスである場合、その属性を validators リストに追加します。
  • その後、クラスの _fields 属性を設定します。validators リスト内のバリデータから名前のリストを作成し、それを cls._fields に割り当てます。
  • 最後に、クラスの create_init() メソッドを呼び出して __init__ メソッドを生成し、変更されたクラスを返します。
  1. コードを追加したら、structure.py ファイルを保存します。ファイルを保存することで、変更が保持されます。

  2. 次に、この新しいデコレータを使用するように stock.py ファイルを変更する必要があります。エディタで stock.py ファイルを開きます。

  3. validate_attributes デコレータを使用するように stock.py ファイルを更新します。既存のコードを次のコードに置き換えます。

## stock.py

from structure import Structure, validate_attributes
from validate import String, PositiveInteger, PositiveFloat

@validate_attributes
class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

変更点に注目してください。

  • Stock クラス定義のすぐ上に @validate_attributes デコレータを追加しました。これにより、Python は validate_attributes デコレータを Stock クラスに適用するように指示されます。
  • デコレータが自動的に処理するため、明示的な _fields 宣言を削除しました。
  • デコレータが __init__ メソッドの作成を担当するため、Stock.create_init() の呼び出しも削除しました。

その結果、クラスはよりシンプルでクリーンになりました。デコレータは、以前は手動で処理していたすべての詳細を処理します。

  1. これらの変更を行った後、すべてが期待どおりに機能することを確認する必要があります。次のコマンドを使用して、再度テストを実行します。
cd ~/project
python3 teststock.py

すべてが正しく機能していれば、次の出力が表示されるはずです。

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

この出力は、すべてのテストが正常に合格したことを示しています。

Stock クラスをインタラクティブにテストしましょう。ターミナルで次のコマンドを実行します。

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

次の出力が表示されるはずです。

Stock('GOOG', 100, 490.1)
Cost: 49010.0

素晴らしい!フィールド宣言と初期化を自動的に処理することでコードを簡素化するクラスデコレータを正常に実装しました。これにより、コードはより効率的で保守しやすくなります。

継承によるデコレータの適用

ステップ 2 では、コードを簡素化するクラスデコレータを作成しました。クラスデコレータは、クラスを引数として受け取り、変更されたクラスを返す特別な種類の関数です。これは、元のコードを変更せずにクラスに機能を追加するための Python の便利なツールです。しかし、すべてのクラスに @validate_attributes デコレータを明示的に適用する必要があります。これは、検証が必要な新しいクラスを作成するたびに、このデコレータを追加することを覚えておく必要があることを意味し、これは少し面倒になる可能性があります。

継承を通じてデコレータを自動的に適用することで、これをさらに改善できます。継承は、サブクラスが親クラスから属性とメソッドを継承できるオブジェクト指向プログラミングの基本的な概念です。Python の __init_subclass__ メソッドは Python 3.6 で導入され、親クラスがサブクラスの初期化をカスタマイズできるようにしました。これは、サブクラスが作成されると、親クラスがそれに何らかのアクションを実行できることを意味します。この機能を使用して、Structure を継承するすべてのクラスにデコレータを自動的に適用できます。

これを実装しましょう。

  1. エディタで structure.py ファイルを開きます。このファイルには Structure クラスの定義が含まれており、__init_subclass__ メソッドを使用するように変更します。

  2. Structure クラスに __init_subclass__ メソッドを追加します。

class Structure:
    _fields = ()
    _types = ()

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f'{type(self).__name__}({values})'

    @classmethod
    def create_init(cls):
        '''
        _fields から __init__ メソッドを作成します
        '''
        body = 'def __init__(self, %s):\n' % ', '.join(cls._fields)
        for name in cls._fields:
            body += f'    self.{name} = {name}\n'

        ## 関数作成コードを実行します
        namespace = {}
        exec(body, namespace)
        setattr(cls, '__init__', namespace['__init__'])

    @classmethod
    def __init_subclass__(cls):
        validate_attributes(cls)

__init_subclass__ メソッドはクラスメソッドであり、クラスのインスタンスではなくクラス自体で呼び出すことができます。Structure のサブクラスが作成されると、このメソッドが自動的に呼び出されます。このメソッド内で、サブクラス cls に対して validate_attributes デコレータを呼び出します。このようにして、Structure のすべてのサブクラスは自動的に検証動作を持つようになります。

  1. ファイルを保存します。

structure.py ファイルに変更を加えたら、変更が適用されるように保存する必要があります。

  1. それでは、この新機能を利用するように stock.py ファイルを更新しましょう。エディタで stock.py ファイルを開いて変更します。このファイルには Stock クラスの定義が含まれており、自動デコレータ適用を使用するために Structure クラスから継承するように変更します。

  2. stock.py ファイルを変更して、明示的なデコレータを削除します。

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

以下の変更を行ったことに注意してください。

  • デコレータは継承を通じて自動的に適用されるため、明示的にインポートする必要がなくなったため、validate_attributes のインポートを削除しました。
  • Structure クラスの __init_subclass__ メソッドが適用を処理するため、@validate_attributes デコレータを削除しました。
  • コードは、検証動作を取得するために Structure からの継承のみに依存するようになりました。
  1. すべてがまだ機能することを確認するために、再度テストを実行します。
cd ~/project
python3 teststock.py

テストを実行することは、変更によって何も壊れていないことを確認するために重要です。すべてのテストが合格した場合、それは継承による自動デコレータ適用が正しく機能していることを意味します。

すべてのテストが合格するはずです。

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Stock クラスを再度テストして、期待どおりに機能することを確認しましょう。

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

このコマンドは Stock クラスのインスタンスを作成し、その表現とコストを出力します。出力が期待どおりであれば、Stock クラスが自動デコレータ適用で正しく機能していることを意味します。

出力:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

この実装はさらにクリーンです!__init_subclass__ を使用することで、デコレータを明示的に適用する必要がなくなりました。Structure を継承するすべてのクラスは、自動的に検証動作を取得します。

行変換機能の追加

プログラミングでは、特に CSV ファイルのようなソースからのデータを扱う場合、データ行からクラスのインスタンスを作成することがよくあります。このセクションでは、Structure クラスのインスタンスをデータ行から作成する機能を追加します。Structure クラスに from_row クラスメソッドを実装することでこれを行います。

  1. まず、エディタで structure.py ファイルを開きます。ここでコードの変更を行います。

  2. 次に、validate_attributes 関数を変更します。この関数は、Validator インスタンスを抽出し、_fields および _types リストを自動的に構築するクラスデコレータです。型情報も収集するように更新します。

def validate_attributes(cls):
    """
    Validator インスタンスを抽出し、_fields および_types リストを自動的に構築する
    クラスデコレータ
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## バリデータ名に基づいて _fields を設定します
    cls._fields = [val.name for val in validators]

    ## バリデータの expected_types に基づいて _types を設定します
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## 初期化メソッドを作成します
    cls.create_init()

    return cls

この更新された関数では、各バリデータから expected_type 属性を収集し、_types クラス変数に格納しています。これは、後で行からデータを正しい型に変換する際に役立ちます。

  1. 次に、Structure クラスに from_row クラスメソッドを追加します。このメソッドにより、リストまたはタプルのいずれかであるデータ行からクラスのインスタンスを作成できます。
@classmethod
def from_row(cls, row):
    """
    データ行(リストまたはタプル)からインスタンスを作成します
    """
    rowdata = [func(val) for func, val in zip(cls._types, row)]
    return cls(*rowdata)

このメソッドの仕組みは次のとおりです。

  • リストまたはタプルの形式のデータ行を受け取ります。
  • _types リストの対応する関数を使用して、行の各値を期待される型に変換します。
  • 次に、変換された値を使用してクラスの新しいインスタンスを作成して返します。
  1. これらの変更を行った後、structure.py ファイルを保存します。これにより、コードの変更が保持されます。

  2. from_row メソッドが期待どおりに機能することを確認するためにテストしましょう。Stock クラスを使用して簡単なテストを作成します。ターミナルで次のコマンドを実行します。

cd ~/project
python3 -c "from stock import Stock; s = Stock.from_row(['GOOG', '100', '490.1']); print(s); print(f'Cost: {s.cost}')"

次のような出力が表示されるはずです。

Stock('GOOG', 100, 490.1)
Cost: 49010.0

文字列値 '100' と '490.1' が自動的に正しい型(整数と浮動小数点数)に変換されたことに注意してください。これは、from_row メソッドが正しく機能していることを示しています。

  1. 最後に、reader.py モジュールを使用して CSV ファイルからデータを読み込んでみましょう。ターミナルで次のコマンドを実行します。
cd ~/project
python3 -c "from stock import Stock; import reader; portfolio = reader.read_csv_as_instances('portfolio.csv', Stock); print(portfolio); print(f'Total value: {sum(s.cost for s in portfolio)}')"

CSV ファイルから株が表示される出力が表示されるはずです。

[Stock('GOOG', 100, 490.1), Stock('AAPL', 50, 545.75), Stock('MSFT', 200, 30.47)]
Total value: 82391.5

from_row メソッドを使用すると、CSV データを Stock クラスのインスタンスに簡単に変換できます。read_csv_as_instances 関数と組み合わせると、構造化データをロードして操作するための強力な方法が得られます。

メソッド引数検証の追加

Python では、堅牢なコードを作成する上でデータの検証は重要な部分です。このセクションでは、メソッド引数を自動的に検証することで、検証をさらに一歩進めます。validate.py ファイルにはすでに @validated デコレータが含まれています。Python のデコレータは、別の関数を変更できる特別な関数です。ここでの @validated デコレータは、関数引数を注釈と比較してチェックできます。Python の注釈は、関数パラメータと戻り値にメタデータを追加する方法です。

コードを変更して、このデコレータを注釈付きのメソッドに適用しましょう。

  1. まず、validated デコレータがどのように機能するかを理解する必要があります。エディタで validate.py ファイルを開いて確認してください。

validated デコレータは、関数注釈を使用して引数を検証します。関数を実行する前に、注釈付きの各パラメータに対してバリデータクラスのインスタンスを作成し、validate メソッドを呼び出して引数をチェックします。たとえば、引数が PositiveInteger で注釈付けされている場合、デコレータは PositiveInteger インスタンスを作成し、渡された値が実際に正の整数であることを検証します。検証が失敗した場合、すべてのエラーを収集し、詳細なエラーメッセージとともに TypeError を発生させます。

  1. 次に、structure.pyvalidate_attributes 関数を変更して、注釈付きのメソッドを validated デコレータでラップします。これは、クラス内の注釈付きのメソッドはすべて、引数が自動的に検証されることを意味します。エディタで structure.py ファイルを開きます。

  2. validate_attributes 関数を更新します。

def validate_attributes(cls):
    """
    クラスデコレータで、以下の処理を行います。
    1. Validator インスタンスを抽出し、_fields および_types リストを構築します。
    2. 注釈付きのメソッドに@validated デコレータを適用します。
    """
    ## validated デコレータをインポートします
    from validate import validated

    ## バリデータディスクリプタを処理します
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## バリデータ名に基づいて _fields を設定します
    cls._fields = [val.name for val in validators]

    ## バリデータの expected_types に基づいて _types を設定します
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## 注釈付きのメソッドに @validated デコレータを適用します
    for name, val in vars(cls).items():
        if callable(val) and hasattr(val, '__annotations__'):
            setattr(cls, name, validated(val))

    ## 初期化メソッドを作成します
    cls.create_init()

    return cls

この更新された関数は、次のことを行います。

  1. 以前と同様にバリデータディスクリプタを処理します。バリデータディスクリプタは、クラス属性の検証ルールを定義するために使用されます。

  2. クラス内のすべての注釈付きメソッドを見つけます。注釈はメソッドパラメータに追加され、引数の期待される型を指定します。

  3. これらのメソッドに @validated デコレータを適用します。これにより、これらのメソッドに渡される引数が注釈に従って検証されることが保証されます。

  4. これらの変更を行った後、ファイルを保存します。ファイルを保存することは、変更が保存され、後で使用できることを確認するために重要です。

  5. 次に、Stock クラスの sell メソッドを更新して注釈を含めましょう。注釈は、引数の期待される型を指定するのに役立ち、これは @validated デコレータによって検証に使用されます。エディタで stock.py ファイルを開きます。

  6. sell メソッドを更新して型注釈を含めます。

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

重要な変更は、nshares パラメータに : PositiveInteger を追加することです。これにより、Python(および @validated デコレータ)は PositiveInteger バリデータを使用してこの引数を検証するようになります。したがって、sell メソッドを呼び出すとき、nshares 引数は正の整数である必要があります。

  1. すべてがまだ機能することを確認するために、再度テストを実行します。テストを実行することは、変更によって既存の機能が壊れていないことを確認するための良い方法です。
cd ~/project
python3 teststock.py

すべてのテストが合格するはずです。

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK
  1. 新しい引数検証をテストしましょう。検証が期待どおりに機能するかどうかを確認するために、有効な引数と無効な引数で sell メソッドを呼び出してみます。
cd ~/project
python3 -c "
from stock import Stock
s = Stock('GOOG', 100, 490.1)
s.sell(25)
print(s)
try:
    s.sell(-25)
except Exception as e:
    print(f'Error: {e}')
"

次のような出力が表示されるはずです。

Stock('GOOG', 75, 490.1)
Error: Bad Arguments
  nshares: nshares must be >= 0

これは、メソッド引数検証が機能していることを示しています!sell(25) への最初の呼び出しは、25 が正の整数であるため成功します。しかし、sell(-25) への 2 回目の呼び出しは、-25 が正の整数ではないため失敗します。

これで、次のための完全なシステムが実装されました。

  1. ディスクリプタを使用したクラス属性の検証。ディスクリプタは、クラス属性の検証ルールを定義するために使用されます。
  2. クラスデコレータを使用したフィールド情報の自動収集。クラスデコレータは、フィールド情報の収集のように、クラスの動作を変更できます。
  3. 行データをインスタンスに変換。これは、外部ソースからのデータを扱う場合に役立ちます。
  4. 注釈を使用したメソッド引数の検証。注釈は、検証のために引数の期待される型を指定するのに役立ちます。

これは、Python でディスクリプタとデコレータを組み合わせることで、表現力豊かで自己検証型のクラスを作成できることを示しています。

まとめ

この実験では、強力な Python の機能を組み合わせて、クリーンで自己検証型のコードを作成する方法を学びました。属性検証のためのディスクリプタの使用、コード生成自動化のためのクラスデコレータの作成、継承によるデコレータの自動適用などの主要な概念を習得しました。

これらのテクニックは、堅牢で保守性の高い Python コードを作成するための強力なツールです。これにより、検証要件を明確に表現し、コードベース全体でそれらを強制することができます。これらのパターンを独自の Python プロジェクトに適用して、コードの品質を向上させ、定型コードを削減できるようになりました。