高度なPython:メタクラス
高度なPython:メタクラスの魅力と活用法
Pythonのクラスオブジェクトとそれが作成されるについての簡単な紹介
この記事は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
の呼び出しは意味があります。child
はChild
型です。私たちはクラスオブジェクトを使用してそれを作成しました。ですので、ある意味では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!
Was this article helpful?
93 out of 132 found this helpful
Related articles