Pythonメタクラス

Pythonメタクラス

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

メタクラスは難解なhttps://realpython.com/python3-object-oriented-programming/[OOP concept]であり、ほぼすべてのPythonコードの背後に潜んでいます。 あなたはそれを知っているかどうかに関係なくそれらを使用しています。 ほとんどの場合、意識する必要はありません。 ほとんどのPythonプログラマーは、メタクラスについて考える必要はほとんどありません。

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

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

ティムピーターズ _ _

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

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

*通知を受け取る:*このチュートリアルのフォローアップをお見逃しなく—https://realpython.com/bonus/newsletter-dont-miss-updates/[Real Python Newsletterに参加するにはここをクリックしてください]次の分割払いが出てきます。

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

Pythonレルムでは、クラスhttps://wiki.python.org/moin/NewClassVsClassicClass[2つの種類のいずれかになります]。 公式の用語はまだ決定されていないため、非公式には古いスタイルと新しいスタイルのクラスと呼ばれます。

古いスタイルのクラス

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

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

新しいスタイルのクラス

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

>>>

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> 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では、オブジェクトのタイプとそのクラスを交換可能に参照するのが妥当です。

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

Pythonでは、すべてがオブジェクトです。クラスもオブジェクトです。 そのため、クラスには型が必要です。 クラスのタイプは何ですか?

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

>>>

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

>>> type(x)
<class '__main__.Foo'>

>>> type(Foo)
<class 'type'>

ご想像のとおり、「+ x 」のタイプはクラス「 Foo 」です。 しかし、クラス自体である ` Foo `の型は ` type `です。 一般に、新しいスタイルのクラスのタイプは ` type +`です。

よく知っている組み込みクラスの型も `+ type +`です:

>>>

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

さらに言えば、 `+ type `の型も ` type +`です(はい、本当に)。

>>>

>>> type(type)
<class 'type'>

`+ type `はメタクラスであり、そのクラスはインスタンスです。 通常のオブジェクトがクラスのインスタンスであるように、Pythonの新しいスタイルのクラス、したがってPython 3のクラスはすべて、「 type +」メタクラスのインスタンスです。

上記の場合:

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

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

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

Python class chain、width = 155、高さ= 304

クラスを動的に定義する

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

>>>

>>> type(3)
<class 'int'>

>>> type(['foo', 'bar', 'baz'])
<class 'list'>

>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>

>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

`+ type(<name>、<bases>、<dct>)`の3つの引数で ` 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__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

>>>

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

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

実施例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

Pythonの + lambda +で定義できるのは非常に単純な関数のみです。 次の例では、やや複雑な関数が外部で定義され、名前空間ディクショナリの「+ attr_val 」に「 f +」という名前で割り当てられます。

>>>

>>> 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 `のインスタンスが作成されるたびに、デフォルトでは ` attr `と呼ばれる値で初期化され、値は ` 100 `になります。 (このようなコードは、通常は ` init ()`メソッドではなく、通常は ` new ()+`で表示されます。 この例は、デモンストレーションを目的としています。)

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

>>>

# 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 "<pyshell#77>", line 1, in <module>
    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):`は、 ` Meta `が ` type `から派生することを指定します。 ` 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)

クラスがオブジェクト作成のテンプレートとして機能するのと同じように、メタクラスはクラス作成のテンプレートとして機能します。 メタクラスは、https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)[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

結論

ティムピーターズが示唆するように、*メタクラス*は、「問題を探す解決策」であるという領域に容易に移行できます。通常、カスタムメタクラスを作成する必要はありません。 目の前の問題をより簡単な方法で解決できる場合は、おそらくそうであるはずです。 それでも、メタクラスを理解すると、https://realpython.com/python3-object-oriented-programming/[Python classes]を理解し、メタクラスが実際に使用する適切なツールである場合を認識できるため、有益です。