Метаклассы в 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 ответа:
Блок класса-это примерно синтаксический сахар для построения словаря, а затем вызов метакласса для построения объекта класса.
Это:
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
Некоторые примечания:
__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__
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, вы не можете проверить дублированные методы, как это предлагается в другом вопросе. Единственный практический способ сделать это-проанализировать исходный код.