web-dev-qa-db-ja.com

Pythonのクラスファクトリ

Pythonが初めてなので、以下のシナリオを実装するためのアドバイスが必要です。

2つの異なるレジストラでドメインを管理するための2つのクラスがあります。両方とも同じインターフェースを持っています。

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

    def lookup(self):
        ...

    def register(self, info):
        ...

そして

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

    def lookup(self):
        ...

    def register(self, info):
        ...

ドメイン名を指定して、拡張子に基づいて正しいレジストラクラスを読み込むDomainクラスを作成したいと思います。

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

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

ファクトリー関数を使用してこれを達成できることは知っていますが(これを参照)、これがそれを行う最良の方法ですか、またはOOP機能を使用するより良い方法がありますか?

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

関数を使用するのは問題ないと思います。

より興味深い質問は、どのレジストラをロードするかをどのように決定するのですか? 1つのオプションは、サブクラスを具体的に実装する抽象ベースRegistrarクラスを作成し、__subclasses__()クラスメソッドを呼び出して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')

これにより、新しいRegistrarsを透過的に追加し、それぞれがサポートするドメインの決定をそれらに委任できます。

75
Alec Thomas

レジストラごとに個別のクラスが必要であると仮定すると(例では明らかではありませんが)、ソリューションは問題ないように見えますが、RegistrarAおよびRegistrarBはおそらく機能を共有しており、 Abstract Base Class から派生できます。

factory関数の代わりに、ディクショナリを指定して、レジストラークラスにマッピングできます。

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

次に:

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

1つのqui言:クラスではなくインスタンスを返すため、ここでは実際にクラスファクトリを実行していません。

22
Jeff Bauer

Pythonでは、実際のクラスを直接変更できます。

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

そして、次のように動作します。

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

私はこのアプローチをうまく使っています。

10
bialix

「ラッパー」クラスを作成し、その__new__()メソッドをオーバーロードして、特殊なサブクラスのインスタンスを返すことができます。例:

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

さらに、相互に排他的でない条件、他の回答で提起された問題に対処するために、最初に自問する問題は、ディスパッチャの役割を果たすラッパークラスが条件を管理するかどうか、または特別なクラスに委任します。特殊なクラスが独自の条件を定義する共有メカニズムを提案できますが、ラッパーはこのように検証を行います(特定の各クラスが特定のドメインのレジストラであるかどうかを検証するクラスメソッドis_registrar_for(を提供する場合)。 ..)他の回答で示唆されているように):

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

私は常にこの問題を抱えています。アプリケーション(およびそのモジュール)にクラスが埋め込まれている場合、関数を使用できます。ただし、プラグインを動的にロードする場合は、さらに動的なものが必要です。メタクラスを介してクラスをファクトリに自動的に登録します。

ここに元々StackOverflowから解除したはずのパターンがありますが、元の投稿へのパスはまだありません

_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

のようなものはどうですか

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

ここで、メタクラスは暗黙的に[〜#〜] 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'
0