Python 中的类工厂

我是 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 类,它给定一个域名,根据扩展加载正确的 Registry 类,例如。

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)
83520 次浏览

假设您需要针对不同注册表的单独类(尽管在您的示例中并不明显) ,那么您的解决方案看起来不错,尽管 注册官 A登记处可能共享功能,并且可以从 抽象基类派生。

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

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

然后:

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

一个挑剔的地方是: 您在这里并没有真正地做一个类工厂,因为您返回的是实例而不是类。

我认为使用函数是可以的。

更有趣的问题是如何确定要加载哪个注册表?一种选择是有一个抽象的基础 Registry 类,它是一个具体实现的子类,然后在它的 __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')

这将允许您透明地添加新的 Registrar,并将每个域支持的决策委托给它们。

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

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

比如说

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

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

这里有一个模式,我确信我最初是从 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)

您可以创建一个“包装器”类并重载它的 __new__()方法,以返回特定子类的实例,例如:

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

此外,为了处理非互斥条件,在其他答案中提出了一个问题,首先要问自己的问题是,是希望扮演调度器角色的包装器类来管理条件,还是将其委托给专门的类。我可以建议一种共享机制,其中专门的类定义自己的条件,但包装器进行验证,像这样(假设每个专门的类公开一个类方法,验证它是否是一个特定域的注册者,是 _ register _ 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)

这里的元类隐式地收集 实体结构中的 Registry 类

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'

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

指定 registrartype 的逻辑不应该包含在这些类中,而应该包含在某个 helper 函数中。

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

好的,这里有一个基于 Alec Thomas 的答案,经过修改和扩展: 处理多级继承和模糊性。如果 _ decision 应该比简单的唯一性检查更复杂,并且可能会更改,那么它可以作为参数提供,而不是类方法。

基类模块 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>