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
メタクラスのインスタンスです。 -
type
はtype
メタクラスのインスタンスでもあるため、それ自体のインスタンスです。
クラスを動的に定義する
組み込みのtype()
関数は、1つの引数が渡されると、オブジェクトの型を返します。 新しいスタイルのクラスの場合、これは通常、https://docs.python.org/3/library/stdtypes.html#instance。class [オブジェクトの__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_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
のインスタンスが作成されるたびに、デフォルトでは、100
の値を持つattr
という属性で初期化されます。 s。 (このようなコードは、通常、__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 "", 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):
は、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)
クラスがオブジェクト作成のテンプレートとして機能するのと同じように、メタクラスはクラス作成のテンプレートとして機能します。 メタクラスは、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を一般的に理解し、メタクラスが実際に使用するのに適切なツールであるかどうかを認識できるようにすることは有益です。