2017-11-23 3 views
1

Ich frage mich, wie eine Metaklasse in Python erstellen, die anderen Klassen erstellen kann, die:Wie erstellt man eine Metaklasse, die einer Klasse ein Array von Instanzen geben und eine "Voodoo" -Instanz bereitstellen kann, die auf alle Klasseninstanzen wirkt?

  • Shop ihre Instanzen in einem Array automatisch
  • eine besondere Instanz, NonMetaClass.all, deren Eigenschaften:
    • Wenn diese Option aktiviert ist, setzen Sie alle Klasseninstanzen mit dem gleichen Schlüssel auf denselben Wert (z. B. Foo.all.num = 3. Alle Instanzen von Foo haben eine num von 3)
    • Bei Zugriff (get), gibt ein Array aller Schlüsselwerte der Klasseninstanzen zurück (z. B. Foo.all.num gibt [5, 3, 2] zurück)
    • Kann nicht gelöscht werden.
    • Wenn aufgerufen (wenn das Attribut eine Funktion ist), rufen Sie diese Methode für alle Instanzen einer Klasse auf.

In Python Bedingungen würde Ich mag eine Klasse machen, der wie folgt lautet:

class Foo(object): 
    BAR = 23 
    def __init__(self): 
     self.a = 5 

    def pointless(): 
     print 'pointless.' 

    def change_a(self): 
     self.a = 52 

In diesem:

class Foo(object): 
    BAR = 23 
    instances = [] 
    all = # Some black magic to create the special "all" instance 
    def __init__(self): 
     self.a = 5 
     Foo.instances.append(self) 

    def pointless(self): 
     print 'pointless.' 

    def change_a(self): 
     self.a = 52 

Und in der Lage sein, es so zu verwenden, :

>>> Foo() 
>>> Foo.instances[0] 
<__main__.Foo instance at 0x102ff5758> 
>>> Foo() 
>>> len(Foo.instances) 
2 
>>> Foo.all.a = 78 
78 
>>> Foo.all.a 
[78, 78] 
>>> Foo.all.change_a() 
>>> Foo.all.a 
[52, 52] 
>>> 
+0

Sorgfalt zu füllen, wie dies verhalten soll, wenn Subklassen „Foo“? Sollen die Unterklassen auch Teil von Foo's Instanzen sein oder für jede Unterklasse ein leeres Instace-Array? – jsbueno

+1

So ist die einfachste Sache, dass jede Unterklasse ihr eigenes "Instanzen" -Attribut hat, und das ist es, was die Antwort, die ich gepostet habe, tut. – jsbueno

+0

Ja, das ist das Verhalten, das ich wollte, danke. Sobald ich Zugriff auf einen Computer habe (ich benutze gerade mein Telefon), werde ich deine Antwort annehmen und sie annehmen, um das Programm zu testen. –

Antwort

0

Ok, habe ich herausgefunden, wie man Mach das für Python 2.7 alleine. Dies ist, was ich glaube, die beste Lösung, obwohl es nicht die einzige sein kann. Es ermöglicht Ihnen, die Attribute Class.all zu setzen, abzurufen und aufzurufen. Ich habe die Metaklasse InstanceUnifier genannt, aber bitte kommentieren Sie, wenn Sie denken, dass es einen besseren (kürzeren, beschreibenden) Namen gibt, den Sie sich vorstellen können.

class InstanceUnifier(type): 
    ''' 
     What we want: A metaclass that can give a class an array of instances and provide a static Class.all object, that, when a method is called on it, calls the same method on every instance of the class. 
    ''' 
    def __new__(cls, name, base_classes, dct): 
     dct['all'] = None 
     dct['instances'] = [] 
     return type.__new__(cls, name, base_classes, dct) 
    def __init__(cls, name, base_classes, dct): 
     class Accessor(object): 
      def __getattribute__(self, name): 
       array = [getattr(inst, name) for inst in cls.instances] 
       if all([callable(item) for item in array]): 
        def proxy_func(*args, **kwargs): 
         for i in range(len(cls.instances)): 
          this = cls.instances[i] 
          func = array[i] 
          func(*args, **kwargs) 
        return proxy_func 
       elif all([not callable(item) for item in array]): 
        return array 
       else: 
        raise RuntimeError('Some objects in class instance array for key "'+name+'" are callable, some are not.') 
      def __setattr__(self, name, value): 
       [setattr(inst, name, value) for inst in cls.instances] 
      def __delattr__(self, name): 
       [delattr(inst, name) for inst in cls.instances] 
     cls.all = Accessor() 
     return type.__init__(cls, name, base_classes, dct) 

    def __call__(cls, *args, **kwargs): 
     inst = type.__call__(cls, *args, **kwargs) 
     cls.instances.append(inst) 
     return inst 
0

Das einzige, was für eine Metaklasse benötigt wird, ist eigentlich ganz einfach: genau das Erstellen der intances und all Attribute.

Alles, was Sie tun müssen, ist, diese in den Namensraum einzufügen. Ah, es muss auch die Klasse __new__ Methode umhüllen, um neue Instanzen in die instances Liste einzufügen.

Der Teil, der das gewünschte Verhalten von all ist, ist interessant, und das kann mit dem Deskriptor-Protokoll implementiert werden, und Attribut Zugriffskontrolle, so müssen wir ein paar spezielle Klassen, die die entsprechenden Objekte zurückgeben, wenn angefordert nach das ".".

"All" ist die Klasse, die als "alle" instanziiert wird - es benötigt nur eine -Methode, um ein anderes spezielles Objekt aus der AllAttr-Klasse zurückzugeben, das bereits an die Elternklasse gebunden ist.

"AllAttr" ist ein spezielles Objekt, das bei jedem Attributzugriff Ihre Anforderungen an die Mitglieder des Attributs "instance" der Eigentümerklasse erfüllt.

Und "CallAllList" ist eine spezielle Liste Unterklasse, die aufrufbar ist, und ruft alle seine Mitglieder nacheinander. Es wird von AllAttr verwendet, wenn das erforderliche Attribut der Besitzerklasse selbst aufrufbar ist.

class CallAllList(list): 
    def __call__(self, *args, **kwargs): 
     return [instance(*args, **kwargs) for instance in self] 


class AllAttr(object): 
    def __init__(self, owner): 
     self._owner = owner 

    def __getattr__(self, attr): 
     method = getattr(self._owner, attr, None) 
     cls = CallAllList if callable(method) else list 
     return cls(getattr(obj, attr) for obj in self._owner.instances) 

    def __setattr__(self, attr, value): 
     if attr == "_owner": 
      return super(AllAttr, self).__setattr__(attr, value) 
     for obj in self._owner.instances: 
      setattr(obj, attr, value) 


class All(object): 
    def __get__(self, instance, owner): 
     return AllAttr(owner) 

    def __repr__(self): 
     return "Representation of all instances of '{}'".format(self.__class__.__name__) 


class MetaAll(type): 
    def __new__(metacls, name, bases, namespace): 
     namespace["all"] = All() 
     namespace["instances"] = [] 
     cls = super(MetaAll, metacls).__new__(metacls, name, bases, namespace) 
     original_new = getattr(cls, "__new__") 
     def __new__(cls, *args, **kwargs): 
      instance = original_new(cls, *args, **kwargs) 
      cls.instances.append(instance) 
      return instance 
     cls.__new__ = __new__ 
     return cls 


class Foo(metaclass=MetaAll): 
    pass 

Der obige Code ist so geschrieben, dass es Python 3 und Python 2 kompatibel ist, da man immer noch zu sein scheint am Beispiel Python2 gegeben Ihres „Drucks“. Die einzige Sache, die nicht kompatibel mit beiden Formen geschrieben werden kann, ist die Metaklasse mit Deklaration selbst - einfach eine __metaclass__ = MetaAll im Körper der Foo-Klasse deklarieren, wenn Sie Python 2 verwenden. Aber Sie sollten nicht wirklich Python2 verwenden, nur zu Python wechseln 3 sobald du kannst.

Update

Es kommt vor, dass Python 2 die „ungebundene Methode“ Figur hat, und das spezielle Gehäuse von __new__ wie 3 in Python nicht funktioniert: Sie haben eine Funktion __new__ auf die Klasse mit dem Namen nicht nur Attribut können. Um die korrekte __new__ Methode aus den Oberklassen zu erhalten, ist der einfachste Weg, eine wegwerfbare Klasse zu erstellen, so dass sie linear gesucht werden kann. Andernfalls müsste man den MRO-Algorithmus neu implementieren, um die richtige __new__ Methode zu erhalten.

Also, für Python 2, sollte die Metaklasse sein:

class MetaAll(type): 
    def __new__(metacls, name, bases, namespace): 
     namespace["all"] = All() 
     namespace["instances"] = [] 
     if "__new__" in namespace: 
      original_new = namespace["__new__"] 
      def __new__(cls, *args, **kwargs): 
       instance = original_new(cls, *args, **kwargs) 
       cls.instances.append(instance) 
       return instance 
     else: 
      # We create a disposable class just to get the '__mro__' 
      stub_cls = super(MetaAll, metacls).__new__(metacls, name, bases, {}) 
      for parent in stub_cls.__mro__[1:]: 
       if "__new__" in parent.__dict__: 
        original_new = parent.__dict__["__new__"] 
        break 

      def __new__(cls, *args, **kwargs): 
       instance = original_new(cls, *args, **kwargs) 
       cls.instances.append(instance) 
       return instance 
     namespace["__new__"] = __new__ 
     final_cls = super(MetaAll, metacls).__new__(metacls, name, bases, namespace) 

     return final_cls 


class Foo(object): 
    __metaclass__ = MetaAll 

(. Jetzt wieder dieses Ding ist alt Just for Python siedeln 3.6)

+0

Haben Sie Ihren Code getestet? Ich habe gerade und bekam den Fehler 'TypeError: ungebundene Methode __new __() muss mit Foo-Instanz als erstes Argument aufgerufen werden (bekam stattdessen MetaAll-Instanz)' –

+0

Ich testete es in Python 3. Python 3 hat keine "ungebundene Methode" - Das ist eine Sache von Python 2. Also - kannst du zu Python 3 wechseln? Dies erfordert einige Änderungen, da Python 2 das '__new__' nicht wie oben beschrieben als Spezialfall-Klassenmethode erkennt. – jsbueno

+0

Danke für die Bearbeitung! Ich werde kurz von meinem Telefon und wieder zurück an einem regulären (Python-installierten) Computer sein, um dies erneut zu testen. –

Verwandte Themen