SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

Pythonのディスクリプタについて:4.メタクラスで登録を楽にする

Effective Pythonの記述に則って、Pythonディスクリプタについて書いてきました。

前回までのまとめ

一つの解答:メタクラスを使う

Effective Pythonでは、上の問題を解消するために、メタクラスを使うことを推奨しています。

公式ドキュメントにも、「これまで試されてきたアイデア」の中に自動プロパティ生成が含まれています。

メタクラスとは?

C#では、メタデータというと、プロパティ名、メソッド名等のクラス情報*1を表します。*2

メタという言葉は、プログラミングでは「プログラムに、文字列等で直接アクセスする」するような意味に捉えられます。

Pythonメタクラスも、意味としては同じような感じです。
メタクラスは、指定したクラスの情報に、直接アクセスする機能を持ちます。
インスタンス生成後、コンストラクタが呼ばれる前に、メタクラスのアクセスが行われるため、特定の規則に沿ってクラスが作られているか等をチェックできます。

メタクラスとして、比較的有名なのはABCMetaだと思います。
私のブログでも、一度取り上げています。

hiroronn.hatenablog.jp

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のnameinternal_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のクラスの内部構造は、辞書がきわめて大きな役割を果たしています。
それが理解できると、この仕組みを利用して、ディスクリプタ等の便利な機能があることがよく分かります。

言語仕様に興味がある方は、ぜひいろいろ調べてみてください。

過去記事

hiroronn.hatenablog.jp

hiroronn.hatenablog.jp

hiroronn.hatenablog.jp


*1:クラスに限らないが、クラスが最も分かりやすい

*2:これらに直接アクセスする方法として、リフレクションが存在する

*3:ABCMetaを、メタクラスとして指定したクラス