Python中的类工厂


70

我是Python的新手,需要一些建议来实现以下方案。

我有两个用于在两个不同的注册商处管理域的类。两者具有相同的接口,例如

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)

Answers:


82

我认为使用功能很好。

更为有趣的问题是,您如何确定要加载的注册商?一种选择是拥有抽象的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')

这样一来,您就可以透明地添加new Registrar,并向它们委派每个域所支持的决策。


1
嗨@Alec。在这种特殊情况下,类中的装饰器(@classmethod)是否必要?如果是,他们在那方面将扮演什么角色?
莫洛克

5
@AlecThomas,@staticmethod在这种情况下使用可能会稍微好一些,我认为
dmytro 2012年

17
除非具体的子类is_registrar_for()是互斥的,否则这种方法是不安全的,并且将来还会如此。返回值的顺序__subclasses__()是任意的。通常,此顺序很重要。结果,如果代码中的某些内容(也许与类定义的顺序一样小)发生了变化,那么最终结果可能会有所不同。IMO这类漏洞的代价是巨大的,并且远远超过了这种方法的好处。我将改用OP所使用的方法,其中一个函数包含子类选择的整个逻辑。
最高

8
如果您确实进行了互斥测试,或出于其他原因认为此方法是安全的,请注意,__subclasses__仅返回直接子类;因此多级继承需要进行一些细微调整才能正确处理。
2012年

11
另请注意,__subclasses__ 适用于活动对象。如果尚未导入类,则该类不会出现在结果中(因为它不“存在”)。
siebz0r 2013年

23

假设您需要为不同的注册服务商使用单独的类(尽管在您的示例中并不明显),您的解决方案看起来还可以,尽管RegistrarARegistrarB可能共享功能,并且可以派生自Abstract Base Class

作为factory功能的替代方法,您可以指定一个字典,映射到您的注册商类:

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

然后:

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

一个小问题:在返回实例而不是类时,您实际上并没有在这里做类工厂。


如果需要通过regexp确定注册商,则可以使用match.groupdict()计算上面的注册商词典中的广告位:registrarRe = re.compile(“(?P <A>。* \。com)|( ?P <B>。* \。biz)“)
Susanne Oberhauser 2013年

11

在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()

我正在成功使用此方法。


请参阅对stackoverflow.com/a/9144059/336527的评论以获取警告(如果所有注册服务商都具有相同的基类并且不使用插槽,则可以放心)。
2012年

31
实际上,这种方法所带来的危险比我意识到的要严重得多:可能无法正确调用特殊方法,等等。我现在坚信绝对不应该这样做,因为弄清楚这可能导致什么问题的原因可能因Python版本,并且不值得提供任何好处。
最大

8

您可以创建一个“包装器”类,并重载其__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)

您的第一个示例正是我自己开发的。但是,这是我发现这样做的唯一地方。您知道这样做有什么弊端吗?
汤姆(Tom),

1
很难说。如果查看文档docs.python.org/2/reference/datamodel.html#object.__new__,没有什么可以阻止这种用法的,但是也没有太多的方法来支持它。
Ion Lesan '16

1
尽管它提到了一个典型的实现,以及它的预期目的(即主要针对不可变的类),但也提到了__new__返回不同于实例的东西的可能性cls,并且由于None明确禁止返回,这将导致结论是允许返回不同类的实例。
Ion Lesan '16

谢谢,离子 我最终找到一些 其他的 例子,虽然它并不总是深受欢迎
汤姆,

2

我一直都有这个问题。如果您在应用程序(及其模块)中嵌入了类,则可以使用函数。但是如果动态加载插件,则需要更多动态功能-通过元类自动向工厂注册类。

我确定这是我最初从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)

1

怎么样

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

0

这里的元类隐式收集ENTITIES字典中的Registars类

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'
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.