Klassenfabrik in Python

70

Ich bin neu in Python und benötige einige Ratschläge zur Implementierung des folgenden Szenarios.

Ich habe zwei Klassen für die Verwaltung von Domains bei zwei verschiedenen Registraren. Beide haben die gleiche Schnittstelle, z

class RegistrarA(Object):
    def __init__(self, domain):
        self.domain = domain

    def lookup(self):
        ...

    def register(self, info):
        ...

und

class RegistrarB(object):
    def __init__(self, domain):
        self.domain = domain

    def lookup(self):
        ...

    def register(self, info):
        ...

Ich möchte eine Domain-Klasse erstellen, die bei gegebenem Domain-Namen die richtige Registrar-Klasse basierend auf der Erweiterung lädt, z

com = Domain('test.com') #load RegistrarA
com.lookup()

biz = Domain('test.biz') #load RegistrarB
biz.lookup()

Ich weiß, dass dies mit einer Factory-Funktion erreicht werden kann (siehe unten), aber ist dies der beste Weg, dies zu tun, oder gibt es einen besseren Weg, OOP-Funktionen zu verwenden?

def factory(domain):
  if ...:
    return RegistrarA(domain)
  else:
    return RegistrarB(domain)
Schmied
quelle

Antworten:

82

Ich denke, die Verwendung einer Funktion ist in Ordnung.

Die interessantere Frage ist, wie Sie bestimmen, welcher Registrar geladen werden soll. Eine Option besteht darin, eine abstrakte Basis-Registrar-Klasse zu haben, die konkrete Implementierungen unterklassifiziert und dann über den __subclasses__()Aufruf einer is_registrar_for()Klassenmethode iteriert :

class Registrar(object):
  def __init__(self, domain):
    self.domain = domain

class RegistrarA(Registrar):
  @classmethod
  def is_registrar_for(cls, domain):
    return domain == 'foo.com'

class RegistrarB(Registrar):
  @classmethod
  def is_registrar_for(cls, domain):
    return domain == 'bar.com'


def Domain(domain):
  for cls in Registrar.__subclasses__():
    if cls.is_registrar_for(domain):
      return cls(domain)
  raise ValueError


print Domain('foo.com')
print Domain('bar.com')

Auf diese Weise können Sie transparent neue Registrars hinzufügen und die Entscheidung, welche Domänen jeweils unterstützt werden, an diese delegieren.

Alec Thomas
quelle
1
Hallo @Alec. Sind in diesem speziellen Fall die Dekorateure (@classmethod) in den Klassen erforderlich? Wenn ja, welche Rolle spielen sie in diesem Zusammenhang?
Morlock
5
@AlecThomas, @staticmethodkönnte in diesem Zusammenhang etwas besser gewesen sein, denke ich
dmytro
17
Dieser Ansatz ist unsicher, es sei denn, die konkreten Unterklassen schließen is_registrar_for()sich gegenseitig aus und werden dies auch in Zukunft bleiben . Die Reihenfolge der von zurückgegebenen Werte __subclasses__()ist beliebig. Und diese Reihenfolge ist im Allgemeinen wichtig. Wenn sich etwas im Code ändert (möglicherweise so geringfügig wie die Reihenfolge der Klassendefinitionen), kann dies zu einem anderen Ergebnis führen. Die Kosten für solche Fehler, IMO, sind enorm und überwiegen bei weitem die Vorteile dieses Ansatzes. Ich würde stattdessen den Ansatz des OP verwenden, bei dem eine einzelne Funktion die gesamte Logik der Unterklassenauswahl enthält.
Max
8
Wenn Sie sich gegenseitig ausschließende Tests durchführen oder diesen Ansatz aus anderen Gründen für sicher halten, beachten Sie, dass __subclasses__nur unmittelbare Unterklassen zurückgegeben werden. Eine mehrstufige Vererbung würde daher eine kleine Optimierung erfordern, um korrekt verarbeitet zu werden.
Max
11
Beachten Sie auch, dass dies __subclasses__ nur für lebende Objekte funktioniert. Wenn eine Klasse noch nicht importiert wurde, wird sie nicht in den Ergebnissen angezeigt (da sie nicht vorhanden ist).
siebz0r
23

Angenommen, Sie benötigen separate Klassen für verschiedene Registrare (obwohl dies in Ihrem Beispiel nicht offensichtlich ist), sieht Ihre Lösung in Ordnung aus, obwohl RegistrarA und RegistrarB wahrscheinlich die Funktionalität gemeinsam nutzen und von einer abstrakten Basisklasse abgeleitet werden könnten .

Alternativ zu Ihrer factoryFunktion können Sie ein Diktat angeben, das Ihren Registrar-Klassen zugeordnet ist:

Registrar = {'test.com': RegistrarA, 'test.biz': RegistrarB}

Dann:

registrar = Registrar['test.com'](domain)

Ein Problem: Sie führen hier nicht wirklich eine Klassenfabrik durch, da Sie eher Instanzen als Klassen zurückgeben.

Jeff Bauer
quelle
Falls Sie den Registrar anhand von regulären Ausdrücken ermitteln müssen, können Sie mit match.groupdict () den Slot im Registrar-Wörterbuch oben berechnen: registrarRe = re.compile ("(? P <A>. * \. Com) | ( ? P <B>. * \. Biz) ")
Susanne Oberhauser
11

In Python können Sie die eigentliche Klasse direkt ändern:

class Domain(object):
  def __init__(self, domain):
    self.domain = domain
    if ...:
      self.__class__ = RegistrarA
    else:
      self.__class__ = RegistrarB

Und dann wird das Folgende funktionieren.

com = Domain('test.com') #load RegistrarA
com.lookup()

Ich benutze diesen Ansatz erfolgreich.

Bialix
quelle
Eine Warnung finden Sie in den Kommentaren zu stackoverflow.com/a/9144059/336527 (Sie sind sicher, wenn alle Registrare dieselbe Basisklasse haben und keine Slots verwenden).
Max
31
Tatsächlich birgt dieser Ansatz eine viel ernstere Gefahr, als mir klar wurde: Spezielle Methoden werden möglicherweise nicht richtig aufgerufen usw. Ich bin jetzt davon überzeugt, dass dies NIEMALS getan werden sollte, da das Durcheinander, herauszufinden, welche Probleme dies verursachen kann, variieren kann die Version von Python, und ist einfach nicht wert, welche Vorteile dies bietet.
Max
8

Sie können eine 'Wrapper'-Klasse erstellen und ihre __new__()Methode überladen , um Instanzen der spezialisierten Unterklassen zurückzugeben, z.

class Registrar(object):
    def __new__(self, domain):
        if ...:
            return RegistrarA(domain)
        elif ...:
            return RegistrarB(domain)
        else:
            raise Exception()

Um sich mit sich nicht gegenseitig ausschließenden Bedingungen zu befassen, die in anderen Antworten angesprochen wurden, müssen Sie sich zunächst die Frage stellen, ob die Wrapper-Klasse, die die Rolle eines Dispatchers spielt, die Bedingungen regeln soll oder es wird es an die Fachklassen delegieren. Ich kann einen gemeinsamen Mechanismus vorschlagen, bei dem die spezialisierten Klassen ihre eigenen Bedingungen definieren, der Wrapper jedoch die Validierung wie folgt durchführt (vorausgesetzt, jede spezialisierte Klasse stellt eine Klassenmethode bereit, die überprüft, ob es sich um einen Registrar für eine bestimmte Domäne handelt, is_registrar_for (. ..) wie in anderen Antworten vorgeschlagen):

class Registrar(object):
    registrars = [RegistrarA, RegistrarB]
    def __new__(self, domain):
        matched_registrars = [r for r in self.registrars if r.is_registrar_for(domain)]

        if len(matched_registrars) > 1:
            raise Exception('More than one registrar matched!')
        elif len(matched_registrars) < 1:
            raise Exception('No registrar was matched!')
        else:
            return matched_registrars[0](domain)
Ion Lesan
quelle
Ihr erstes Beispiel ist genau das, was ich selbst entwickelt habe. Dies ist jedoch der einzige Ort, an dem ich es so gefunden habe. Kennen Sie irgendwelche Nachteile, wenn Sie es so machen?
Tom
1
Es ist schwer zu sagen. Wenn Sie die Dokumentation docs.python.org/2/reference/datamodel.html#object.__new__ überprüfen , gibt es nichts, was diese Verwendung behindert , aber auch nicht viel, um sie zu unterstützen.
Ion Lesan
1
Obwohl eine typische Implementierung sowie das, wofür sie gedacht war (dh hauptsächlich für unveränderliche Klassen), erwähnt wird, wird auch die Möglichkeit erwähnt, __new__etwas anderes als eine Instanz von zurückzugeben cls, und da die Rückgabe Noneausdrücklich verboten ist, würde dies zu einer Schlussfolgerung, dass eine Instanz einer anderen Klasse zurückgegeben werden darf.
Ion Lesan
Danke, Ion. Am Ende habe ich ein paar andere Beispiele gefunden , die jedoch nicht immer gut ankommen .
Tom
2

Ich habe die ganze Zeit dieses Problem. Wenn Sie die Klassen in Ihre Anwendung (und ihre Module) eingebettet haben, können Sie eine Funktion verwenden. Wenn Sie Plugins jedoch dynamisch laden, benötigen Sie etwas Dynamischeres - die automatische Registrierung der Klassen bei einer Factory über Metaklassen.

Hier ist ein Muster, von dem ich sicher bin, dass ich es ursprünglich aus StackOverflow entfernt habe, aber ich habe noch nicht den Pfad zum ursprünglichen Beitrag

_registry = {}

class PluginType(type):
    def __init__(cls, name, bases, attrs):
        _registry[name] = cls
        return super(PluginType, cls).__init__(name, bases, attrs)

class Plugin(object):
    __metaclass__  = PluginType # python <3.0 only 
    def __init__(self, *args):
        pass

def load_class(plugin_name, plugin_dir):
    plugin_file = plugin_name + ".py"
    for root, dirs, files in os.walk(plugin_dir) :
        if plugin_file in (s for s in files if s.endswith('.py')) :
            fp, pathname, description = imp.find_module(plugin_name, [root])
            try:
                mod = imp.load_module(plugin_name, fp, pathname, description)
            finally:
                if fp:
                    fp.close()
    return

def get_class(plugin_name) :
    t = None
    if plugin_name in _registry:
        t = _registry[plugin_name]
    return t

def get_instance(plugin_name, *args):
    return get_class(plugin_name)(*args)
Mayur Patel
quelle
1

wie wäre es mit so etwas

class Domain(object):
  registrars = []

  @classmethod
  def add_registrar( cls, reg ):
    registrars.append( reg )

  def __init__( self, domain ):
    self.domain = domain
    for reg in self.__class__.registrars:
       if reg.is_registrar_for( domain ):
          self.registrar = reg  
  def lookup( self ):
     return self.registrar.lookup()    

Domain.add_registrar( RegistrarA )
Domain.add_registrar( RegistrarB )

com = Domain('test.com')
com.lookup()
Coyo
quelle
0

Hier ist ein Metaklasse sammelt implizit Registars Klassen in einem ENTITIES dict

class DomainMeta(type):
    ENTITIES = {}

    def __new__(cls, name, bases, attrs):
        cls = type.__new__(cls, name, bases, attrs)
        try:
            entity = attrs['domain']
            cls.ENTITIES[entity] = cls
        except KeyError:
            pass
        return cls

class Domain(metaclass=DomainMeta):
    @classmethod
    def factory(cls, domain):
        return DomainMeta.ENTITIES[domain]()

class RegistrarA(Domain):
    domain = 'test.com'
    def lookup(self):
        return 'Custom command for .com TLD'

class RegistrarB(Domain):
    domain = 'test.biz'
    def lookup(self):
        return 'Custom command for .biz TLD'


com = Domain.factory('test.com')
type(com)       # <class '__main__.RegistrarA'>
com.lookup()    # 'Custom command for .com TLD'

com = Domain.factory('test.biz')
type(com)       # <class '__main__.RegistrarB'>
com.lookup()    # 'Custom command for .biz TLD'
Christophe Morio
quelle