17

私は次のPythonコードを持っています:

class FooMeta(type):
    def __setattr__(self, name, value):
        print name, value
        return super(FooMeta, self).__setattr__(name, value)

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

との両方で__setattr__メタクラスが呼び出されることを期待していました。ただし、まったく呼び出されません。クラスが定義された後に何かを割り当てると、メソッド呼び出されます。FOOaFoo.whatever

この動作の理由は何ですか?クラスの作成中に発生する割り当てをインターセプトする方法はありますか?メソッドが再定義されているかどうかを確認したいので、inを使用しても機能attrsしません。__new__

4

4 に答える 4

27

クラスブロックは、辞書を作成し、メタクラスを呼び出してクラスオブジェクトを作成するための大まかな構文糖衣です。

これ:

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

あなたが書いたかのようにほとんど出てきます:

d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)

名前空間の汚染がない場合のみ(実際には、メタクラスを判別するためにすべてのベースを検索することもありますが、メタクラスの競合があるかどうかもわかりませんが、ここでは無視しています)。

メタクラス__setattr__は、そのインスタンスの1つ(クラスオブジェクト)に属性を設定しようとしたときに何が起こるかを制御できますが、それを行わないクラスブロック内では、ディクショナリオブジェクトに挿入しているため、dictクラスは制御しますメタクラスではなく、何が起こっているのか。だからあなたは運が悪い。


Python 3.xを使用している場合を除きます!Python 3.xでは__prepare__、メタクラスにclassmethod(またはstaticmethod)を定義できます。これは、メタクラスコンストラクターに渡される前に、クラスブロック内に設定された属性を累積するために使用されるオブジェクトを制御します。デフォルト__prepare__では通常の辞書が返されるだけですが、キーの再定義を許可しないカスタムのdictのようなクラスを作成し、それを使用して属性を蓄積することができます。

from collections import MutableMapping


class SingleAssignDict(MutableMapping):
    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)

    def __getitem__(self, key):
        return self._d[key]

    def __setitem__(self, key, value):
        if key in self._d:
            raise ValueError(
                'Key {!r} already exists in SingleAssignDict'.format(key)
            )
        else:
            self._d[key] = value

    def __delitem__(self, key):
        del self._d[key]

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __contains__(self, key):
        return key in self._d

    def __repr__(self):
        return '{}({!r})'.format(type(self).__name__, self._d)


class RedefBlocker(type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        return SingleAssignDict()

    def __new__(metacls, name, bases, sad):
        return super().__new__(metacls, name, bases, dict(sad))


class Okay(metaclass=RedefBlocker):
    a = 1
    b = 2


class Boom(metaclass=RedefBlocker):
    a = 1
    b = 2
    a = 3

これを実行すると、次のようになります。

Traceback (most recent call last):
  File "/tmp/redef.py", line 50, in <module>
    class Boom(metaclass=RedefBlocker):
  File "/tmp/redef.py", line 53, in Boom
    a = 3
  File "/tmp/redef.py", line 15, in __setitem__
    'Key {!r} already exists in SingleAssignDict'.format(key)
ValueError: Key 'a' already exists in SingleAssignDict

いくつかのメモ:

  1. __prepare__メタクラスのインスタンス(クラス)が存在する前に呼び出されるため、classmethodまたはである必要があります。staticmethod
  2. typeまだ3番目のパラメータが実数dictである必要があるため、を通常のパラメータに__new__変換するメソッドが必要です。SingleAssignDict
  3. サブクラス化することもできますが、おそらく(2)を回避できたでしょうが、のような非基本的なメソッドが、のような基本的なメソッドのオーバーライドを尊重しないdictため、これを行うのは本当に嫌いです。だから私は辞書をサブクラス化してラップすることを好みます。update__setitem__collections.MutableMapping
  4. 実際のOkay.__dict__オブジェクトは通常の辞書です。これは、必要な辞書の種類によって設定され、気難しいためtypeですtype。これは、クラスの作成後にクラス属性を上書きしても例外が発生しないことを意味します。クラスオブジェクトのディクショナリによって強制的に上書きされないようにしたい場合__dict__は、スーパークラスの呼び出し後に属性を上書きできます。__new__

残念ながら、この手法はPython 2.xでは使用できません(チェックしました)。この__prepare__メソッドは呼び出されません。これは、Python 2.xの場合と同様に、メタクラスは__metaclass__クラスブロック内の特別なキーワードではなく、magic属性によって決定されます。これは、クラスブロックの属性を蓄積するために使用されるdictオブジェクトが、メタクラスが認識されるまでにすでに存在していることを意味します。

Python2と比較してください。

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

ほぼ同等であること:

d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)

呼び出すメタクラスが辞書から決定される場合とPython3の場合。

class Foo(metaclass=FooMeta):
    FOO = 123
    def a(self):
        pass

ほぼ同等であること:

d = FooMeta.__prepare__('Foo', ())
d['Foo'] = 123
def a(self):
    pass
d['a'] = a
Foo = FooMeta('Foo', (), d)

使用する辞書がメタクラスから決定される場所。

于 2012-05-26T02:57:30.880 に答える
7

クラスの作成中に割り当てが発生することはありません。または:それらは起こっていますが、あなたが思っている文脈ではありません。__new__すべてのクラス属性は、クラス本体スコープから収集され、最後の引数としてメタクラス'に渡されます。

class FooMeta(type):
    def __new__(self, name, bases, attrs):
        print attrs
        return type.__new__(self, name, bases, attrs)

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123

理由:クラス本体のコードが実行されるとき、まだクラスがありません。つまり、メタクラスがまだ何かをインターセプトする機会がないということです。

于 2012-05-25T22:47:38.330 に答える
2

クラス属性は単一のディクショナリとしてメタクラスに渡されます。私の仮説では、これはクラスの属性を一度に更新するために使用されます。たとえば、各アイテムに対して行うのではなく、__dict__次のよう になります。さらに重要なことに、それはすべてCランドで処理され、カスタムを呼び出すように作成されたものではありません。cls.__dict__.update(dct)setattr()__setattr__()

__init__()クラスの名前空間がとして渡されるので、メタクラスのメソッドのクラスの属性に対して必要なことを行うのは簡単ですdict。そうするだけです。

于 2012-05-25T22:47:19.657 に答える
0

クラスの作成中に、名前空間はdictに評価され、クラス名と基本クラスとともに、メタクラスに引数として渡されます。そのため、クラス定義内でクラス属性を割り当てると、期待どおりに機能しません。空のクラスを作成してすべてを割り当てるわけではありません。また、dictに重複するキーを含めることはできないため、クラスの作成中に属性はすでに重複排除されています。クラス定義の後に属性を設定することによってのみ、カスタム__setattr__をトリガーできます。

名前空間はdictであるため、他の質問で示唆されているように、重複したメソッドをチェックする方法はありません。そのための唯一の実用的な方法は、ソースコードを解析することです。

于 2012-05-26T01:25:52.310 に答える