はじめに
この実験では、Python の最も強力で高度な機能の 1 つであるメタクラスについて学びます。メタクラスを使用すると、クラスの作成をカスタマイズでき、クラスの定義方法とインスタンス化方法を制御できます。実際の例を通じてメタクラスを探索します。
この実験の目的は、メタクラスとは何か、そしてそれがどのように機能するかを理解し、実際のプログラミング問題を解決するためのメタクラスを実装し、Python でのメタクラスの実用的なアプリケーションを探索することです。この実験で変更されるファイルは structure.py と validate.py です。
問題の理解
メタクラスの探索を始める前に、解決しようとしている問題を理解することが重要です。プログラミングでは、属性に特定の型を持つ構造体を作成する必要があることがよくあります。以前の作業では、型チェック付きの構造体を作成するシステムを開発しました。このシステムを使用すると、各属性が特定の型を持つクラスを定義でき、これらの属性に割り当てられる値はその型に従って検証されます。
このシステムを使用して Stock クラスを作成する例を次に示します。
from validate import String, PositiveInteger, PositiveFloat
from structure import Structure
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
このコードでは、まず validate モジュールからバリデータ型 (String、PositiveInteger、PositiveFloat) を、structure モジュールから Structure クラスをインポートしています。次に、Structure を継承する Stock クラスを定義しています。Stock クラスの内部では、特定のバリデータ型を持つ属性を定義しています。たとえば、name 属性は文字列でなければならず、shares は正の整数でなければならず、price は正の浮動小数点数でなければなりません。
しかし、このアプローチには問題があります。ファイルの先頭ですべてのバリデータ型をインポートする必要があります。実際のシナリオでバリデータ型を増やしていくと、これらのインポートが非常に長くなり、管理が困難になる可能性があります。このため、from validate import * を使用することになるかもしれませんが、これは一般的に悪い習慣とされています。なぜなら、名前の衝突を引き起こし、コードの可読性を低下させる可能性があるからです。
出発点を理解するために、Structure クラスを見てみましょう。エディタで structure.py ファイルを開き、その内容を調べる必要があります。これにより、メタクラス機能を追加する前に、基本的な構造体の処理がどのように実装されているかを確認できます。
code structure.py
ファイルを開くと、Structure クラスの基本的な実装が表示されます。このクラスは属性の初期化を処理する責任がありますが、まだメタクラス機能はありません。
次に、バリデータクラスを調べてみましょう。これらのクラスは validate.py ファイルで定義されています。これらのクラスはすでにディスクリプタ機能を持っているため、属性のアクセスと設定方法を制御できます。しかし、先に説明したインポートの問題を解決するために、これらを強化する必要があります。
code validate.py
これらのバリデータクラスを見ることで、検証プロセスがどのように機能するか、そしてコードを改善するためにどのような変更が必要かをよりよく理解できるでしょう。
バリデータ型の収集
Python では、バリデータはデータが特定の基準を満たしていることを保証するのに役立つクラスです。この実験の最初のタスクは、基本の Validator クラスを変更して、そのすべてのサブクラスを収集できるようにすることです。なぜこれが必要なのでしょうか?すべてのバリデータサブクラスを収集することで、すべてのバリデータ型を含む名前空間を作成できます。後で、この名前空間を Structure クラスに注入します。これにより、さまざまなバリデータを管理および使用しやすくなります。
では、コードの作業を始めましょう。validate.py ファイルを開きます。ターミナルで次のコマンドを使用して開くことができます。
code validate.py
ファイルが開いたら、Validator クラスにクラスレベルの辞書と __init_subclass__() メソッドを追加する必要があります。クラスレベルの辞書はすべてのバリデータサブクラスを格納するために使用され、__init_subclass__() メソッドは Python の特殊メソッドで、現在のクラスのサブクラスが定義されるたびに呼び出されます。
次のコードを Validator クラスの定義の直後に追加します。
## Add this to the Validator class in validate.py
validators = {} ## Dictionary to collect all validator subclasses
@classmethod
def __init_subclass__(cls):
"""Register each validator subclass in the validators dictionary"""
Validator.validators[cls.__name__] = cls
コードを追加した後、変更後の Validator クラスは次のようになります。
class Validator:
validators = {} ## Dictionary to collect all validator subclasses
@classmethod
def __init_subclass__(cls):
"""Register each validator subclass in the validators dictionary"""
Validator.validators[cls.__name__] = cls
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
self.validate(value)
instance.__dict__[self.name] = value
def validate(self, value):
pass
これで、String や PositiveInteger のような新しいバリデータ型が定義されるたびに、Python は自動的に __init_subclass__() メソッドを呼び出します。このメソッドは、新しいバリデータサブクラスをクラス名をキーとして validators 辞書に追加します。
コードが機能するかテストしましょう。validators 辞書の内容を確認するための簡単な Python スクリプトを作成します。ターミナルで次のコマンドを実行できます。
python3 -c "from validate import Validator; print(Validator.validators)"
すべてが正しく機能する場合、次のような出力が表示され、すべてのバリデータ型とそれに対応するクラスが表示されます。
{'Typed': <class 'validate.Typed'>, 'Positive': <class 'validate.Positive'>, 'NonEmpty': <class 'validate.NonEmpty'>, 'String': <class 'validate.String'>, 'Integer': <class 'validate.Integer'>, 'Float': <class 'validate.Float'>, 'PositiveInteger': <class 'validate.PositiveInteger'>, 'PositiveFloat': <class 'validate.PositiveFloat'>, 'NonEmptyString': <class 'validate.NonEmptyString'>}
これで、すべてのバリデータ型を含む辞書ができました。次のステップでこれを使用してメタクラスを作成します。
StructureMeta メタクラスの作成
では、次に行うことについて話しましょう。すべてのバリデータ型を収集する方法を見つけました。次のステップはメタクラスを作成することです。では、メタクラスとは具体的に何でしょうか?Python では、メタクラスは特殊な種類のクラスです。そのインスタンスはクラス自体です。つまり、メタクラスはクラスの作成方法を制御することができます。クラス属性が定義される名前空間を管理することができます。
私たちの状況では、Structure サブクラスを定義するときにバリデータ型を使用できるようにするメタクラスを作成したいと思います。毎回これらのバリデータ型を明示的にインポートする必要はありません。
まず、structure.py ファイルを再度開きましょう。次のコマンドを使用して開くことができます。
code structure.py
ファイルが開いたら、Structure クラス定義の前にいくつかのコードを追加する必要があります。このコードは私たちのメタクラスを定義します。
from validate import Validator
from collections import ChainMap
class StructureMeta(type):
@classmethod
def __prepare__(meta, clsname, bases):
"""Prepare the namespace for the class being defined"""
return ChainMap({}, Validator.validators)
@staticmethod
def __new__(meta, name, bases, methods):
"""Create the new class using only the local namespace"""
methods = methods.maps[0] ## Extract the local namespace
return super().__new__(meta, name, bases, methods)
メタクラスを定義したので、Structure クラスを変更してこれを使用する必要があります。これにより、Structure を継承するすべてのクラスがメタクラスの機能を利用できるようになります。
class Structure(metaclass=StructureMeta):
_fields = []
def __init__(self, *args, **kwargs):
if len(args) > len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
## Set all of the positional arguments
for name, val in zip(self._fields, args):
setattr(self, name, val)
## Set the remaining keyword arguments
for name, val in kwargs.items():
if name not in self._fields:
raise TypeError(f'Invalid argument: {name}')
setattr(self, name, val)
def __repr__(self):
values = [getattr(self, name) for name in self._fields]
args_str = ','.join(repr(val) for val in values)
return f'{type(self).__name__}({args_str})'
このコードが何をするかを分解してみましょう。
__prepare__()メソッドは Python の特殊メソッドです。クラスが作成される前に呼び出されます。その役割は、クラス属性が定義される名前空間を準備することです。ここではChainMapを使用しています。ChainMapはレイヤード辞書を作成する便利なツールです。私たちの場合、バリデータ型を含め、クラス名前空間でアクセス可能にします。__new__()メソッドは新しいクラスを作成する責任があります。ローカル名前空間のみを抽出します。これはChainMapの最初の辞書です。バリデータ辞書は破棄します。なぜなら、すでに名前空間でバリデータ型を使用できるようにしているからです。
この設定により、Structure を継承するすべてのクラスは、バリデータ型を明示的にインポートする必要なく、すべてのバリデータ型にアクセスできます。
では、実装をテストしましょう。強化された Structure 基底クラスを使用して Stock クラスを作成します。
cat > stock.py << EOF
from structure import Structure
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
EOF
メタクラスが正しく機能している場合、バリデータ型をインポートせずに Stock クラスを定義できるはずです。これは、メタクラスがすでに名前空間でバリデータ型を使用できるようにしているからです。
実装のテスト
メタクラスを実装し、Structure クラスを変更したので、実装のテストを行う時が来ました。テストは、すべてが正しく動作していることを確認するのに役立つため、非常に重要です。テストを実行することで、潜在的な問題を早期に発見し、コードが期待通りに動作することを確認できます。
まず、Stock クラスが期待通りに動作するかどうかを確認するために、単体テストを実行しましょう。単体テストは、コードの個々の部分をチェックする小さな独立したテストです。この場合、Stock クラスが正しく機能することを確認したいと思います。単体テストを実行するには、ターミナルで次のコマンドを使用します。
python3 teststock.py
すべてが正しく動作している場合、すべてのテストがエラーなくパスするはずです。テストが正常に実行されると、出力は次のようになります。
........
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
ドットはパスした各テストを表し、最後の OK はすべてのテストが成功したことを示します。
では、実際のデータとテーブル整形機能を使用して Stock クラスをテストしましょう。これにより、Stock クラスがデータとどのように相互作用するか、およびテーブル整形がどのように機能するかを、より現実的なシナリオで確認できます。ターミナルで次のコマンドを使用します。
python3 -c "
from stock import Stock
from reader import read_csv_as_instances
from tableformat import create_formatter, print_table
## Read portfolio data into Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print('Portfolio:')
print(portfolio)
## Format and print the portfolio data
print('\nFormatted table:')
formatter = create_formatter('text')
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"
このコードでは、まず必要なクラスと関数をインポートします。次に、CSV ファイルからのデータを Stock インスタンスに読み込みます。その後、ポートフォリオデータを印刷し、テーブルに整形して整形されたテーブルを印刷します。
次のような出力が表示されるはずです。
Portfolio:
[Stock('AA',100,32.2), Stock('IBM',50,91.1), Stock('CAT',150,83.44), Stock('MSFT',200,51.23), Stock('GE',95,40.37), Stock('MSFT',50,65.1), Stock('IBM',100,70.44)]
Formatted table:
name shares price
---------- ---------- ----------
AA 100 32.2
IBM 50 91.1
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.1
IBM 100 70.44
私たちが達成したことを振り返ってみましょう。
- すべてのバリデータ型を自動的に収集するメカニズムを作成しました。これは、すべてのバリデータを手動で追跡する必要がないことを意味し、時間を節約し、エラーの可能性を減らします。
- これらの型を
Structureサブクラスの名前空間に注入するメタクラスを実装しました。これにより、サブクラスはこれらのバリデータを明示的にインポートすることなく使用できます。 - バリデータ型の明示的なインポートの必要性を排除しました。これにより、コードがよりクリーンで読みやすくなります。
- これらすべての処理は裏で行われるため、新しい構造を定義するコードがクリーンでシンプルになります。
最終的な stock.py ファイルは、メタクラスを使用しない場合と比較して非常にクリーンです。
from structure import Structure
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
バリデータ型を直接インポートする必要がないため、コードはより簡潔で保守しやすくなります。これは、メタクラスがコードの品質を向上させることができる素晴らしい例です。
まとめ
この実験では、Python のメタクラスの力をどのように活用するかを学びました。まず、バリデータ型のインポート管理に関するチャレンジを理解しました。次に、Validator クラスを変更してそのサブクラスを自動的に収集し、StructureMeta メタクラスを作成してバリデータ型をクラス名前空間に注入しました。最後に、Stock クラスを使用して実装をテストし、明示的なインポートの必要性を排除しました。
メタクラスは Python の高度な機能で、クラスの作成プロセスをカスタマイズすることができます。これらは控えめに使用すべきですが、この実験で示されているように、特定の問題に対してエレガントな解決策を提供します。メタクラスを使用することで、検証済み属性を持つ構造を定義するコードを簡素化し、明示的なバリデータ型のインポートの必要性を排除し、より保守可能でエレガントな API を作成しました。このメタクラスに基づく名前空間注入パターンは、ユーザー向けの API を簡素化するために他のシナリオにも適用できます。