Python でのメタプログラミング

タスクを単純化する

Comments

メタデータがデータに関するデータであるのと同様に、メタプログラミングとはプログラムを操作するプログラムを作成することを意味します。メタプログラミングは他のプログラムを生成するプログラムであるというのが共通の認識となっていますが、メタプログラミングの実例はそれだけに限られません。自身の読み取り、分析、変換、あるいは変更を行うよう意図されているプログラムはすべて、メタプログラミングの例です。具体的な例としては、以下が挙げられます。

  • ドメイン固有言語 (DSL)
  • パーサー
  • インタープリター
  • コンパイラー:
  • 定理証明器
  • 項書き換え器

このチュートリアルでは、Python でのメタプログラミングについて探ります。このチュートリアルで説明する概念の理解を促すために、まず、Python の機能をレビューして皆さんの Python に関する知識を呼び覚まします。Python での type 関数は、オブジェクトのクラスを返すだけのものではありませんが、その重要性についても説明します。続いて、Python でメタプログラミングを行う方法と、メタプログラミングによって特定のタスクが単純化される仕組みを説明します。

簡単な振り返り

Python でのプログラミング経験があればご存知のとおり、Python ではすべてがオブジェクトであり、オブジェクトを作成するのはクラスです。けれども、すべてがオブジェクトであるとしたら (そしてクラスもオブジェクトであるならば)、クラスは何によって作成されるのでしょうか?これがまさに、私が答えようとしている質問です。

上述の発言が果たして正しいのかどうかという、基本的なところから確認しましょう。

>>> class SomeClass:
...     pass
>>> some_object = SomeClass()
>>> type(some_obj)
<__main__.SomeClass instance at 0x7f8de4432f80>

type 関数がオブジェクトに対して呼び出されて、そのオブジェクトのクラスを返します。

>>> import inspect
>>>inspect.isclass(SomeClass)
True
>>>inspect.isclass(some_object)
False
>>>inspect.isclass(type(some_object))
True

inspect.isclass はクラスが渡されると True を返し、渡されなければ False を返します。some_object はクラスではないため (SomeClass クラスのインスタンスです)、False が返されます。また、type(some_object)some_object のクラスを返すため、inspect.isclass(type(some_object))True を返します。

>>> type(SomeClass)
<type 'classobj'>>>>
inspect.isclass(type(SomeClass))
True

classobj は、Python 3 内のすべてのクラスがデフォルトで継承するクラスです。これで、すべての説明がつきました。けれども、classobj についてはどうでしょうか?ひとひねり加えてみましょう。

>>> type(type(SomeClass))
<type 'type'>
>>>inspect.isclass(type(type(SomeClass)))
True
>>>type(type(type(SomeClass)))
<type 'type'>
>>>isclass(type(type(type(SomeClass))))
True

面白くなってきましたか?冒頭の発言 (すべてはオブジェクトである) は、完全に真実であるわけではないことがわかります。それよりも、このように表現したほうが適切でしょう。

Python ではすべてのものがオブジェクトであり、type を除き、すべてのオブジェクトはクラスのインスタンスであるか、メタクラスのインスタンスであるかのどちらかです。

この発言を検証します。

>>> some_obj = SomeClass()
>>> isinstance(some_obj,SomeClass)
True
>>> isinstance(SomeClass, type)
True

これで、インスタンスはインスタンス化のクラスであり、クラスはメタクラスのインスタンスであることが明かになりました。

type の意外な側面

type 自体はクラスです。type はその固有の型であり、メタクラスです。クラスがインスタンス化されて、そのインスタンスの動作が定義されるのと同じように、メタクラスがインスタンス化されることによって、そのインスタンスの動作が定義されます。

type は、Python が使用する組み込みメタクラスです。Python で (SomeClass の動作など) クラスの動作を変更するには、type メタクラスを継承してカスタム・メタクラスを定義します。つまり、メタクラスが、Python でメタプログラミングを行う手段となるのです。

クラスが定義されている場合

まず、すでに身に付けている知識を復習しましょう。Python の基本的なビルディング・ブロックには、以下があります。

  • ステートメント
  • 関数
  • クラス

ステートメントは、プログラム内で実際の処理を行うためのものです。ステートメントはグローバル・スコープ (モジュール・レベル) で実行することも、ローカル・スコープ (関数内に限定) で実行することもできます。関数は、いわば基本のコード単位であり、特定のタスクを処理して完了するように順序付けられた 1 つ以上のステートメントからなります。関数はモジュール・レベルで定義することも、クラスのメソッドとして定義することもできます。クラスは、オブジェクト指向のプログラミングを機能させるための手段です。オブジェクトをインスタンス化する方法とオブジェクトの特性 (属性とメソッド) は、クラスによって定義されます。

クラスの名前空間は辞書として階層化されます。以下に一例を示します。

>>> class SomeClass:
...     class_var = 1
...     def __init__(self):
...         self.some_var = 'Some value'

>>> SomeClass.__dict__
{'__doc__': None,
 '__init__': <function __main__.__init__>,
 '__module__': '__main__',
 'class_var': 1}

>>> s = SomeClass()

>>> s.__dict__
{'some_var': 'Some value'}

キーワード class が出現するたびに、以下の処理が行われます。

  • クラスの本文 (ステートメントと関数) が分離されます。
  • クラスの名前空間の辞書が作成されます (ただし、データはまだ取り込まれません)。
  • クラスの本文が実行されて、属性、定義済みのメソッド、そのクラスに関する有用な追加情報が名前空間の辞書に取り込まれます。
  • 基底クラス内、または作成されるクラスのメタクラス・フック (追って説明します) 内で、メタクラスが識別されます。
  • 識別されたメタクラスが呼び出され、呼び出しに指定されたクラスの名前、base 要素、および属性を使用してインスタンス化されます。

Python 内では type がデフォルトのメタクラスであるため、Python では type を使用してクラスを作成できます。

type の別の側面

1 つの属性を指定して type を呼び出すと、type によって既存のクラスの type 情報が生成されます。一方、3 つの属性を指定して呼び出した場合は、新しいクラス・オブジェクトが作成されます。type を呼び出すときに指定する属性は、クラスの名前、基底クラスのリスト、クラスに名前空間を与える辞書 (すべてのフィールドとメソッド) です。

以下のコードを見てください。
>>> class SomeClass: pass

上記は以下のコードと同等です。
>>> SomeClass = type('SomeClass', (), {})

以下はもう 1 つの例です。

class ParentClass:
    pass

class SomeClass(ParentClass):
    some_var = 5
    def some_function(self):
        print("Hello!")

上記のコードは実質的に以下のコードと同等です。

def some_function(self):
    print("Hello")

ParentClass = type('ParentClass', (), {})
SomeClass = type('SomeClass',
                 [ParentClass],
                 {'some_function': some_function,
                  'some_var':5})

このように、動作を注入できないクラスであっても、type ではなくカスタム・メタクラスを使用すれば、動作を注入することができます。動作を変更するメタクラスの実装について詳しく探る前に、Python での一般的なメタプログラミング方法について、さらにいくつかの例を見ていきましょう。

デコレーター: Python での一般的なメタプログラミング例

デコレーターは、関数またはクラスの動作を変更するための手段です。デコレーターは、以下に示すように使用します。

@some_decorator
def some_func(*args, **kwargs):
    pass

@some_decorator は、some_func が別の some_decorator 関数によってラップされることを表すシンタックス・シュガーです。Python では関数ならびに (type メタクラスを除く) クラスはオブジェクトであるため、以下の処理を適用することができます。

  • 変数に割り当てる
  • コピーする
  • 他の関数にパラメーターとして渡す

上記の構文は、実質的には以下のコードと同等です。:
some_func = some_decorator(some_func)

皆さんが疑問に思っている場合に備え、some_decorator の定義方法を以下に示しておきます。

def some_decorator(f):
    """
    The decorator receives function as a parameter.
    """
    def wrapper(*args, **kwargs):
        # doing something before calling the function
        f(*args, **kwargs)
        # doing something after the function is called
    return wrapper

例えば、URL からデータを取得する関数があるとします。データの取得元となるサーバーにはスロットリング・メカニズムが備わっていて、1 つの IP アドレスから大量のリクエストが定期的に送信されてくることをサーバーが検出すると、リクエストの量が調整されます。このスクレイパーに人間に似た動作をさせるには、不定期の間隔でリクエストを送信して、サーバーを欺くという方法があります。デコレーターにそうさせることができるかどうか見ていきましょう。

from functools import wraps
import random
import time

def wait_random(min_wait=1, max_wait=30):
    def inner_function(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            time.sleep(random.randint(min_wait, max_wait))
            return func(*args, **kwargs)

        return wrapper

    return inner_function

@wait_random(10, 15)
def function_to_scrape():
    # some scraping stuff

inner_function@wraps デコレーターを目にするのは初めてかもしれませんが、よく見れば、inner_function は前に定義した some_decorator に似ていることがわかるはずです。wait_random 内にもう 1 つのラッピング層を設けることによって、デコレーターにもパラメーター (min_waitmax_wait) を渡せるようになります。@wraps は、名前、doc 文字列、関数の属性などといった func のメタデータをコピーする便利なデコレーターです。これらのメタデータを使用しなければ、help(func) などの関数呼び出しから有用な結果を得ることはできません。その場合、関数は func ではなく wrapper の doc 文字列と情報を返すことになるためです。

一方、scraper クラスにそのような関数が複数含まれる場合はどうすればよいでしょうか。

class Scraper:
    def func_to_scrape_1(self):
        # some scraping stuff
        pass
    def func_to_scrape_2(self):
        # some scraping stuff
        pass
    def func_to_scrape_3(self):
        # some scraping stuff
        pass

1 つの方法として、@wait_random を使用して、すべての関数を個別にラップすることもできますが、それよりも有効な方法があります。それは、クラス・デコレーターを作成することです。その概念は、クラスの名前空間を 1 つひとつ調べて関数を識別し、それらの関数をデコレーターでラップするというものです。

def classwrapper(cls):
    for name, val in vars(cls).items():
        # `callable` return `True` if the argument is callable
        # i.e. implements the `__call`
        if callable(val):
            # instead of val, wrap it with our decorator.
            setattr(cls, name, wait_random()(val))
    return cls

これで、クラス全体を @classwrapper でラップすることができます。けれども、複数の scraper クラスがある場合、または scraper のサブクラスが複数ある場合はどうなるでしょうか?その場合は、それらのクラスまたはサブクラスのそれぞれに対して @classwrapper を使用することか、そのようなシナリオの場合にメタクラスを作成することができます。

メタクラス

カスタム・メタクラスを作成するには、以下の 2 つのステップが必要になります。

  1. type メタクラスのサブクラスを作成します。
  2. メタクラス・フックを使用して、新しいメタクラスをクラスの作成プロセスに挿入します。

type クラスをサブクラス化し、__init____new____prepare____call__ などのマジック・メソッドを変更して、クラスを作成するときにその動作を変更します。マジック・メソッドには、基底クラス、クラスの名前、属性、属性の値などの情報を指定します。Python 2 では、メタクラス・フックは __metaclass__ という名前のクラスに含まれる静的フィールドとなっていますが、Python 3 ではクラスの基底クラス・リスト内に metaclass 属性としてメタクラスを指定できます。

>>> class CustomMetaClass(type):
...     def __init__(cls, name, bases, attrs):	
...         for name, value in attrs.items():
                # do some stuff
...             print('{} :{}'.format(name, value))
>>> class SomeClass:
...          # the Python 2.x way
...         __metaclass__ = CustomMetaClass
...         class_attribute = "Some string"
__module__ :__main__
__metaclass__ :<class '__main__.CustomMetaClass'>
class_attribute :Some string

上記の CustomMetaClass を構成する __init__ メソッドには print ステートメントが含まれているため、自動的に属性が出力されます。例えば、Python プロジェクトの協力者が迷惑にもクラスの属性とメソッドに camelCase を使用した名前を選んだとします。当然、その協力者は snake_case を使用すべきです (なにしろ、Python なのですから)。この場合、すべての camelCase 属性を snake_case に変更するメタクラスを作成できるでしょうか?

def camel_to_snake(name):
    """
    A function that converts camelCase to snake_case.
    Referred from: https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
    """
    import re
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

class SnakeCaseMetaclass(type):
    def __new__(snakecase_metaclass, future_class_name,
                future_class_parents, future_class_attr):
        snakecase_attrs = {}
        for name, val in future_class_attr.items():
            snakecase_attrs[camel_to_snake(name)] = val
        return type(future_class_name, future_class_parents,
                    snakecase_attrs)

上記で使用しているマジック・メソッドが __init__ ではなく use __new__ であるのは何故かと言うと、実際にインスタンスを作成する最初のステップは __new__ であるためです。このマジック・メソッドは、クラスの新しいインスタンスを返します。一方、__init__ は何も返しません。このマジック・メソッドの役割は、作成された後のインスタンスを初期化することです。単純な経験則として、新しいインスタンスの作成を制御する必要がある場合は new を使用し、新しいインスタンスの初期化を制御する必要がある場合は init を使用してください。

メタクラス内に実装されている __init__ を目にすることはほとんどないでしょう。それは、__init__ が実際に呼び出される時点でクラスはすでに作成されていることから、このメソッドにそれほど強力な制御力はないためです。このことは、サブクラス化の際に __init__ を実行することでクラス・デコレーターに変更を加えようとしても、クラス・デコレーターはサブクラスに対して呼び出されないという事実を取ってもわかるはずです。

このタスク (camelCase 属性がクラスに入り込まないようにするタスク) には新しいインスタンスの作成が含まれているため、カスタム SnakeCaseMetaClass に含まれる __new__ メソッドをオーバーライドしなければなりません。これが上手く行くかどうか確かめましょう。

>>> class SomeClass(metaclass=SnakeCaseMetaclass):
...     camelCaseVar = 5
>>> SomeClass.camelCaseVar
AttributeError: type object 'SomeClass' has no attribute 'camelCaseVar'
>>> SomeClass.camel_case_var
5

上手く行きました!Python でメタクラスを作成して使用する方法がわかったところで、次はメタクラスをどのように利用できるのか調べましょう。

Python でメタクラスを使用する

メタクラスは、属性、メソッド、およびそれらの値に対し、さまざまに異なるガイドラインを強制するために使用できます。上記の (snake_case を用いた) 例に沿った同様の例としては、以下が挙げられます。

  • 値をドメインに制限する
  • 値を暗黙的にカスタム・クラスに変換する (クラスを作成するユーザーから複雑な側面のすべてを隠す必要がある場合)
  • さまざまに異なる命名規則とスタイルのガイドラインを適用する (すべてのメソッドに docstring を持たせるなど)
  • クラスに新しい属性を追加する

クラス定義自体にロジックのすべてを定義するのではなく、メタクラスを使用するのはなぜなのかというと、主にコードベース全体でコードを繰り返すことを避けるためです。

メタクラスの実際の使用ケース

メタクラスはサブクラスの間で継承されるため、コードが冗長になるという実際的な問題が解決されます (DRY (Don't Repeat Yourself) の原則)。また、クラスの作成に伴う複雑なロジックを抽象化するという点でも、メタクラスは役立ちます。メタクラスは一般に、クラス・オブジェクトの作成時に追加のアクションを実行したり、コードをさらに追加したりすることによって、ロジックを抽象化するためです。例として、メタクラスの実際の使用ケースには以下があります。

  • 抽象基底クラス
  • クラスの登録
  • ライブラリーおよびフレームワーク内で API を作成する

それぞれの使用ケースを見ていきましょう。

抽象基底クラス

抽象基底クラスとは、継承されることだけが意図されていて、インスタンス化は意図されていないクラスのことです。以下の Python クラスがあるとします。

from abc import ABCMeta, abstractmethod

class Vehicle(metaclass=ABCMeta):

    @abstractmethod
    def refill_tank(self, litres):
        pass

    @abstractmethod
    def move_ahead(self):
        pass

上記の Vehicle クラスを継承する Truck クラスを作成しましょう。

class Truck(Vehicle):
    def __init__(self, company, color, wheels):
        self.company = company
        self.color = color
        self.wheels = wheels

    def refill_tank(self, litres):
        pass

    def move_ahead(self):
        pass

上記のコードでは、抽象メソッドを実装していないことに注目してください。この Truck クラスのオブジェクトをインスタンス化しようとするとどうなるか見てください。

>>> mini_truck = Truck("Tesla Roadster", "Black", 4)

TypeError: Can't instantiate abstract class Truck with abstract methods move_ahead, refill_tank

このエラーを修正するには、Truck クラスに含まれる両方の抽象メソッドを定義します。

class Truck(Vehicle):
    def __init__(self, company, color, wheels):
        self.company = company
        self.color = color
        self.wheels = wheels

    def refill_tank(self, litres):
        pass

    def move_ahead(self):
        pass
>>> mini_truck = Truck("Tesla Roadster", "Black", 4)
>>> mini_truck
<__main__.Truck at 0x7f881ca1d828>

クラスの登録

クラスの登録を理解するために、ある特定のサーバーに複数のファイル・ハンドラーがあるという例を取り上げます。ファイル形式に基づく正しいハンドラー・クラスを迅速に検出できるようにするために、ハンドラー辞書を作成して、コード内に出現する複数の異なるハンドラーを CustomMetaclass によって登録させます。

handlers = {}

class CustomMetaclass(type):
    def __new__(meta, name, bases, attrs):
        cls = type.__new__(meta, name, bases, attrs)
        for ext in attrs["files"]:
            handlers[ext] = cls
        return cls

class Handler(metaclass=CustomMetaclass):
    formats = []
    # common stuff for all kinds of file format handlers


class ImageHandler(Handler):
    formats = ["jpeg", "png"]

class AudioHandler(Handler):
    formats = ["mp3", "wav"]
>>> handlers
{'mp3': __main__.AudioHandler,
 'jpeg': __main__.ImageHandler,
 'png': __main__.ImageHandler,
 'wav': __main__.AudioHandler}

これで、ファイルの形式に基づいて、どのハンドラー・クラスを使用すべきかを簡単に識別できるようになります。一般的に言えば、クラスの特性を保管する、ある種のデータ構造を維持する必要がある場合は常に、メタクラスを使用できます。

API を作成する

サブクラス間でロジックが重複しないようにできること、そしてユーザーにとっては知る必要のないカスタム・クラスの作成ロジックを隠せることから、フレームワークやライブラリー内ではメタクラスが広く用いられています。つまり、ボイラープレートを縮小できる可能性、そして API を簡潔にできる可能性があることを意味します。一例として、Django の ORM を使用した以下のコード・スニペットを見てください。

>>> from from django.db import models
>>> class Vehicle(models.Model):
...    color = models.CharField(max_length=10)
...    wheels = models.IntegerField()

上記では、Django パッケージに含まれる models.Model クラスを継承する Vehicle クラスを作成します。このクラス内に、自動車の特性を表すいくつかのフィールド (color と wheels) を定義します。このように作成したクラスのオブジェクトをインスタンス化してみましょう。

>>> four_wheeler = Vehicle(color="Blue", wheels="Four")
# Raises an error
>>> four_wheeler = Vehicle(color="Blue", wheels=4)
>>> four_wheeler.wheels
4

Vehicle のモデルを作成するユーザーとして対処しなければならなかったのは、models.Model クラスを継承して高位レベルでのステートメントをいくつか作成することだけです。残りの作業 (データベースへの接続を作成する、無効な値に対してエラーを起動する、models.IntegerField ではなく int 型を返すなど) は、model.Models とこのクラスで使用するメタクラスにより、舞台裏で行われています。

まとめ

このチュートリアルでは、Python におけるインスタンス、クラス、メタクラスの関係を明かにし、メタプログラミングを使用してコードを操作する方法について説明しました。カスタム動作をクラスやメソッドに注入する手段となる関数デコレーターとクラス・デコレーターについて解説した後、Python のデフォルト type メタクラスをサブクラス化してカスタム・メタクラスを実装する方法を見ていきました。そして最後に、メタクラスの実際的な使用ケースを紹介しました。メタクラスを使用すべきかどうかについてはオンラインで盛んに議論されていますが、この記事で学んだ知識があれば、メタプログラミングを実際に使用して、それによってより効率的に問題を解決できるかどうかを分析して答えを見つけられるはずです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=ビジネス・アナリティクス, Open source
ArticleID=1062203
ArticleTitle=Python でのメタプログラミング
publish-date=09202018