2012-05-25 8 views
12

Ich habe folgende Python-Code:Python-Metaklassen: Warum wird __setattr_ nicht für Attribute aufgerufen, die während der Klassendefinition gesetzt werden?

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 

I __setattr__ der Meta-Klasse erwartet hätte genannt werden sowohl für FOO und a. Es wird jedoch überhaupt nicht aufgerufen. Wenn ich nach der Definition der Klasse Foo.whatever etwas zuweisen, wird die Methode aufgerufen.

Was ist der Grund für dieses Verhalten und gibt es eine Möglichkeit, die Zuordnungen abzufangen, die während der Erstellung der Klasse auftreten? Mit attrs in __new__ wird nicht funktionieren, da ich gerne check if a method is being redefined.

+0

Dieses genaue Problem ist der Grund, warum metaclass Syntax in py3 geändert hat! – SingleNegationElimination

Antwort

20

Ein Klassenblock ist in etwa syntaktischer Zucker zum Erstellen eines Wörterbuchs und zum Aufrufen einer Metaklasse zum Erstellen des Klassenobjekts.

Dieses:

class Foo(object): 
    __metaclass__ = FooMeta 
    FOO = 123 
    def a(self): 
     pass 

Kommt ziemlich aus, als ob Sie geschrieben hatte:

d = {} 
d['__metaclass__'] = FooMeta 
d['FOO'] = 123 
def a(self): 
    pass 
d['a'] = a 
Foo = d.get('__metaclass__', type)('Foo', (object,), d) 

Nur ohne den Namensraum Umweltverschmutzung (und in Wirklichkeit gibt es auch eine Suche durch alle Basen zu bestimmen die Metaklasse, oder ob es einen Metaklassenkonflikt gibt, aber ich ignoriere das hier).

Die metaclass' __setattr__ kann kontrollieren, was passiert, wenn Sie versuchen, auf einer seiner Instanzen ein Attribut gesetzt (das Klassenobjekt), aber innerhalb der Klasse Block Sie nicht, das zu tun, sind Sie in ein Einfügen Wörterbuchobjekt, so dass die dict Klasse steuert, was vor sich geht, nicht Ihre Metaklasse. Du hast also kein Glück.


Es sei denn, Sie verwenden Python 3.x! In Python 3.x können Sie eine __prepare__ Klassenmethode (oder staticmethod) für eine Metaklasse definieren, die steuert, welches Objekt zum Sammeln von Attributen verwendet wird, die in einem Klassenblock gesetzt sind, bevor sie an den Metaklassenkonstruktor übergeben werden. Der Standard __prepare__ gibt einfach ein normales Wörterbuch, aber man konnte eine benutzerdefinierte dict-ähnliche Klasse bauen, die keine Schlüssel erlauben neu definiert werden, und dass Ihre Attribute zu akkumulieren verwenden:

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 

des Lauf gibt mir:

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 

Einige Anmerkungen:

  1. __prepare__ hat eine classmethod oder staticmethod, sein, weil es vor th genannt zu werden ist Die Metaklasseninstanz (Ihre Klasse) existiert.
  2. type noch braucht, um seine dritte Parameter eine echte dict zu sein, so haben Sie eine __new__ Methode haben, der die SingleAssignDict zu einem normalen
  3. wandelt ich dict subclassed hätte, die wahrscheinlich vermieden hätte (2), aber Ich mag das wirklich nicht, weil die nicht-grundlegenden Methoden wie update Ihre Überschreibungen der grundlegenden Methoden wie __setitem__ nicht respektieren. Daher bevorzuge ich die Unterklasse collections.MutableMapping und wickle ein Wörterbuch ein.
  4. Das eigentliche Okay.__dict__ Objekt ist ein normales Wörterbuch, weil es von type festgelegt wurde und type ist knifflig über die Art von Wörterbuch, das es will. Dies bedeutet, dass das Überschreiben von Klassenattributen nach der Klassenerstellung keine Ausnahme auslöst. Sie können das Attribut nach dem Oberklassenaufruf in __new__ überschreiben, wenn Sie das vom Klassenobjekt erzwungene no-overwriting beibehalten möchten.

Leider ist diese Technik in Python 2.x nicht verfügbar (I geprüft). Die Methode __prepare__ wird nicht aufgerufen, was sinnvoll ist, da in Python 2.x die Metaklasse durch das magische Attribut __metaclass__ und nicht durch ein spezielles Schlüsselwort im Klassenblock bestimmt wird. Das bedeutet, dass das dict-Objekt, das zum Sammeln von Attributen für den Klassenblock verwendet wird, bereits existiert, wenn die Metaklasse bekannt ist.

Vergleichen Python 2:

class Foo(object): 
    __metaclass__ = FooMeta 
    FOO = 123 
    def a(self): 
     pass 

Sein entspricht in etwa:

d = {} 
d['__metaclass__'] = FooMeta 
d['FOO'] = 123 
def a(self): 
    pass 
d['a'] = a 
Foo = d.get('__metaclass__', type)('Foo', (object,), d) 

Wo die Metaklasse wird aus dem Wörterbuch bestimmt aufzurufen, im Vergleich zu Python 3:

class Foo(metaclass=FooMeta): 
    FOO = 123 
    def a(self): 
     pass 

Being grob entspricht:

d = FooMeta.__prepare__('Foo',()) 
d['Foo'] = 123 
def a(self): 
    pass 
d['a'] = a 
Foo = FooMeta('Foo',(), d) 

Wo das zu verwendende Wörterbuch aus der Metaklasse ermittelt wird.

2

Klassenattribute werden an die Metaklasse als einzelnes Wörterbuch weitergegeben, und meine Hypothese ist, dass dies verwendet wird, um das Attribut der Klasse auf einmal zu aktualisieren, z. etwas wie cls.__dict__.update(dct) statt setattr() auf jedes Element zu tun. Mehr auf den Punkt, es ist alles in C-land behandelt und wurde einfach nicht geschrieben, um eine benutzerdefinierte __setattr__() anrufen.

Es ist einfach genug, die Attribute der Klasse in der Metaklasse __init__() mit den gewünschten Eigenschaften zu bearbeiten, da Sie den Klassennamespace als dict übergeben, also tun Sie das einfach.

7

Es gibt keine Zuweisungen während der Erstellung der Klasse. Oder: Sie passieren, aber nicht in dem Kontext, in dem du denkst, dass sie es sind. Alle Klassenattribute werden von der Klasse Körper Umfang gesammelt und metaclass' __new__, als das letzte Argument übergeben:

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 

Grund: wenn der Code in der Klasse Körper ausführt, gibt es noch keine Klasse. Was bedeutet, dass Metaklasse keine Möglichkeit hat, irgendetwas abzufangen noch.

0

Während der Klassenerstellung wird Ihr Namespace nach einem dict ausgewertet und als Argument an die Metaklasse übergeben, zusammen mit dem Klassennamen und den Basisklassen. Aus diesem Grund funktioniert die Zuweisung eines Klassenattributs innerhalb der Klassendefinition nicht wie erwartet. Es erstellt keine leere Klasse und weist alles zu. Sie können in einem Diktat auch keine doppelten Schlüssel haben, daher werden Attribute während der Klassenerstellung bereits dedupliziert. Nur wenn Sie ein Attribut nach der Klassendefinition setzen, können Sie Ihre benutzerdefinierte __setattr__ auslösen.

Da es sich bei dem Namespace um ein Diktat handelt, können Sie doppelte Methoden nicht überprüfen, wie von Ihrer anderen Frage vorgeschlagen. Der einzige praktische Weg ist das Parsen des Quellcodes.