web-dev-qa-db-ja.com

__new__?の代わりにメタクラスの__call__メソッドを使用する

メタクラスについて議論するとき、 ドキュメント 状態:

もちろん、他のクラスメソッドをオーバーライドする(または新しいメソッドを追加する)こともできます。たとえば、メタクラスでカスタム__call__()メソッドを定義すると、クラスが呼び出されたときのカスタム動作が可能になります。常に新しいインスタンスを作成するわけではありません。

私の質問は次のとおりです。たとえば、クラスが呼び出されたときに、新しいオブジェクトを作成するのではなく、キャッシュするなどのカスタム動作が必要だとします。これを行うには、クラスの__new__メソッドをオーバーライドします。代わりに__call__を使用してメタクラスを定義する必要があるのはいつですか?このアプローチでは、__new__では実現できないことは何ですか?

37
Eli Bendersky

あなたの質問への直接の答えは、インスタンスの作成をカスタマイズするだけではなくmoreしたいとき、またはクラスdoes作成方法から。

Pythonでのシングルトンの作成 への私の回答と関連する議論を参照してください。

いくつかの利点があります。

  1. クラスが行うことと、作成方法の詳細を分離することができます。メタクラスとクラスはそれぞれ1つのことに責任があります。

  2. メタクラスにコードを1回記述し、それを使用して、複数の継承を気にすることなく、いくつかのクラスの呼び出し動作をカスタマイズできます。

  3. サブクラスは__new__メソッドの動作をオーバーライドできますが、メタクラスの__call____new__を呼び出す必要すらありません。

  4. セットアップ作業がある場合は、メタクラスの__new__メソッドで行うことができます。これは、クラスが呼び出されるたびではなく、1回だけ行われます。

単一責任の原則について心配していなければ、__new__のカスタマイズがうまく機能するケースは確かにたくさんあります。

しかし、インスタンスが作成されるときではなく、クラスが作成されるときに、より早く行わなければならない他のユースケースがあります。メタクラスが必要になるのは、これらがプレイされるときです。多くの優れた例については、 Pythonでのメタクラスの(具体的な)ユースケースは何ですか? を参照してください。

23
agf

これらのメソッドの実行順序を注意深く観察すると、微妙な違いが少しわかりやすくなります。

_class Meta_1(type):
    def __call__(cls, *a, **kw):
        print "entering Meta_1.__call__()"
        rv = super(Meta_1, cls).__call__(*a, **kw)
        print "exiting Meta_1.__call__()"
        return rv

class Class_1(object):
    __metaclass__ = Meta_1
    def __new__(cls, *a, **kw):
        print "entering Class_1.__new__()"
        rv = super(Class_1, cls).__new__(cls, *a, **kw)
        print "exiting Class_1.__new__()"
        return rv

    def __init__(self, *a, **kw):
        print "executing Class_1.__init__()"
        super(Class_1,self).__init__(*a, **kw)
_

上記のコードは実際にはdoを実行していないことに注意してください。各メソッドは、その親の実装、つまりデフォルトに従います。したがって、ログを記録するだけでなく、次のように宣言したかのようになります。

_class Meta_1(type): pass
class Class_1(object):
    __metaclass__ = Meta_1
_

次に、_Class_1_のインスタンスを作成しましょう

_c = Class_1()
# entering Meta_1.__call__()
# entering Class_1.__new__()
# exiting Class_1.__new__()
# executing Class_1.__init__()
# exiting Meta_1.__call__()
_

したがって、typeが_Meta_1_の親である場合、type.__call__()の疑似実装を次のように想像できます。

_class type:
    def __call__(cls, *args, **kwarg):

        # ... a few things could possibly be done to cls here... maybe... or maybe not...

        # then we call cls.__new__() to get a new object
        obj = cls.__new__(cls, *args, **kwargs)

        # ... a few things done to obj here... maybe... or not...

        # then we call obj.__init__()
        obj.__init__(*args, **kwargs)

        # ... maybe a few more things done to obj here

        # then we return obj
        return obj
_

上記の呼び出し順序から、Meta_1.__call__()(またはこの場合はtype.__call__())にClass_1.__new__()およびClass_1.__init__()は最終的に作成されます。その実行の過程で、Meta_1.__call__()は、どちらにも触れられていないオブジェクトを返す可能性があります。シングルトンパターンへのこのアプローチを例にとります:

_class Meta_2(type):
    __Class_2_singleton__ = None
    def __call__(cls, *a, **kw):
        # if the singleton isn't present, create and register it
        if not Meta_2.__Class_2_singleton__:
            print "entering Meta_2.__call__()"
            Meta_2.__Class_2_singleton__ = super(Meta_2, cls).__call__(*a, **kw)
            print "exiting Meta_2.__call__()"
        else:
            print ("Class_2 singleton returning from Meta_2.__call__(), "
                    "super(Meta_2, cls).__call__() skipped")
        # return singleton instance
        return Meta_2.__Class_2_singleton__

class Class_2(object):
    __metaclass__ = Meta_2
    def __new__(cls, *a, **kw):
        print "entering Class_2.__new__()"
        rv = super(Class_2, cls).__new__(cls, *a, **kw)
        print "exiting Class_2.__new__()"
        return rv

    def __init__(self, *a, **kw):
        print "executing Class_2.__init__()"
        super(Class_2, self).__init__(*a, **kw)
_

タイプ_Class_2_のオブジェクトを繰り返し作成しようとするとどうなるかを見てみましょう

_a = Class_2()
# entering Meta_2.__call__()
# entering Class_2.__new__()
# exiting Class_2.__new__()
# executing Class_2.__init__()
# exiting Meta_2.__call__()

b = Class_2()
# Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped

c = Class_2()
# Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped

print a is b is c
True
_

次に、クラスの__new__()メソッドを使用してこの実装を観察し、同じことを実行してみます。

_import random
class Class_3(object):

    __Class_3_singleton__ = None

    def __new__(cls, *a, **kw):
        # if singleton not present create and save it
        if not Class_3.__Class_3_singleton__:
            print "entering Class_3.__new__()"
            Class_3.__Class_3_singleton__ = rv = super(Class_3, cls).__new__(cls, *a, **kw)
            rv.random1 = random.random()
            rv.random2 = random.random()
            print "exiting Class_3.__new__()"
        else:
            print ("Class_3 singleton returning from Class_3.__new__(), "
                   "super(Class_3, cls).__new__() skipped")

        return Class_3.__Class_3_singleton__ 

    def __init__(self, *a, **kw):
        print "executing Class_3.__init__()"
        print "random1 is still {random1}".format(random1=self.random1)
        # unfortunately if self.__init__() has some property altering actions
        # they will affect our singleton each time we try to create an instance 
        self.random2 = random.random()
        print "random2 is now {random2}".format(random2=self.random2)
        super(Class_3, self).__init__(*a, **kw)
_

上記の実装は、クラスにシングルトンを正常に登録しても、__init__()の呼び出しを妨げないことに注意してください。これは、暗黙的にtype.__call__()で発生します(typeがデフォルトです)何も指定されていない場合はメタクラス)。これにより、いくつかの望ましくない影響が生じる可能性があります。

_a = Class_3()
# entering Class_3.__new__()
# exiting Class_3.__new__()
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.739298365475

b = Class_3()
# Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.247361634396

c = Class_3()
# Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.436144427555

d = Class_3()
# Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped
# executing Class_3.__init__()
# random1 is still 0.282724600824
# random2 is now 0.167298405242

print a is b is c is d
# True
_
16
Michael Ekoka

1つの違いは、メタクラス__call__メソッドを定義することにより、クラスまたはサブクラスの__new__メソッドのいずれかが呼び出される機会を得る前に、それが呼び出されることを要求することです。

class MetaFoo(type):
    def __call__(cls,*args,**kwargs):
        print('MetaFoo: {c},{a},{k}'.format(c=cls,a=args,k=kwargs))

class Foo(object):
    __metaclass__=MetaFoo

class SubFoo(Foo):
    def __new__(self,*args,**kwargs):
        # This never gets called
        print('Foo.__new__: {a},{k}'.format(a=args,k=kwargs))

 sub=SubFoo()
 foo=Foo()

 # MetaFoo: <class '__main__.SubFoo'>, (),{}
 # MetaFoo: <class '__main__.Foo'>, (),{}

SubFoo.__new__が呼び出されないことに注意してください。対照的に、メタクラスなしでFoo.__new__を定義すると、サブクラスがFoo.__new__をオーバーライドできるようになります。

もちろん、MetaFoo.__call__を定義してcls.__new__を呼び出すこともできますが、それはあなた次第です。これを拒否することで、サブクラスが__new__メソッドを呼び出されないようにすることができます。

ここでメタクラスを使用することには説得力のある利点はありません。また、「シンプルは複雑よりも優れている」ので、__new__の使用をお勧めします。

15
unutbu

肉付けされたPython 3バージョンのパイロスコープの答えは、誰かがコピー、貼り付け、ハッキングするのに便利かもしれないと思った この記事 から取られます:

class Meta(type):

     @classmethod
     def __prepare__(mcs, name, bases, **kwargs):
         print('  Meta.__prepare__(mcs=%s, name=%r, bases=%s, **%s)' % (
             mcs, name, bases, kwargs
         ))
         return {}

     def __new__(mcs, name, bases, attrs, **kwargs):
         print('  Meta.__new__(mcs=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
             mcs, name, bases, ', '.join(attrs), kwargs
         ))
         return super().__new__(mcs, name, bases, attrs)

     def __init__(cls, name, bases, attrs, **kwargs):
         print('  Meta.__init__(cls=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
             cls, name, bases, ', '.join(attrs), kwargs
         ))
         super().__init__(name, bases, attrs)

     def __call__(cls, *args, **kwargs):
         print('  Meta.__call__(cls=%s, args=%s, kwargs=%s)' % (
             cls, args, kwargs
         ))
         return super().__call__(*args, **kwargs)

print('** Meta class declared')

class Class(metaclass=Meta, extra=1):

     def __new__(cls, myarg):
         print('  Class.__new__(cls=%s, myarg=%s)' % (
             cls, myarg
         ))
         return super().__new__(cls)

     def __init__(self, myarg):
         print('  Class.__init__(self=%s, myarg=%s)' % (
             self, myarg
         ))
         self.myarg = myarg
         super().__init__()

     def __str__(self):
         return "<instance of Class; myargs=%s>" % (
             getattr(self, 'myarg', 'MISSING'),
         )

print('** Class declared')

Class(1)
print('** Class instantiated')

出力:

** Meta class declared
  Meta.__prepare__(mcs=<class '__main__.Meta'>, name='Class', bases=(), **{'extra': 1})
  Meta.__new__(mcs=<class '__main__.Meta'>, name='Class', bases=(), attrs=[__module__, __qualname__, __new__, __init__, __str__, __classcell__], **{'extra': 1})
  Meta.__init__(cls=<class '__main__.Class'>, name='Class', bases=(), attrs=[__module__, __qualname__, __new__, __init__, __str__, __classcell__], **{'extra': 1})
** Class declared
  Meta.__call__(cls=<class '__main__.Class'>, args=(1,), kwargs={})
  Class.__new__(cls=<class '__main__.Class'>, myarg=1)
  Class.__init__(self=<instance of Class; myargs=MISSING>, myarg=1)
** Class instantiated

同じ記事で強調されているもう1つの優れたリソースは、David BeazleyのPyCon 2013 Python 3メタプログラミングチュートリアル です。

3
Chris

これは、ライフサイクルのフェーズとアクセスできる内容の問題です。 __call__が呼び出されますafter__new__および初期化パラメーターが渡されますbefore__init__に渡されるため、それらを操作できます。このコードを試して、その出力を調べてください。

class Meta(type):
    def __new__(cls, name, bases, newattrs):
        print "new: %r %r %r %r" % (cls, name, bases, newattrs,)
        return super(Meta, cls).__new__(cls, name, bases, newattrs)

    def __call__(self, *args, **kw):
        print "call: %r %r %r" % (self, args, kw)
        return super(Meta, self).__call__(*args, **kw)

class Foo:
    __metaclass__ = Meta

    def __init__(self, *args, **kw):
        print "init: %r %r %r" % (self, args, kw)

f = Foo('bar')
print "main: %r" % f
1
pyroscope