web-dev-qa-db-fra.com

Usine de classe en Python

Je connais Python pour la première fois et j'ai besoin de conseils pour mettre en œuvre le scénario ci-dessous.

J'ai deux classes pour la gestion de domaines chez deux registraires différents. Les deux ont la même interface, par exemple.

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

    def lookup(self):
        ...

    def register(self, info):
        ...

et

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

    def lookup(self):
        ...

    def register(self, info):
        ...

J'aimerais créer une classe de domaine qui, à partir d'un nom de domaine, charge la classe de registraire appropriée en fonction de l'extension, par exemple.

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

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

Je sais que cela peut être accompli en utilisant une fonction usine (voir ci-dessous), mais est-ce la meilleure façon de le faire ou existe-t-il une meilleure façon en utilisant les fonctionnalités OOP?

def factory(domain):
  if ...:
    return RegistrarA(domain)
  else:
    return RegistrarB(domain)
58
drjeep

Je pense que l'utilisation d'une fonction est très bien.

La question la plus intéressante est de savoir comment déterminer quel registraire charger. Une option consiste à avoir une classe abstraite Registrar avec sous-classe d'implémentations concrètes, puis itérera sur sa __subclasses__() en appelant une méthode de classe is_registrar_for():

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')

Cela vous permettra d'ajouter de manière transparente de nouvelles Registrars et de leur déléguer la décision des domaines pris en charge par chacun.

72
Alec Thomas

En supposant que vous ayez besoin de classes distinctes pour différents bureaux d'enregistrement (bien que ce ne soit pas évident dans votre exemple), votre solution semble correcte, bien que RegistrarA et RegistrarB partagent probablement des fonctionnalités et puissent être dérivées d'un Abstract Base Class .

En guise d'alternative à votre fonction factory, vous pouvez spécifier un dict, mappé à vos classes de registraire:

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

Ensuite:

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

Un petit reproche: vous ne faites pas vraiment une classe fabrique ici car vous renvoyez des instances plutôt que des classes.

21
Jeff Bauer

En Python, vous pouvez modifier directement la classe actuelle:

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

Et ensuite, ça va marcher.

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

J'utilise cette approche avec succès.

9
bialix

Vous pouvez créer une classe 'wrapper' et surcharger sa méthode __new__() pour renvoyer des instances des sous-classes spécialisées, par exemple:

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

De plus, afin de traiter des conditions non mutuellement exclusives, une question soulevée dans d’autres réponses, la première question à vous poser est de savoir si vous voulez que la classe wrapper, qui joue le rôle de répartiteur, régisse les conditions ou il le déléguera aux classes spécialisées. Je peux suggérer un mécanisme partagé, dans lequel les classes spécialisées définissent leurs propres conditions, mais le wrapper effectue la validation de la manière suivante (à condition que chaque classe spécialisée expose une méthode de classe permettant de vérifier s’il s’agit d’un registraire pour un domaine particulier, is_registrar_for (. ..) comme suggéré dans d'autres réponses):

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)
5
Ion Lesan

J'ai ce problème tout le temps. Si vous avez les classes incorporées dans votre application (et ses modules), vous pouvez utiliser une fonction. mais si vous chargez les plug-ins de manière dynamique, vous avez besoin de quelque chose de plus dynamique: enregistrer les classes auprès d'une fabrique via des métaclasses automatiquement.

Voici un modèle que je suis sûr d'avoir tiré de StackOverflow à l'origine, mais je n'ai toujours pas le chemin d'accès au message d'origine.

_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)
2
Mayur Patel

que diriez-vous de quelque chose comme 

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()
1
Coyo

Ici, une métaclasse collecte implicitement les classes de registres dans une ENTIT&EACUTE;S 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'
0
Christophe Morio