Pythonメタクラス

Pythonメタクラス

metaprogrammingという用語は、プログラムがそれ自体を知っている、または操作する可能性を指します。 Pythonは、metaclassesと呼ばれるクラスのメタプログラミングの形式をサポートしています。

メタクラスは難解なOOP conceptであり、事実上すべてのPythonコードの背後に潜んでいます。 あなたはそれを知っているかどうかに関係なくそれらを使用しています。 ほとんどの場合、意識する必要はありません。 ほとんどのPythonプログラマーは、メタクラスについて考える必要はほとんどありません。

ただし、必要に応じて、Pythonはすべてのオブジェクト指向言語がサポートするわけではない機能を提供します。内部でカスタムメタクラスを定義できます。 Zen of Pythonを作成したPythonの第一人者であるTim Petersからの次の引用で示唆されているように、カスタムメタクラスの使用については多少議論の余地があります。

「メタクラスは、99%のユーザーが心配する必要のない魔法です。 それらが必要かどうか疑問に思う場合、あなたは必要ありません(実際にそれらを必要とする人々は、彼らがそれらを必要とすることを確実に知っており、理由についての説明は必要ありません)

Tim Peters

Pythonの愛好家は(Pythonの愛好家が知られているように)カスタムメタクラスを使用すべきではないと信じているPythonistaがいます。 それは少し遠いかもしれませんが、おそらくカスタムメタクラスはほとんど必要ないことは事実です。 問題がそれらを必要とすることが明らかでない場合、より簡単な方法で解決すれば、おそらくよりクリーンで読みやすくなります。

それでも、Pythonメタクラスを理解することは価値があります。Pythonクラスの内部をよりよく理解できるからです。 あなたは決して知らない:あなたはいつかカスタムメタクラスがあなたが望むものであると知っているような状況の1つに自分自身を見つけるかもしれません。

Get Notified:このチュートリアルのフォローアップをお見逃しなく—https://realpython.com/bonus/newsletter-dont-miss-updates/ [ここをクリックしてRealPythonニュースレターに参加してください]いつかわかります次の記事が出ます。

古いスタイルと 新しいスタイルのクラス

Pythonレルムでは、クラスcan be one of two varieties。 公式の用語はまだ決定されていないため、非公式には古いスタイルと新しいスタイルのクラスと呼ばれます。

古いスタイルのクラス

古いスタイルのクラスでは、クラスとタイプはまったく同じものではありません。 古いスタイルのクラスのインスタンスは、常にinstanceと呼ばれる単一の組み込み型から実装されます。 objが古いスタイルのクラスのインスタンスである場合、obj.__class__はクラスを指定しますが、type(obj)は常にinstanceです。 次の例は、Python 2.7から取得したものです。

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__

>>> type(x)

新しいスタイルのクラス

新しいスタイルのクラスは、クラスと型の概念を統一します。 objが新しいスタイルのクラスのインスタンスである場合、type(obj)obj.__class__と同じです。

>>>

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__

>>> type(obj)

>>> obj.__class__ is type(obj)
True

>>>

>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True

タイプとクラス

Python 3では、すべてのクラスは新しいスタイルのクラスです。 したがって、Python 3では、オブジェクトのタイプとそのクラスを交換可能に参照するのが妥当です。

Note: Python 2では、クラスはデフォルトで古いスタイルです。 Python 2.2より前では、新しいスタイルのクラスはまったくサポートされていませんでした。 Python 2.2以降では作成できますが、明示的に新しいスタイルとして宣言する必要があります。

in Python, everything is an object.クラスもオブジェクトであることを忘れないでください。 そのため、クラスには型が必要です。 クラスのタイプは何ですか?

次の点を考慮してください。

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)


>>> type(Foo)

予想どおり、xのタイプはクラスFooです。 ただし、クラス自体であるFooのタイプはtypeです。 一般に、新しいスタイルのクラスのタイプはtypeです。

使い慣れた組み込みクラスのタイプもtypeです。

>>>

>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...




さらに言えば、typeのタイプもtypeです(はい、本当に):

>>>

>>> type(type)

typeはメタクラスであり、そのクラスはインスタンスです。 通常のオブジェクトがクラスのインスタンスであるのと同じように、Pythonの新しいスタイルのクラス、つまりPython 3のクラスは、typeメタクラスのインスタンスです。

上記の場合:

  • xは、クラスFooのインスタンスです。

  • Fooは、typeメタクラスのインスタンスです。

  • typetypeメタクラスのインスタンスでもあるため、それ自体のインスタンスです。

Python class chain

クラスを動的に定義する

組み込みのtype()関数は、1つの引数が渡されると、オブジェクトの型を返します。 新しいスタイルのクラスの場合、これは通常、https://docs.python.org/3/library/stdtypes.html#instanceclass [オブジェクトの__class__属性]と同じです。

>>>

>>> type(3)


>>> type(['foo', 'bar', 'baz'])


>>> t = (1, 2, 3, 4, 5)
>>> type(t)


>>> class Foo:
...     pass
...
>>> type(Foo())

3つの引数(type(<name>, <bases>, <dct>))を使用してtype()を呼び出すこともできます。

  • <name>はクラス名を指定します。 これがクラスの__name__属性になります。

  • <bases>は、クラスが継承する基本クラスのタプルを指定します。 これがクラスの__bases__属性になります。

  • <dct>は、クラス本体の定義を含む名前空間ディクショナリを指定します。 これがクラスの__dict__属性になります。

この方法でtype()を呼び出すと、typeメタクラスの新しいインスタンスが作成されます。 つまり、新しいクラスを動的に作成します。

次の各例では、上のスニペットはtype()を使用して動的にクラスを定義し、その下のスニペットはclassステートメントを使用して通常の方法でクラスを定義します。 いずれの場合も、2つのスニペットは機能的に同等です。

例1

この最初の例では、type()に渡される<bases>引数と<dct>引数は両方とも空です。 親クラスからの継承は指定されず、名前空間ディクショナリには最初に何も配置されません。 これは可能な限り最も単純なクラス定義です。

>>>

>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

例2

ここで、<bases>は、単一の要素Fooを持つタプルであり、Barが継承する親クラスを指定します。 属性attrは、最初に名前空間ディクショナリに配置されます。

>>>

>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__

>>> x.__class__.__bases__
(,)

>>>

>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__

>>> x.__class__.__bases__
(,)

実施例3

今回は、<bases>は再び空になります。 2つのオブジェクトは、<dct>引数を介して名前空間ディクショナリに配置されます。 1つ目はattrという名前の属性で、2つ目はattr_valという名前の関数で、定義されたクラスのメソッドになります。

>>>

>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

>>>

>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

実施例4

lambda in Pythonで定義できるのは、非常に単純な関数だけです。 次の例では、少し複雑な関数が外部で定義され、名前空間ディクショナリのattr_valfという名前で割り当てられています。

>>>

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

>>>

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

カスタムメタクラス

この使い古された例をもう一度考えてみましょう。

>>>

>>> class Foo:
...     pass
...
>>> f = Foo()

Foo()は、クラスFooの新しいインスタンスを作成します。 インタプリタがFoo()に遭遇すると、以下が発生します。

  • Fooの親クラスの__call__()メソッドが呼び出されます。 Fooは標準の新しいスタイルのクラスであるため、その親クラスはtypeメタクラスであり、type__call__()メソッドが呼び出されます。

  • その__call__()メソッドは、次に以下を呼び出します。

    • __new__()

    • __init__()

Foo__new__()__init__()を定義していない場合、デフォルトのメソッドはFooの祖先から継承されます。 ただし、Fooがこれらのメソッドを定義している場合、それらは祖先からのメソッドをオーバーライドします。これにより、Fooをインスタンス化するときにカスタマイズされた動作が可能になります。

以下では、new()というカスタムメソッドが定義され、Foo__new__()メソッドとして割り当てられています。

>>>

>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

これにより、クラスFooのインスタンス化動作が変更されます。Fooのインスタンスが作成されるたびに、デフォルトでは、100の値を持つattrという属性で初期化されます。 s。 (このようなコードは、通常、__init__()メソッドに表示され、通常は__new__()には表示されません。 この例は、デモンストレーションを目的としています。)

さて、すでに繰り返したように、クラスもオブジェクトです。 Fooのようなクラスを作成するときに、インスタンス化の動作を同様にカスタマイズしたいとします。 上記のパターンに従う場合は、カスタムメソッドを再度定義し、それをFooがインスタンスであるクラスの__new__()メソッドとして割り当てます。 Footypeメタクラスのインスタンスであるため、コードは次のようになります。

>>>

# Spoiler alert:  This doesn't work!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "", line 1, in 
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

ご覧のとおり、typeメタクラスの__new__()メソッドを再割り当てすることはできません。 Pythonは許可していません。

これもおそらく同じです。 typeは、すべての新しいスタイルのクラスが派生するメタクラスです。 とにかくいじってはいけません。 しかし、クラスのインスタンス化をカスタマイズする場合、どのような手段がありますか?

可能な解決策の1つは、カスタムメタクラスです。 基本的に、typeメタクラスをいじくり回す代わりに、typeから派生した独自のメタクラスを定義して、代わりにそれをいじくり回すことができます。

最初のステップは、次のように、typeから派生するメタクラスを定義することです。

>>>

>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

定義ヘッダーclass Meta(type):は、Metatypeから派生することを指定します。 typeはメタクラスであるため、Metaもメタクラスになります。

カスタム__new__()メソッドがMetaに対して定義されていることに注意してください。 typeメタクラスに直接それを行うことはできませんでした。 __new__()メソッドは次のことを行います。

  • super()を介して親メタクラス(type)の__new__()メソッドに委任し、実際に新しいクラスを作成します

  • カスタム属性attrを値100でクラスに割り当てます

  • 新しく作成されたクラスを返します

ブードゥーの残りの半分:新しいクラスFooを定義し、そのメタクラスが標準メタクラスtypeではなくカスタムメタクラスMetaであることを指定します。 これは、次のようにクラス定義でmetaclassキーワードを使用して行われます。

>>>

>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

Voila!Fooは、Metaメタクラスからattr属性を自動的に取得しました。 もちろん、同様に定義する他のクラスも同様に行います。

>>>

>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

クラスがオブジェクト作成のテンプレートとして機能するのと同じように、メタクラスはクラス作成のテンプレートとして機能します。 メタクラスは、class factoriesと呼ばれることもあります。

次の2つの例を比較します。

オブジェクトファクトリ:

>>>

>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100

クラスファクトリ:

>>>

>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100

これは本当に必要ですか?

上記のクラスファクトリの例と同じくらい簡単ですが、それがメタクラスの仕組みの本質です。 クラスのインスタンス化をカスタマイズできます。

それでも、これは、新しく作成された各クラスにカスタム属性attrを付与するだけで大​​騒ぎになります。 そのためだけにメタクラスが本当に必要ですか?

Pythonには、同じことを効果的に達成できる方法が少なくとも2つあります。

単純な継承:

>>>

>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

クラスデコレータ:

>>>

>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

結論

Tim Petersが示唆しているように、metaclassesは、「問題を探すための解決策」であるという領域に簡単に向きを変えることができます。通常、カスタムメタクラスを作成する必要はありません。 目の前の問題をより簡単な方法で解決できる場合は、おそらくそうであるはずです。 それでも、メタクラスを理解して、Python classesを一般的に理解し、メタクラスが実際に使用するのに適切なツールであるかどうかを認識できるようにすることは有益です。