`classmethod`とメタクラスメソッドの違いは何ですか?


12

Pythonでは、@classmethodデコレータを使用してクラスメソッドを作成できます。

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>

あるいは、メタクラスで通常の(インスタンス)メソッドを使用できます。

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>

の出力に示されているようにC.f()、これら2つのアプローチは同様の機能を提供します。

@classmethodメタクラスでの通常のメソッドの使用と使用の違いは何ですか?

回答:


5

クラスはメタクラスのインスタンスであるため、メタクラスの「インスタンスメソッド」がクラスメソッドのように動作することは予想外ではありません。

しかし、はい、違いがあります-そしてそれらのいくつかは意味論以上のものです:

  1. 最も重要な違いは、メタクラスのメソッドがクラスインスタンスから「見えない」ことです。これは、Pythonでの属性ルックアップ(簡略化された方法-記述子が優先される場合があります)がインスタンスの属性を検索するために発生します。インスタンスに属性が存在しない場合、Pythonはそのインスタンスのクラスを検索し、検索が続行されます。クラスのスーパークラスではなく、クラスのクラス。Python stdlibは、abc.ABCMeta.registerメソッドでこの機能を使用します。クラス自体に関連するメソッドは、競合することなくインスタンス属性として自由に再利用できます(ただし、メソッドは依然として競合します)。
  2. もう1つの違いは明らかですが、メタクラスで宣言されたメソッドはいくつかのクラスで使用でき、それ以外の場合は関連しません-異なるクラス階層があり、それらが扱うものにはまったく関連がなく、すべてのクラスにいくつかの共通機能が必要な場合、両方の階層のベースとして含める必要があるミックスインクラスを考え出す必要があります(たとえば、すべてのクラスをアプリケーションレジストリに含める場合など)。(注意:ミックスインはメタクラスよりも優れた呼び出しになる場合があります)
  3. classmethodは特殊な「classmethod」オブジェクトですが、メタクラスのメソッドは通常の関数です。

したがって、クラスメソッドが使用するメカニズムは「記述子プロトコル」であることが起こります。通常の関数は、インスタンスから取得したときに引数__get__を挿入selfし、クラスから取得したときにその引数を空のままにするメソッドを備えてい classmethodますが、オブジェクトには__get__、クラス自体(「所有者」)を両方の状況での最初のパラメーター。

これはほとんどの場合実際的な違いはありませんが、メタクラスのメソッドがmeta.method関数を取得するために、デコレータを動的に追加するために、関数としてメソッドにアクセスする場合は、関数にアクセスする必要がある場合は、すぐに使用できます、cls.my_classmethod.__func__ それを使用してクラスメソッドからclassmethodオブジェクトを取得する必要があります(ラッピングを行う場合は、別のオブジェクトを作成して割り当て直す必要があります)。

基本的に、これらは2つの例です。


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)

言い換えると、メタクラスで定義されたメソッドがインスタンスから可視でclassmethodオブジェクトが可視ではないという重要な違いは別として、実行時のその他の違いは、あいまいで意味がないように見えますが、それは言語が移動する必要がないために起こりますクラスメソッドの特別なルールの外に:言語設計の結果として、クラスメソッドを宣言する両方の方法が可能です。1つは、クラス自体がオブジェクトであるという事実であり、もう1つは、多くの場合の可能性です。インスタンスとクラスの属性アクセスを特殊化できる記述子プロトコルの使用:

classmethod組み込みは、ネイティブコードで定義されているが、それはただ純粋なPythonでコーディングすることができ、正確に同じ方法で動作します。以下の5行のクラスclassmethodは、組み込みの@classmethod" at all (though distinguishable through introspection such as calls toisinstance , and evenrepr`とランタイムに違いがないデコレータとして使用できます):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)

また、メソッドを超え@propertyて、メタクラスののような特殊化された属性がまったく同じように特殊化されたクラス属性として機能し、驚くべき動作をまったく行わないことを覚えておくことは興味深いです。


2

質問で行ったように@classmethod言い表すと、とメタクラスは似ているように見えるかもしれませんが、目的はかなり異なります。@classmethodの引数に挿入されるクラスは、通常、インスタンスを構築するために使用されます(つまり、代替コンストラクター)。一方、メタクラスは通常、クラス自体を変更するために使用されます(たとえば、DjangoがそのモデルDSLで行うことのように)。

それは、クラスメソッド内のクラスを変更できないということではありません。しかし、問題は、なぜ最初からそれを変更したい方法でクラスを定義しなかったのでしょうか。そうでない場合は、複数のクラスを使用するようにリファクタリングすることをお勧めします。

最初の例を少し拡張してみましょう。

class C:
    @classmethod
    def f(cls):
        print(f'f called with cls={cls}')

Python docsを借用すると、上記は次のように拡張されます。

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

class C:
    def f(cls):
        print(f'f called with cls={cls}')
    f = ClassMethod(f)

注どの__get__インスタンスまたはクラスのいずれかを取る(あるいはその両方)、したがって、あなたは両方を行うことができますすることができますC.fC().f。これは、AttributeErrorfor をスローするメタクラスの例とは異なりますC().f

さらに、メタクラスの例でfは、には存在しませんC.__dict__。で属性fC.f検索するとき、インタプリタは見てC.__dict__、それが見つからなかった後にtype(C).__dict__(つまりM.__dict__)を見ます。あなたは柔軟性をオーバーライドしたい場合、これは問題ありfC、私はこれが今まで実用的になります疑うが、。


0

あなたの例では、違いは、メタクラスとしてMが設定される他のいくつかのクラスにあります。

class M(type):
    def f(cls):
        pass

class C(metaclass=M):
    pass

class C2(metaclass=M):
    pass

C.f()
C2.f()
class M(type):
     pass

class C(metaclass=M):
     @classmethod
     def f(cls):
        pass

class C2(metaclass=M):
    pass

C.f()
# C2 does not have 'f'

メタクラスの詳細はこちらメタクラスの (具体的な)ユースケースは何ですか?


0

@classmethodとMetaclassはどちらも異なります。

Pythonのすべてがオブジェクトです。すべてのものはすべてのものを意味します。

メタクラスとは?

言ったようにすべてのものはオブジェクトです。クラスはオブジェクトでもあり、実際にはクラスは正式にメタクラスと呼ばれる他の不思議なオブジェクトのインスタンスです。Pythonのデフォルトのメタクラスは、指定されていない場合は「タイプ」です

デフォルトでは、定義されたすべてのクラスはタイプのインスタンスです。

クラスはメタクラスのインスタンスです

言及された行動を理解することはいくつかの重要なポイントです

  • クラスはメタクラスのインスタンスなので。
  • インスタンス化されたすべてのオブジェクトと同様に、オブジェクト(インスタンス)もクラスから属性を取得します。クラスはメタクラスから属性を取得します

次のコードを検討する

class Meta(type):
    def foo(self):
        print(f'foo is called self={self}')
        print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))

class C(metaclass=Meta):
    pass

C.foo()

どこ、

  • クラスCはクラスMetaのインスタンスです
  • 「クラスC」は「クラスメタ」のインスタンスであるクラスオブジェクト
  • 他のオブジェクト(インスタンス)と同様に、「クラスC」は、「クラスメタ」クラスで定義された属性/メソッドにアクセスできます。
  • したがって、「C.foo()」をデコードします。「C」は「Meta」のインスタンスであり、「foo」は「C」である「Meta」のインスタンスを介したメソッド呼び出しです。
  • メソッド「foo」の最初の引数は、「classmethod」とは異なり、クラスではなくインスタンスへの参照です

「クラスC」が「クラスメタ」のインスタンスであるかのように確認できます。

  isinstance(C, Meta)

クラスメソッドとは何ですか?

Pythonメソッドはバインドされていると言われています。Pythonは、メソッドがインスタンスでのみ呼び出される必要があるという制限を課しているので。インスタンスを作成せずに、インスタンスなしで(javaの静的メンバーのように)クラスを介して直接メソッドを呼び出す場合があります。デフォルトでは、メソッドを呼び出すためにインスタンスが必要です。回避策として、Pythonは、指定されたメソッドをインスタンスではなくクラスにバインドする組み込み関数classmethodを提供します。

クラスメソッドはクラスにバインドされています。インスタンス(自己)ではなくクラス自体への参照である少なくとも1つの引数を取ります

組み込み関数/デコレータクラスメソッドが使用されている場合。最初の引数はインスタンスではなくクラスへの参照になります

class ClassMethodDemo:
    @classmethod
    def foo(cls):
        print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

「classmethod」を使用したので、次のようにインスタンスを作成せずにメソッド「foo」を呼び出します

ClassMethodDemo.foo()

上記のメソッド呼び出しはTrueを返します。最初の引数clsは確かに「ClassMethodDemo」への参照であるため

概要:

  • Classmethodの最初の引数は「クラス(伝統的にclsと呼ばれます)自体への参照」です
  • メタクラスのメソッドはクラスメソッドではありません。メタクラスのメソッドは、「クラスではなくインスタンス(伝統的に自己と呼ばれる)への参照」である最初の引数を受け取ります
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.