Un bloque de clase es aproximadamente un azúcar sintáctico para construir un diccionario, y luego invoca una metaclase para construir el objeto de clase.
Este:
class Foo(object):
__metaclass__ = FooMeta
FOO = 123
def a(self):
pass
sale más o menos como si hubieras escrito:
d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)
Sólo sin la contaminación del espacio de nombres (y, en realidad, también hay una búsqueda a través de todas las bases para determinar la metaclase, o si hay un conflicto de metaclase, pero ignoro eso aquí).
La metaclase __setattr__
puede controlar lo que sucede cuando se intenta establecer un atributo en una de sus instancias (la clase de objeto), pero dentro del bloque de clases no está haciendo eso, vas a insertar en una objeto de diccionario, por lo que la clase dict
controla lo que sucede, no su metaclase. Entonces no tienes suerte.
¡A menos que esté utilizando Python 3.x! En Python 3.x puede definir un método de clase __prepare__
(o método estático) en una metaclase, que controla qué objeto se usa para acumular atributos establecidos dentro de un bloque de clases antes de pasarlos al constructor de la metaclase. El valor por defecto __prepare__
simplemente devuelve un diccionario normal, pero se podría construir una clase dict-como la costumbre que no permite que las llaves para ser redefinidos, y el uso que de acumular sus atributos:
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
Operando esto me da:
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
Algunas notas:
__prepare__
tiene que ser un classmethod
o staticmethod
, porque está siendo llamado ante º La instancia de metaclass '(tu clase) existe.
type
todavía necesita su tercer parámetro a ser un verdadero dict
, por lo que tiene que tener un método __new__
que convierte la SingleAssignDict
a uno normal
- que podría haber una subclase
dict
, que probablemente habría evitado (2), pero Realmente no me gusta hacer eso debido a que los métodos no básicos como update
no respetan las anulaciones de los métodos básicos como __setitem__
. Por lo tanto, prefiero la subclase collections.MutableMapping
y ajustar un diccionario.
- El objeto real
Okay.__dict__
es un diccionario normal, porque fue establecido por type
y type
es quisquilloso sobre el tipo de diccionario que desea. Esto significa que sobrescribir los atributos de clase después de la creación de clase no genera una excepción. Puede sobrescribir el atributo __dict__
después de la llamada a la superclase en __new__
si desea mantener la no sobrescritura forzada por el diccionario del objeto de clase.
Por desgracia, esta técnica no está disponible en Python 2.x (he comprobado). El método __prepare__
no se invoca, lo cual tiene sentido ya que en Python 2.x la metaclase está determinada por el atributo mágico __metaclass__
en lugar de una palabra clave especial en el bloque de clases; lo que significa que el objeto dict utilizado para acumular atributos para el bloque de clase ya existe en el momento en que se conoce la metaclase.
Compare Python 2:
class Foo(object):
__metaclass__ = FooMeta
FOO = 123
def a(self):
pass
ser más o menos equivalente a:
d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)
Cuando la metaclase para invocar se determina a partir del diccionario, en comparación con Python 3:
class Foo(metaclass=FooMeta):
FOO = 123
def a(self):
pass
Ser aproximadamente equivalente a:
d = FooMeta.__prepare__('Foo',())
d['Foo'] = 123
def a(self):
pass
d['a'] = a
Foo = FooMeta('Foo',(), d)
Donde el diccionario a utilizar se determina a partir de la metaclase.
este problema exacto es la razón por la cual la sintaxis de metaclas ha cambiado en py3! – SingleNegationElimination