Подтвердить что ты не робот

Python metaclasses: Почему не __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__ не будет работать, так как я хотел бы проверить, переопределяется ли метод.

4b9b3361

Ответ 1

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

Это:

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.x! В Python 3.x вы можете определить метод __prepare__ classmethod (или staticmethod) в метаклассе, который контролирует, какой объект используется для накопления атрибутов, установленных в блоке класса, прежде чем они будут переданы конструктору метакласса. По умолчанию __prepare__ просто возвращает нормальный словарь, но вы можете создать пользовательский диктоподобный класс, который не позволяет переопределять ключи и использовать их для накопления ваших атрибутов:

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

Некоторые примечания:

  • __prepare__ должен быть classmethod или staticmethod, потому что он вызывается перед экземпляром метакласса (ваш класс).
  • type все еще нуждается в том, чтобы его третий параметр был реальным dict, поэтому у вас должен быть метод __new__, который преобразует SingleAssignDict в обычный
  • Я мог бы подклассифицировать dict, который, вероятно, избежал бы (2), но мне очень не нравится это делать из-за того, как неосновные методы, такие как update, не уважают ваши переопределения основных методов, таких как __setitem__. Поэтому я предпочитаю подкласс collections.MutableMapping и завершать словарь.
  • Фактический Okay.__dict__ объект является нормальным словарем, потому что он был установлен type и type является тонким в отношении того типа словаря, который он хочет. Это означает, что переписывание атрибутов класса после создания класса не вызывает исключения. Вы можете перезаписать атрибут __dict__ после вызова суперкласса в __new__, если вы хотите сохранить отмену без перезаписи с помощью словаря класса объектов.

К сожалению, этот метод недоступен в Python 2.x(я проверил). Метод __prepare__ не вызывается, что имеет смысл, так как в Python 2.x метакласс определяется магическим атрибутом __metaclass__, а не специальным ключевым словом в 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)

Если словарь для использования определяется из метакласса.

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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