2012-05-25 13 views
12

Tengo el siguiente código Python:Metaclases de Python: ¿Por qué no se pide __setattr__ para los atributos establecidos durante la definición de la clase?

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 

lo que habría esperado __setattr__ de la clase meta de ser llamado para ambos FOO y a. Sin embargo, no se llama en absoluto. Cuando asigno algo a Foo.whatever después de que se haya definido la clase, el método es llamado.

¿Cuál es la razón de este comportamiento y hay una manera de interceptar las asignaciones que ocurren durante la creación de la clase? Usar attrs en __new__ no funcionará porque me gustaría check if a method is being redefined.

+0

este problema exacto es la razón por la cual la sintaxis de metaclas ha cambiado en py3! – SingleNegationElimination

Respuesta

20

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:

  1. __prepare__ tiene que ser un classmethod o staticmethod, porque está siendo llamado ante º La instancia de metaclass '(tu clase) existe.
  2. 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
  3. 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.
  4. 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.

2

Los atributos de clase se pasan a la metaclase como un único diccionario y mi hipótesis es que esto se usa para actualizar el atributo __dict__ de la clase a la vez, p. algo así como cls.__dict__.update(dct) en lugar de setattr() en cada artículo. Más al punto, todo se maneja en C-land y simplemente no fue escrito para llamar a un __setattr__() personalizado.

Es bastante fácil hacer lo que quiera con los atributos de la clase en el método __init__() de su metaclass, ya que se pasa el espacio de nombres de clase como dict, así que haga eso.

7

No hay asignaciones durante la creación de la clase. O bien: están sucediendo, pero no en el contexto que crees que son. Todos los atributos de clase se recogen de alcance cuerpo de la clase y se pasan a metaclase __new__, como último argumento:

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 

Razón: cuando el código en el cuerpo de la clase ejecuta, no hay clase aún. Lo que significa que no hay oportunidad para que metaclass intercepte nada yet.

0

Durante la creación de la clase, su espacio de nombre se evalúa a un dict y se pasa como un argumento a la metaclase, junto con el nombre de clase y las clases base. Por eso, asignar un atributo de clase dentro de la definición de la clase no funcionaría de la manera esperada. No crea una clase vacía y asigna todo. Tampoco puede tener claves duplicadas en un dict, por lo que durante la creación de clases los atributos ya están desduplicados. Solo al establecer un atributo después de la definición de la clase puede activar su __setattr__ personalizado.

Como el espacio de nombres es un dict, no hay forma de que compruebe los métodos duplicados, como sugiere su otra pregunta. La única forma práctica de hacerlo es analizar el código fuente.

Cuestiones relacionadas