0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonメタプログラミング:思いのままに操る

Posted at

Group44.png

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 PythonPython Cookbook(この記事の一部の内容はこれらから引用されています)などの本を参照するか、公式ドキュメントのいくつかの章、例えば前述のディスクリプタのHow - Toや、データモデルのセクションなどを読むことができます。または直接Pythonのソースコード、Pythonで書かれたソースコードやCPythonのソースコードを調べることもできます。

これらの技術を完全に理解した後でのみ使用し、あちこちで使おうとしないでください。

Leapcell: 最適なサーバレスWebホスティングプラットフォーム

barndpic.png

最後に、Pythonサービスのデプロイに非常に適したプラットフォームLeapcellをおすすめします。

1. 多言語対応

  • JavaScript、Python、Go、またはRustで開発できます。

2. 無制限のプロジェクトを無料でデプロイ

  • 使用量に応じて課金 - リクエストがなければ料金は発生しません。

3. 圧倒的なコスト効率

  • 使い放題でアイドル料金はかかりません。
  • 例: 25ドルで平均応答時間60msで694万回のリクエストをサポートできます。

4. ストリームライン化された開発者体験

  • 直感的なUIで簡単にセットアップできます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • アクション可能な洞察のためのリアルタイムメトリックとログ。

5. 簡単なスケーラビリティと高性能

  • 高い同時実行数を簡単に処理できる自動スケーリング。
  • オペレーションのオーバーヘッドはゼロ - 構築に集中できます。

Frame3-withpadding2x.png

ドキュメントで詳細を確認!

LeapcellのTwitter: https://x.com/LeapcellHQ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?