Effective Pythonの記述に則って、Pythonのディスクリプタについて書いてきました。
前回までのまとめ
一つの解答:メタクラスを使う
Effective Pythonでは、上の問題を解消するために、メタクラスを使うことを推奨しています。
公式ドキュメントにも、「これまで試されてきたアイデア」の中に自動プロパティ生成が含まれています。
メタクラスとは?
C#では、メタデータというと、プロパティ名、メソッド名等のクラス情報*1を表します。*2
メタという言葉は、プログラミングでは「プログラムに、文字列等で直接アクセスする」するような意味に捉えられます。
Pythonのメタクラスも、意味としては同じような感じです。
メタクラスは、指定したクラスの情報に、直接アクセスする機能を持ちます。
インスタンス生成後、コンストラクタが呼ばれる前に、メタクラスのアクセスが行われるため、特定の規則に沿ってクラスが作られているか等をチェックできます。
メタクラスとして、比較的有名なのはABCMeta
だと思います。
私のブログでも、一度取り上げています。
ABCMeta
の役割は、
@abstractmethod
のメソッドを継承先で実装していないと、エラーを出してくれます。
と記事内にある通り、継承先のクラス内容をチェックしてくれるものです。
この機能は、生成されているクラスのフィールドやプロパティにアクセスすることで、実現されています。
ABCMeta
は、基底クラス*3の@abstractmethod
付きのメソッドが、その継承先で宣言されていない場合にエラーを出します。
これは、完全解読できていないものの、@abstractmethod
付きのメソッド取得→サブクラスチェック、の順で処理して、実現しているようです。
メタクラスを作る
メタクラスを作るには、以下の二つが必要です。
type
を継承する__new__
メソッドを実装する
ソースは、以下のようになります。
- grade_meta.py
"""Gradeを使用するクラス用の、メタクラス定義""" from grade import Grade class GradeMeta(type): """Gradeプロパティの自動生成メタクラス""" def __new__(mcs, name, bases, namespace): """生成""" for key, value in namespace.items(): if isinstance(value, Grade): value.name = key value.internal_name = "_" + key result = type.__new__(mcs, name, bases, namespace) return result
__new__
の引数は、上のようにしていれば問題ないようで、それぞれ以下の意味を持ちます。
名前 | 説明 |
---|---|
mcs | メタクラス自身。mcs 以外の名称は、pylintが警告を出す |
name | メタクラスを使ったクラス名 |
bases | 基底クラスのタプル? |
namespace | メタクラスのクラス辞書 |
上の__new__
は、クラス変数の初期化が終わった後に呼び出されるため、ディスクリプタは生成済みの状態です。
元のクラス書き換え
- Gradeクラス
"""GradeプロパティのDescriptor""" class Grade(object): """各試験の結果""" def __init__(self): """初期化""" self.name = "" self.internal_name = "" def __get__(self, instance, instance_type): """getter descriptor""" if instance is None: return self return getattr(instance, self.internal_name, 0) def __set__(self, instance, value: int): """setter descriptor""" if not (0 <= value <= 100): raise ValueError("Grade must be between 0 and 100") setattr(instance, self.internal_name, value)
Gradeのname
とinternal_name
は、メタクラスにより設定されます。
よって、__init__
では初期値のみ設定します。
また、引数からname
が消えたため、インスタンス生成も引数が無くなりました。
使用クラス(メタクラスも使う)も見てみます。
- Examクラス
from grade import Grade from grade_meta import GradeMeta class Exam(metaclass=GradeMeta): """試験結果""" math_grade = Grade() writing_grade = Grade() science_grade = Grade() # 以下省略
Exam
クラスは、GradeMeta
をメタクラスとして指定しました。
よって、クラス変数にあるGrade
クラスのインスタンスには、メタクラスによって名前が設定されます。
公式ドキュメント
3. データモデル — Python 3.6.3 ドキュメント
おわりに
このブログでの、ディスクリプタの説明はいったん終了です。
Pythonのクラスの内部構造は、辞書がきわめて大きな役割を果たしています。
それが理解できると、この仕組みを利用して、ディスクリプタ等の便利な機能があることがよく分かります。
言語仕様に興味がある方は、ぜひいろいろ調べてみてください。