Leapcell: 最適なサーバレスWebホスティングプラットフォーム
Pythonにおけるメタプログラミングの探究
多くの人は「メタプログラミング」という概念に疎く、またそれに対する非常に正確な定義も存在しません。この記事はPython内のメタプログラミングを中心に展開します。ただ、実際にはここで議論される内容が「メタプログラミング」の厳密な定義に完全に沿っているとは限りません。この記事のテーマを表すより適切な用語が見つからなかったので、この用語を借用しただけです。
サブタイトルは「コントロールしたいすべてをコントロールする」です。基本的に、この記事は1つのことに焦点を当てています。Pythonが提供する機能を活用して、コードをできるだけエレガントかつ簡潔にすることです。具体的には、プログラミング技術を通じて、より高い抽象度で抽象化の特性を変更することです。
まず第一に、Pythonにおいてすべてがオブジェクトであるということはよく知られた陳腐な話です。さらに、Pythonは特別なメソッドやメタクラスなど、多くの「メタプログラミング」メカニズムを提供しています。オブジェクトに属性やメソッドを動的に追加する操作は、Pythonにおいて全く「メタプログラミング」とは見なされません。しかし、一部の静的言語ではこれを実現するには一定の技術が必要です。Pythonプログラマーを困惑させやすいいくつかの側面について議論しましょう。
まず、オブジェクトを異なるレベルに分類してみましょう。一般的に、オブジェクトにはその型があり、Pythonは長い間型をオブジェクトとして実装しています。そのため、インスタンスオブジェクトとクラスオブジェクトがあり、これらは2つのレベルです。基本的な理解を持つ読者は、メタクラスの存在に気付いているでしょう。簡単に言えば、メタクラスは「クラス」の「クラス」であり、つまりクラスよりも高いレベルにあります。これにより、もう1つのレベルが追加されます。もっとあるでしょうか?
インポート時 vs 実行時
異なる視点から見て、前の3つのレベルと同じ基準を適用する必要がない場合、インポート時(ImportTime)と実行時(RunTime)の2つの概念を区別できます。それらの境界は明確ではありません。名前が示すように、これらは2つの時点、つまりインポートの時点と実行の時点を指します。
モジュールがインポートされるときに何が起こりますか? グローバルスコープ内の文(定義でない文)が実行されます。関数定義はどうでしょう? 関数オブジェクトが作成されますが、その中のコードは実行されません。クラス定義の場合、クラスオブジェクトが作成され、クラス定義スコープ内のコードが実行され、クラスメソッド内のコードは当然実行されません。
実行時にはどうでしょう? 関数やメソッド内のコードが実行されます。もちろん、まずそれらを呼び出す必要があります。
メタクラス
したがって、メタクラスとクラスはインポート時に属すると言えます。モジュールがインポートされた後、それらが作成されます。インスタンスオブジェクトは実行時に属します。単にモジュールをインポートするだけではインスタンスオブジェクトは作成されません。ただし、あまり教義的にならないようにしましょう。なぜなら、モジュールスコープ内でクラスをインスタンス化すると、インスタンスオブジェクトも作成されるからです。ただ、通常は関数内にインスタンス化を書くので、このような分類になっています。
作成されるインスタンスオブジェクトの特性を制御したい場合は、どうすればいいでしょう? かなり簡単です。クラス定義内の__init__
メソッドをオーバーライドします。では、クラスのいくつかのプロパティを制御したい場合はどうでしょう? そのようなニーズはありますか? 間違いなくあります!
古典的なシングルトンパターンに関して、それを実装する方法は複数あることは誰もが知っています。要件は、クラスが1つのインスタンスのみを持つことです。
最も簡単な実装は以下の通りです。
class _Spam:
def __init__(self):
print("Spam!!!")
_spam_singleton = None
def Spam():
global _spam_singleton
if _spam_singleton is not None:
return _spam_singleton
else:
_spam_singleton = _Spam()
return _spam_singleton
このようなファクトリーのようなパターンはあまりエレガントではありません。もう一度要件を見直しましょう。我々はあるクラスが1つのインスタンスのみを持つことを望んでいます。クラス内で定義するメソッドはインスタンスオブジェクトの振る舞いです。したがって、クラスの振る舞いを変えたい場合は、もっと高いレベルの何かが必要です。ここでメタクラスが登場します。前述の通り、メタクラスはクラスのクラスです。つまり、メタクラスの__init__
メソッドはクラスの初期化メソッドです。__call__
メソッドもあることを知っています。これにより、インスタンスを関数のように呼び出すことができます。そして、メタクラスのこのメソッドは、クラスがインスタンス化されるときに呼び出されるメソッドです。
コードは以下のように書けます。
class Singleton(type):
def __init__(self, *args, **kwargs):
self._instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self._instance is None:
self._instance = super().__call__(*args, **kwargs)
return self._instance
else:
return self._instance
class Spam(metaclass = Singleton):
def __init__(self):
print("Spam!!!")
一般的なクラス定義と比較して、主に2つの違いがあります。1つは、Singleton
の基底クラスがtype
であること、もう1つはSpam
の定義にmetaclass = Singleton
があることです。type
とは何でしょう? それはobject
のサブクラスであり、object
はそのインスタンスです。つまり、type
はすべてのクラスのクラス、最も基本的なメタクラスです。すべてのクラスが作成されるときに必要ないくつかの操作を定めています。したがって、独自のメタクラスはtype
をサブクラス化する必要があります。同時に、type
はオブジェクトでもあるので、object
のサブクラスです。少し理解しにくいですが、大まかなイメージを掴んでおけばいいでしょう。
デコレータ
次にデコレータについて話しましょう。多くの人は、デコレータがPythonで最も理解しにくい概念の1つだと考えています。実際、それは単なる構文シュガーに過ぎません。関数もオブジェクトであることを理解すれば、簡単に独自のデコレータを書くことができます。
from functools import wraps
def print_result(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(result)
return result
return wrapper
@print_result
def add(x, y):
return x + y
# 以下と等価:
# add = print_result(add)
add(1, 3)
ここでは、@wraps
というデコレータも使用しています。これは、返される内部関数wrapper
が元の関数と同じ関数シグネチャを持つようにするために使用されます。基本的に、デコレータを書くときはこれを追加するべきです。
コメントに書いた通り、@デコレータ
の形式はfunc = デコレータ(func)
と等価です。この点を理解すると、より多くの種類のデコレータを書くことができます。たとえば、クラスデコレータや、デコレータをクラスとして書くことができます。
def attr_upper(cls):
for attrname, value in cls.__dict__.items():
if isinstance(value, str):
if not value.startswith('__'):
setattr(cls, attrname, bytes.decode(str.encode(value).upper()))
return cls
@attr_upper
class Person:
sex ='man'
print(Person.sex) # MAN
通常のデコレータとクラスデコレータの実装の違いに注意してください。
データ抽象 - ディスクリプタ
いくつかのクラスに特定の共通の特性を持たせ、またはクラス定義内でそれらを制御できるようにしたい場合は、独自のメタクラスをカスタマイズし、それをこれらのクラスのメタクラスにすることができます。いくつかの関数に特定の共通の機能を持たせ、コードの重複を避けたい場合は、デコレータを定義することができます。では、インスタンスの属性にいくつかの共通の特性を持たせたい場合はどうでしょう? 一部の人はproperty
を使用できると言うかもしれません。実際、そうすることができます。ただ、このロジックは各クラス定義に書かなければなりません。これらのクラスのインスタンスの一部の属性に同じ特性を持たせたい場合は、独自のディスクリプタクラスをカスタマイズすることができます。
ディスクリプタに関しては、この記事https://docs.python.org/3/howto/descriptor.htmlが非常によく説明しています。同時に、ディスクリプタが関数の背後でどのように隠されており、関数とメソッドの統一と違いを実現しているのかも詳しく説明しています。以下にいくつかの例を示します。
class TypedField:
def __init__(self, _type):
self._type = _type
def __get__(self, instance, cls):
if instance is None:
return self
else:
return getattr(instance, self.name)
def __set_name__(self, cls, name):
self.name = name
def __set__(self, instance, value):
if not isinstance(value, self._type):
raise TypeError('Expected' + str(self._type))
instance.__dict__[self.name] = value
class Person:
age = TypedField(int)
name = TypedField(str)
def __init__(self, age, name):
self.age = age
self.name = name
jack = Person(15, 'Jack')
jack.age = '15' # エラーが発生します
ここではいくつかの役割があります。TypedField
はディスクリプタクラスであり、Person
の属性はディスクリプタクラスのインスタンスです。ディスクリプタはPerson
の属性として、つまりクラス属性として存在するように見えますが、実際にはPerson
のインスタンスが同じ名前の属性にアクセスすると、ディスクリプタが機能します。Python 3.5以前のバージョンでは__set_name__
という特別なメソッドが存在しないことに注意してください。これは、ディスクリプタがクラス定義でどの名前が与えられたのかを知りたい場合は、インスタンス化する際に明示的にディスクリプタに渡す必要があり、つまりもう1つのパラメータが必要ということです。しかし、Python 3.6ではこの問題が解決されました。ディスクリプタクラス定義内で__set_name__
メソッドをオーバーライドするだけです。また、__get__
の書き方にも注意してください。基本的に、instance
の判定は必要で、そうしないとエラーが発生します。理由はあまり難しくないので、ここでは詳細を説明しません。
サブクラスの作成を制御する - メタクラスの代替方法
Python 3.6では、__init_subclass__
という特別なメソッドを実装することで、サブクラスの作成をカスタマイズすることができます。このように、場合によってはやや煩雑なメタクラスを使わなくても済むようになります。
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
class Plugin1(PluginBase):
pass
class Plugin2(PluginBase):
pass
まとめ
メタクラスなどのメタプログラミング技術は、多くの人にとってやや難解で理解しにくく、多くの場合、それらを使う必要はありません。ただ、ほとんどのフレームワークの実装ではこれらの技術が利用されており、ユーザーが書くコードが簡潔で理解しやすくなっています。これらの技術をもっと深く理解したい場合は、Fluent PythonやPython Cookbook(この記事の一部の内容はこれらから引用されています)などの本を参照するか、公式ドキュメントのいくつかの章、例えば前述のディスクリプタのHow - Toや、データモデルのセクションなどを読むことができます。または直接Pythonのソースコード、Pythonで書かれたソースコードやCPythonのソースコードを調べることもできます。
これらの技術を完全に理解した後でのみ使用し、あちこちで使おうとしないでください。
Leapcell: 最適なサーバレスWebホスティングプラットフォーム
最後に、Pythonサービスのデプロイに非常に適したプラットフォームLeapcellをおすすめします。
1. 多言語対応
- JavaScript、Python、Go、またはRustで開発できます。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に応じて課金 - リクエストがなければ料金は発生しません。
3. 圧倒的なコスト効率
- 使い放題でアイドル料金はかかりません。
- 例: 25ドルで平均応答時間60msで694万回のリクエストをサポートできます。
4. ストリームライン化された開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- アクション可能な洞察のためのリアルタイムメトリックとログ。
5. 簡単なスケーラビリティと高性能
- 高い同時実行数を簡単に処理できる自動スケーリング。
- オペレーションのオーバーヘッドはゼロ - 構築に集中できます。
LeapcellのTwitter: https://x.com/LeapcellHQ