高度なPython:メタクラス

高度なPython:メタクラスの魅力と活用法

Pythonのクラスオブジェクトとそれが作成されるについての簡単な紹介

As Atlas is to the heavens, metaclasses are to classes. Photo by Alexander Nikitenko on Unsplash

この記事はAdvanced Pythonシリーズ(前の記事ではPythonの関数について)の続きです。今回は、メタクラスの簡単な紹介を行います。このトピックはかなり高度ですが、エンジニアがカスタムメタクラスを実装する必要がまれです。ただし、それはすべての知識豊かなPython開発者が知っているべき最も重要な構造やメカニズムの一つであり、OOPパラダイムを可能にするためです。

メタクラスのアイデアとクラスオブジェクトの作成方法を理解した後、カプセル化、抽象化、継承、および多様性といったOOPの原則を学び続けることができます。そして、ソフトウェアエンジニアリングのいくつかの原則によって導かれる数多くのデザインパターンを適用する方法を理解することができます(例:SOLID)。

それでは、このささいな例から始めましょう:

class Person:    passclass Child(Person):    passchild = Child()

オブジェクト指向プログラミングについて学んだ時、おそらくクラスとオブジェクトが何であるかを説明する一般的な考え方に出会ったことでしょう。それは次のように言います:

「クラスはクッキーの型のようなものであり、オブジェクトはそれで作られるクッキーです」。

これは非常に直感的な説明であり、アイデアをかなり明確に伝えています。とは言っても、私たちの例は機能がほとんどなかったり、全くなかったりする2つのテンプレートを定義していますが、それでも動作します。 __init__メソッドを定義し、いくつかのオブジェクトの属性を設定し、より使いやすくすることができます。

ただし、興味深い点は、Pythonではクラスが「テンプレート」であり、それからオブジェクトを作成するために使用されるが、それ自体もオブジェクトであるということです。PythonでOOPを学んでいる人ならば、この文をさっと読み流すだけで、深く考えないかもしれません。Pythonではすべてがオブジェクトです、だから何だろうってことでしょう。しかし一度これを考え始めると、多くの質問が浮かび上がり、興味深いPythonの細かい点が明らかになります。

これらの例を基にして、自分自身に問いかけるべきいくつかの質問が以下にあります:

  • クラスはオブジェクトなので、いつ作成されるのですか?
  • クラスオブジェクトは誰が作成するのですか?
  • クラスはオブジェクトなのに、オブジェクトをインスタンス化する際に呼び出すことができるのはなぜですか?

クラスオブジェクトの作成

Pythonは解釈型言語として広く知られています。つまり、行ごとに解釈プログラムやプロセスが進み、それを機械語に変換しようとします。これは、プログラミングコードを実行する前にプログラミングコードを機械語に変換するコンパイル型のプログラミング言語(例:C)と対照的です。これは非常に単純化された見方です。もっと正確に言えば、Pythonはコンパイル型と解釈型の両方ですが、これはまた別の機会の話題です。私たちの例に重要なのは、インタプリタがクラス定義を読み終えると、クラスオブジェクトが作成されることです。それ以降に、それを元にオブジェクトをインスタンス化することができます。もちろん、クラスオブジェクトは暗黙的にインスタンス化されますが、明示的に行う必要があります。

では、インタプリタがクラスコードブロックの読み込みを終えた後にどのような「プロセス」がトリガーされるのでしょうか?直接詳細に入ることもできますが、一つのグラフが千の言葉を語ると言われます:

オブジェクト、クラス、メタクラスの関連性について。イメージ:イリヤ・ラザレヴィッチさん提供

もし気づいていなければ、Pythonには目的に応じて使用できるtype関数があります。オブジェクトを引数としてtypeを呼び出すと、そのオブジェクトの型が返されます。非常に巧妙ですね!以下をご覧ください:

class Person:    passclass Child(Person):    passchild = Child()type(child)# Childtype(Child)# type

この例でのtypeの呼び出しは意味があります。childChild型です。私たちはクラスオブジェクトを使用してそれを作成しました。ですので、ある意味ではtype(child)はその「作成者」の名前を返してくれると考えることができます。そして、Childクラスがその作成者であり、新しいインスタンスを作成するためにそれが呼び出されたからです。しかし、クラスオブジェクトtype(Child)の「作成者」を取得しようとすると、typeが返されます。要するに、オブジェクトはクラスのインスタンスであり、クラスは型のインスタンスです。今ごろ、「クラスが関数のインスタンスであるとはどういうことか?」と疑問に思われるかもしれませんが、その答えはtypeが関数でありクラスの両方であるためです。これは昔ながらの後方互換性のために意図的にそのまま残されています。

あなたの頭を混乱させるであろうものは、クラスオブジェクトを作成するために使用されるクラスの名前です。それはメタクラスと呼ばれます。ここで重要なのは、オブジェクト指向の観点からの継承と、このパラダイムを実践するための言語の仕組みの違いを理解することです。メタクラスがこの仕組みを提供します。さらに混乱が生じる可能性があるのは、メタクラスも通常のクラスと同じように親クラスを継承できることです。しかし、これはすぐに「インセプション的」なプログラミングになる可能性があるため、あまり深入りしないでおきましょう。

私たちは日常的にこれらのメタクラスに取り組まなければならないのでしょうか?実際には、ほとんどの場合、デフォルトの動作で十分です。稀な場合には、メタクラスを定義し使用する必要があるかもしれません。

それでは、旅を続けましょう。今度は新しい例をご紹介します:

class Parent:    def __init__(self, name, age):        self.name = name        self.age = ageparent = Parent('John', 35)

Pythonのオブジェクト指向プログラミングにおける最初の一歩となるであろうものです。私たちは__init__をコンストラクタとして教えられます。オブジェクト属性の値を設定し、準備完了です。しかし、この__init__ダンダーメソッドは正確に言うと「初期化」ステップです。オブジェクトを初期化するためにそれを呼び出すのに、なぜオブジェクトのインスタンスが返されるのでしょう?returnはありませんよね?では、これはどういうことなのでしょうか?クラスのインスタンスを返すのは誰でしょうか?

Pythonの旅の最初に覚えておくべきなのは、暗黙的に呼び出される__new__という別のメソッドが存在することです。このメソッドは、__init__が初期化される前にインスタンスを作成します。以下に例を示します:

class Parent:    def __new__(cls, name, age):        print('newが呼ばれました')        return super().__new__(cls)    def __init__(self, name, age):        print('initが呼ばれました')        self.name = name        self.age = ageparent = Parent('John', 35)# newが呼ばれました# initが呼ばれました

すぐにわかると思いますが、__new__super().__new__(cls)を返しています。これは、新しいインスタンスです。super()Parentの親クラスを取得し、それは暗黙的にobjectクラスです。このクラスはPythonのすべてのクラスで継承されています。そしてそれ自体もオブジェクトです。Pythonの創造者による別の革新的な動きです!

isinstance(オブジェクト、オブジェクト)# True 

しかし、__new____init__は何を結びつけるのですか?’John’と35を引数としてParentを呼び出すと、オブジェクトのインスタンス化が行われることが分かります。もう一度見てみましょう。クラスオブジェクトを呼び出しているのです。

Pythonの呼び出し可能

Pythonは構造的に型付けされた言語であり、クラス内に特定のメソッドを定義することができます。これらのメソッドは、プロトコル(オブジェクトの使用方法)を記述し、そのため、クラスのすべてのインスタンスは期待される動作をします。他のプログラミング言語から来ている場合、プロトコルには特定のインターフェースのようなものがあります。ただし、ここではわざわざ特定のインターフェースを実装していることを明示的に示す必要はありません。プロトコルで説明されているメソッドを実装するだけで、すべてのオブジェクトがプロトコルの動作を持ちます。その中の1つがCallableです。ダンダーメソッド__call__を実装することで、オブジェクトを関数のように呼び出すことができます。次の例をご覧ください。

class Parent:    def __new__(cls、name、age):        print('new is called')        return super(). __new__(cls)     def __init__(self、name、age):        print('init is called')        self.name = name        self.age = age     def __call__(self):        print('Parent here!')parent = Parent('John'、35)parent()# Parent here!

クラス定義内で__call__を実装することで、クラスのインスタンスがCallableになります。しかし、Parent('John'、35)はどうなるのでしょうか?クラスオブジェクトで同じことを実現するにはどうすればよいのでしょうか?オブジェクトのタイプ定義(クラス)がそのオブジェクトがCallableであることを指定している場合、クラスオブジェクトタイプ(タイプつまりメタクラス)もクラスオブジェクトがCallableであることを指定する必要がありますね。その場で__new____init__の呼び出しが行われます。

この時点で、メタクラスの使用を始める時がきました。

Pythonのメタクラス

クラスオブジェクトの作成プロセスを変更する2つの方法が少なくともあります。1つはクラスデコレータを使用する方法で、もう1つは明示的にメタクラスを指定する方法です。メタクラスのアプローチについて説明します。メタクラスは通常のクラスのように見えますが、唯一の例外はtypeクラスを継承しなければならないということです。なぜなら、typeクラスには、コードが期待どおりに動作するために必要なすべての実装が含まれているからです。例えば:

class MyMeta(type):    def __call__(self、*args、**kwargs):        print(f'{self.__name__} is called'              f' with args={args}、kwargs={kwarg}')class Parent(metaclass = MyMeta):    def __new__(cls、name、age):        print('new is called')        return super(). __new__(cls)     def __init__(self、name、age):        print('init is called')        self.name = name        self.age = ageparent = Parent('John'、35)# Parent is called with args =('John'、35)、kwargs = {}type(parent)# NoneType

ここで、MyMetaは新しいクラスオブジェクトの生成の背後にある力であり、新しいクラスインスタンスの作成方法も指定しています。例の最後の2行をよく見てください。parentには何も入っていません! しかし、なぜですか? あなたが見るように、 MyMeta.__call__は単に情報を出力して何も返しません。 無意識に言えば、それはNoneを返すということです、つまりNoneTypeのオブジェクトです。

では、どうやって修正すればよいのでしょうか?

class MyMeta(type):    def __call__(cls、*args、**kwargs):        print(f'{cls.__name__} is called'              f'with args={args}、kwargs={kwargs}')        print('metaclass calls __new__')        obj = cls.__new__(cls、*args、**kwargs)         if isinstance(obj、cls):            print('metaclass calls __init__')            cls.__init__(obj、*args、**kwargs)        return objclass Parent(metaclass = MyMeta):    def __new__(cls、name、age):        print('new is called')        return super(). __new__(cls)     def __init__(self、name、age):        print('init is called')        self.name = name        self.age = ageparent = Parent('John'、35)# Parent is called with args =('John'、35)、kwargs = {}# metaclass calls __new__# new is called# metaclass calls __init__# init is calledtype(parent)# Parentstr(parent)# '<__main__.Parent object at 0x103d540a0>'

出力結果から、MyMeta.__call__呼び出し時に何が起こるかがわかります。提供された実装は、全体の動作を示すための例にすぎません。自分自身のメタクラスの一部をオーバーライドする場合は、より注意が必要です。カバーしなければならないいくつかのエッジケースがあります。たとえば、Parent.__new__Parentクラスのインスタンスでないオブジェクトを返すエッジケースがあります。その場合、Parent.__init__メソッドによって初期化されません。これはあなたが意識しておかなければならない予想される動作であり、同じクラスのインスタンスでないオブジェクトを初期化することは実際には意味をなしません。

結論

これにより、クラスを定義し、インスタンスを作成する際に何が起こるかについての簡単な概要が終わります。もちろん、クラスブロックの解釈中に何が起こるかをさらに詳しく見ることもできます。これもメタクラスで行われます。ほとんどの人にとって、特定のメタクラスを作成して使用する必要がないというのは幸運です。それでも、すべての動作を理解することは役に立ちます。Pythonのメタクラスの使用を必要とするかどうかわからない場合には、おそらく必要ないと言えるでしょう。

参考文献

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more