【问题标题】:Class factory in PythonPython中的类工厂
【发布时间】:2023-03-15 07:49:01
【问题描述】:

我是 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):
        ...

我想创建一个域类,给定一个域名,根据扩展名加载正确的注册商类,例如

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)

【问题讨论】:

    标签: python factory


    【解决方案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,并将每个支持哪些域的决定委托给他们。

    【讨论】:

    • 嗨@Alec。在这种特殊情况下,类中的装饰器(@classmethod)是否必要?如果是,他们在这种情况下扮演什么角色?
    • @AlecThomas, @staticmethod 在这种情况下使用可能会稍微好一些,我认为
    • 这种方法是不安全的,除非具体子类的is_registrar_for() 是互斥的,并且将来仍将如此__subclasses__() 返回的值的顺序是任意的。总的来说,这个顺序很重要。结果,如果代码中的某些内容(可能与类定义的顺序一样小)发生更改,您最终可能会得到不同的结果。此类错误的成本,IMO,是巨大的,并且远远超过了这种方法的好处。我会改为使用 OP 使用的方法,其中单个函数包含子类选择的整个逻辑。
    • 如果您确实有互斥测试,或者出于其他原因认为这种方法是安全的,请注意__subclasses__ 仅返回直接子类;所以多级继承需要进行一些小调整才能正确处理。
    • 还要注意__subclasses__ 适用于活动对象。如果一个类尚未导入,则它不会出现在结果中(因为它不“存在”)。
    【解决方案2】:

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

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

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

    然后:

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

    一个小问题:你不是真的在这里做一个类工厂,因为你返回的是实例而不是类。

    【讨论】:

    • 如果您需要通过正则表达式确定注册商,您可以使用 match.groupdict() 来计算上面 Registrar 字典中的 slot:registrarRe = re.compile("(?P .*\.com)|(?P.*\.biz)")
    【解决方案3】:

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

    我正在成功地使用这种方法。

    【讨论】:

    • 请参阅 cmets 至 stackoverflow.com/a/9144059/336527 以获取警告(如果所有注册商具有相同的基类并且不使用插槽,则您是安全的)。
    • 实际上,这种方法带有一个much more serious danger,而不是我意识到的:特殊方法可能无法正确调用,等等。我现在确信永远不应该这样做,因为搞清楚什么是一团糟这可能导致的问题可能因 Python 的版本而异,而且不值得它提供的任何好处。
    • 看起来很 hacky,与使用普通函数并没有什么不同。
    【解决方案4】:

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

    【讨论】:

    • 你的第一个例子正是我自己开发的;但是,这是我发现这样做的唯一地方。你知道这样做有什么缺点吗?
    • 很难说。如果您查看文档docs.python.org/2/reference/datamodel.html#object.__new__,没有什么可以阻止这种用法,但也没有太多支持它。
    • 虽然它提到了一个典型的实现,以及它的用途(即主要用于不可变类),但提到了__new__ 返回与cls 实例不同的东西的可能性,同样,由于明确禁止返回None,因此会得出允许返回不同类的实例的结论。
    • 谢谢,离子。我最终找到了fewotherexamples,尽管它并不总是well-received
    【解决方案5】:

    我一直有这个问题。如果您的应用程序(及其模块)中嵌入了类,那么您可以使用函数;但是如果你动态加载插件,你需要一些更动态的东西——通过元类自动向工厂注册类。

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

    【讨论】:

      【解决方案6】:

      怎么样

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

      【讨论】:

        【解决方案7】:

        由于方法可能是共享的,因此使用一些基类是有意义的。 getattr可以在工厂函数中使用,动态调用另一个类。

        计算注册器类型的逻辑不应该是这些类的一部分,而应该是一些辅助函数。

        import sys
        
        class RegistrarBase():
            """Registrar Base Class"""
            def __init__(self, domain):
                self.name = domain
        
            def register(self, info):
                pass
        
            def lookup(self):
                pass
            def __repr__(self):
                return "empty domain"
        
        
        class RegistrarA(RegistrarBase):
            def __repr__(self):
                return ".com domain"
                            
        class RegistrarB(RegistrarBase):
            def __repr__(self):
                return ".biz domain"
        
        
        def create_registrar(domainname, registrartype):
            try: 
                registrar = getattr(sys.modules[__name__], registrartype)
                return registrar(domainname)
            except:
                return RegistrarBase(domainname)
        
            
            
        domain = create_registrar(domainname = 'test.com', registrartype='RegistrarA')    
        
        print(domain)    
        print(domain.name)
        #.com domain
        #test.com
        

        【讨论】:

        • 这与 OP 它试图做的有点不同:他的每个派生类都可以识别它的域。您提出了某种与域名/类名对应的文本表。
        【解决方案8】:

        好的,这是基于 Alec Thomas 的答案的答案,经过修改和扩展:照顾多级继承和歧义。如果 _resolve 应该比简单的唯一性检查更复杂并且可能会更改,则它可以作为参数而不是类方法提供。

        基类模块 bbb.py:

        from __future__ import annotations
        
        from abc import ABC, abstractmethod
        from typing import Sequence, Type
        
        
        class Base(ABC):
        
            def __init__(self, *args, **kwargs):
                ...
        
            @classmethod
            def isit(cls, _s: str) -> bool:
                return False
        
            @classmethod
            def from_str(cls, s: str, *args, **kwargs) -> Base:
                subs = cls._findit(s)
                sc = cls._resolve(s, subs)
                return sc(*args, **kwargs)
        
            @classmethod
            def _findit(cls, s: str) -> Sequence[Type[Base]]:
                subs = [cls] if cls.isit(s) else []
                subs += [ssc for sc in cls.__subclasses__() for ssc in sc._findit(s)]
                return subs
        
            @classmethod
            def _resolve(cls, s: str, subs: Sequence[Type[Base]]) -> Type[Base]:
                if len(subs) == 0:
                    raise Exception(f'Cannot find subclass for {s}')
        
                if len(subs) > 1:
                    raise Exception(
                        f'Cannot choose unique subclass for {s}: {subs}')
                sc = subs[0]
                return sc
        
        
        class B(Base):
            @classmethod
            def isit(cls, s: str) -> bool:
                res = s == 'b class'
                return res
            enter code here
        

        派生类模块ccc.py:

        from bbb import Base
        
        
        class C(Base):
            @classmethod
            def isit(cls, s: str) -> bool:
                res = s == 'c class'
                return res
        
        
        class CC(Base):
            @classmethod
            def isit(cls, s: str) -> bool:
                res = s == 'cc class'
                return res
        

        使用方法:

        In [4]: from bbb import Base
        
        In [5]: import ccc
        
        In [6]: Base.from_str('b class')
        Out[6]: <bbb.B at 0x1adf2665288>
        
        In [7]: Base.from_str('c class')
        Out[7]: <ccc.C at 0x1adf266a908>
        
        In [8]: Base.from_str('cc class')
        Out[8]: <ccc.CC at 0x1adf2665608>
        

        【讨论】:

          【解决方案9】:

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

          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'
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-02-22
            • 2023-04-08
            • 2019-03-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多