Метаклассы в Python: почему не setattr называется набор атрибутов в определение класса?


У меня есть следующий код 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__ вызова мета-класса для обоих FOO и a. Впрочем, это и не называется вовсе. Когда я назначаю что-то Foo.whatever После определения класса, вызывается метод .

В чем причина такого поведения и есть ли способ перехватить задания, которые происходят во время создания класса? Использование attrs в __new__ не будет работать, так как я хотел бы проверить, является ли метод пересматриваются.

4 12

4 ответа:

Блок класса-это примерно синтаксический сахар для построения словаря, а затем вызов метакласса для построения объекта класса.

Это:

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__ может контролировать то, что происходит, когда вы попробуйте установить атрибут на один из его экземпляров (объект класса), но внутри блока класса вы этого не делаете, вы вставляете в объект словаря, поэтому класс dict управляет тем, что происходит, а не вашим метаклассом. Так что тебе не повезло.


Если только вы не используете Python 3.х! В 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 все еще нуждается в том, чтобы его третий параметр был реальным dict, поэтому у вас должен быть метод __new__, который преобразует SingleAssignDict в нормальный
  3. Я мог бы подклассировать dict, что, вероятно, позволило бы избежать (2), но мне очень не нравится делать это из-за того, что неосновные методы, такие как update, не уважают ваши переопределения основных методов, таких как __setitem__. Поэтому я предпочитаю подкласс collections.MutableMapping и завернуть словарь.
  4. фактический Okay.__dict__ объект является нормальным словарем, потому что он был задан type и type привередливо относится к виду словарь ей нужен. Это означает, что перезапись атрибутов класса после его создания не вызывает исключения. Вы можете перезаписать атрибут __dict__ после вызова суперкласса в __new__, Если хотите сохранить принудительную отмену перезаписи словарем объекта класса.

К сожалению, этот метод недоступен в Python 2.x (я проверил). Метод __prepare__ не вызывается, что имеет смысл, как и в Python 2.x метакласс определяется скорее атрибутом __metaclass__ magic чем специальное ключевое слово в classblock; это означает, что объект dict, используемый для накопления атрибутов для блока класса, уже существует ко времени, когда метакласс известен.

Сравните Python 2:

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)

Где метакласс для вызова определяется из словаря, в отличие от Python 3:

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)

Где используемый словарь определяется из метакласса.

Во время создания класса не происходит никаких назначений. Или: они происходят, но не в том контексте, в котором вы их себе представляете. Все атрибуты класса собираются из области тела класса и передаются в metaclass ' __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

Причина: когда код в теле класса выполняется, класса еще нет. Это означает, что у метакласса пока нет возможности перехватить что-либо .

Атрибуты класса передаются в метакласс как единый словарь, и моя гипотеза заключается в том, что это используется для обновления атрибута __dict__ класса все сразу, например, что-то вроде cls.__dict__.update(dct), а не делать setattr() для каждого элемента. Более того, все это обрабатывается в C-land и просто не было написано, чтобы вызвать обычай __setattr__().

Достаточно легко сделать все, что вы хотите, с атрибутами класса в методе вашего метакласса __init__(), так как вы передаете пространство имен класса как dict, так что просто сделайте это.

Во время создания класса пространство имен вычисляется в dict и передается в качестве аргумента метаклассу вместе с именем класса и базовыми классами. Из-за этого назначение атрибута класса внутри определения класса не будет работать так, как вы ожидаете. Он не создает пустой класс и не присваивает все подряд. Вы также не можете иметь дублированные ключи в dict, поэтому во время создания класса атрибуты уже дедуплицируются. Только установив атрибут после определения класса, вы можете Запустите свой пользовательский _ _ setattr__.

Поскольку пространство имен является dict, вы не можете проверить дублированные методы, как это предлагается в другом вопросе. Единственный практический способ сделать это-проанализировать исходный код.