2017-01-27 3 views
2

Ich schreibe einen benutzerdefinierten Typ Foo und ich möchte folgendes erreichen: wennKlassenname als idempotent Funktion typisieren

foo = Foo(bar) 

dann schreiben, wenn bar bereits eine Instanz von Foo, dann Foo(foo) zurückgibt, Objekt unmodifiziert, so beziehen sich foo und bar auf dasselbe Objekt. Andernfalls sollte ein neues Objekt vom Typ Foo erstellt werden, unter Verwendung von Informationen aus bar in welcher Weise auch immer Foo.__init__ als angemessen erachtet wird.

Wie kann ich das tun?

Ich würde annehmen, dass Foo.__new__ der Schlüssel dazu ist. Es sollte ziemlich einfach sein, Foo.__new__ das vorhandene Objekt zurückzugeben, wenn die instanceof Überprüfung erfolgreich ist, und andernfalls super().__new__ aufrufen. Aber the documentation for __new__ schreibt dazu:

Wenn __new__() gibt eine Instanz von cls, dann wird die neue Instanz __init__() Methode wie __init__(self[, ...]) aufgerufen werden, wo Selbst ist die neue Instanz und die übrigen Argumente sind die gleichen wie waren übergeben an __new__().

In diesem Fall würde ich eine Instanz der angeforderten Klasse zurückgeben, wenn auch keine neue. Kann ich den Anruf auf __init__ irgendwie verhindern? Oder muss ich innerhalb von __init__ eine Überprüfung hinzufügen, um festzustellen, ob es für eine neu erstellte Instanz oder für eine bereits vorhandene Instanz aufgerufen wird? Letzteres klingt nach Code-Duplikation, die vermeidbar sein sollte.

+0

Ist es nicht erlaubt, Factory-Methoden zu verwenden, um dies zu erreichen? – Tagc

+0

@Tagc: Ich bin irgendwie gewöhnt, Konstruktornamen für Umwandlungen zu verwenden.Zum Beispiel geben 'float (x)', 'str (x)' oder 'tuple (x)' das gleiche Objekt zurück, wenn es bereits vom richtigen Typ ist. Ich benutze diese Notation ziemlich oft, also möchte ich es auch für meine eigenen Typen, um konsistent zu sein. Es ist also eine Frage des Stils, denke ich. Funktionell würde eine Fabrikmethode natürlich genauso gut funktionieren. – MvG

Antwort

6

IMHO sollten Sie direkt __new__ und __init__ verwenden. Der Test in init, um zu sehen, ob Sie ein neues Objekt init sollte oder haben bereits eine bestehende so einfach ist, dass es keine Code-Duplizierung ist und die zusätzliche Komplexität ist IMHO akzeptabel

class Foo: 
    def __new__(cls, obj): 
     if isinstance(obj, cls): 
      print("New: same object") 
      return obj 
     else: 
      print("New: create object") 
      return super(Foo, cls).__new__(cls) 
    def __init__(self, obj): 
     if self is obj: 
      print("init: same object") 
     else: 
      print("init: brand new object from ", obj) 
      # do the actual initialization 

Es gibt wie erwartet:

>>> foo = Foo("x") 
New: create object 
init: brand new object from x 
>>> bar = Foo(foo) 
New: same object 
init: same object 
>>> bar is foo 
True 
+0

Das 'Selbst ist arg'-Muster ist ordentlich, hatte nicht darüber nachgedacht. Ich schätze, dass ich am Ende noch eine Metaklasse schreiben werde, um diese Überprüfung automatisch in Konstruktoren abgeleiteter Klassen einzuführen. – MvG

0

eine Möglichkeit, dies zu erreichen, ist durch den erforderlichen Code in eine Metaklasse wie folgt bewegen:

class IdempotentCast(type): 
    """Metaclass to ensure that Foo(x) is x if isinstance(x, Foo)""" 
    def __new__(cls, name, bases, namespace, **kwds): 
     res = None 
     defineNew = all(i.__new__ is object.__new__ for i in bases) 
     if defineNew: 
      def n(cls, *args, **kwds): 
       if len(args) == 1 and isinstance(args[0], cls) and not kwds: 
        return args[0] 
       else: 
        return super(res, cls).__new__(cls) 
      namespace["__new__"] = n 
     realInit = namespace.get("__init__") 
     if realInit is not None or defineNew: 
      @functools.wraps(realInit) 
      def i(self, *args, **kwds): 
       if len(args) != 1 or args[0] is not self: 
        if realInit is None: 
         return super(res, self).__init__(*args, **kwds) 
        else: 
         return realInit(self, *args, **kwds) 
      namespace["__init__"] = i 
     res = type.__new__(cls, name, bases, namespace) 
     return res 

class Foo(metaclass=IdempotentCast): 
    ... 

das metaclass fügt eine __new__ Methode sei denn, es exis ts eine Basisklasse, die bereits eine __new__ Methode hinzugefügt hat. Für Klassenhierarchien, bei denen eine solche Klasse eine andere Klasse erweitert, wird die Methode __new__ nur einmal hinzugefügt. Außerdem wird der Konstruktor umgebrochen, um zu prüfen, ob das erste Argument identisch mit self ist (dank the answer by Serge Ballesta für den Hinweis auf diese einfache Überprüfung). Andernfalls wird der ursprüngliche Konstruktor oder der Basiskonstruktor aufgerufen, wenn kein Konstruktor definiert wurde.

Ziemlich viel Code, aber Sie brauchen das nur einmal und können es verwenden, um diese Semantik für so viele Typen einzuführen, wie Sie möchten. Wenn Sie dies nur für eine einzelne Klasse benötigen, können andere Antworten geeigneter sein.