懒惰 class 工厂?

Lazy class factory?

我有一种情况,我希望能够使用基数 class 来构造派生的 classes 的 objects。具体的 child class returned 依赖于无法传递给 constructor/factory 方法的信息,因为它尚不可用。相反,该信息已被下载并解析以确定 child class.

所以我想我想懒惰地初始化我的 objects,只传递一个 URL,它可以从中下载所需的信息,但要等到实际生成 child object 直到程序需要它(即在第一次访问时)。

因此,当 object 首次创建时,它是基数 class 的 object。但是,当我第一次访问它时,我希望它下载它的信息,将自己转换为适当的派生 class,并 return 请求的信息。

我如何在 python 中执行此操作?我想我想要类似工厂方法的东西,但具有某种 delayed-initialization 功能。有这方面的设计模式吗?

这可以在 Python 中以多种方式完成 - 甚至可能不求助于元class。

如果您可以只创建 class 个实例,直到您需要的那一刻,那么只需创建一个可调用对象(可以是部分函数)即可计算 class 并创建实例。

但您的文字描述您希望 class 在 "first access" 上仅 "intialize" - 包括更改其自身的类型。这是可行的 - 但它需要在 class 特殊方法中进行一些连接 - 如果 "first access" 你的意思是调用方法或读取属性,那很容易 - 我们只需要自定义 __getattribute__方法来触发初始化机制。另一方面,如果您的 class 将实现 Python 的 "magic" "dunder" 方法,例如 __len____getitem____add__ - 然后 "first access" 可能意味着在实例上触发这些方法之一,它有点棘手 - 每个 dunder 方法都必须由将导致初始化发生的代码包装 - 作为对这些方法的访问不经过 __getattribute__.

至于设置 subclass 类型本身,只需在实例上将 __class__ 属性设置为正确的 subclass 即可。 Python 允许如果两个 classes(旧的和新的)的所有祖先都具有 相同的 __slots__ 设置 - 因此,即使您使用 __slots__ 功能, 你不能在 subclasses.

上改变它

所以,一共有三种情况:

1 - 延迟实例化

将 class 定义本身包装成一个函数,预加载 URL 或它需要的其他数据。调用该函数时,将计算并实例化新的 class。


from functools import lru_cache

class Base:        
    def __repr__(self):
        return f"Instance of {self.__class__.__name__}"

@lru_cache
def compute_subclass(url):
    # function that eagerly computes the correct subclass
    # given the url .
    # It is strongly suggested that this uses some case
    # of registry, so that classes that where computed once,
    # are readly available when the input parameter is defined.
    # Python's  lru_cache decorator can do that
    ...
    class Derived1(Base):
        def __init__(self, *args, **kwargs):
            self.parameter = kwargs.pop("parameter", None)

    ...
    subclass = Derived1

    return subclass

def prepare(*args, **kwargs):
    def instantiate(url):
        subclass = compute_subclass(url)
        instance = subclass(*args, **kwargs)
        return instance
    return instantiate

这可以用作:

In [21]: lazy_instance = prepare(parameter=42)                                                                

In [22]: lazy_instance                                                                                        
Out[22]: <function __main__.prepare.<locals>.instantiate(url)>

In [23]: instance = lazy_instance("fetch_from_here")                                                          

In [24]: instance                                                                                             
Out[24]: Instance of Derived1

In [25]: instance.parameter                                                                                   
Out[25]: 42


2 - 在 attribute/method 访问时初始化 - 没有特殊的 __magic__ 方法

在class__getattribute__方法中触发class-计算和初始化


from functools import lru_cache
class Base:
    def __init__(self, *args, **kwargs):
        # just annotate intialization parameters that can be later
        # fed into sublasses' init. Also, this can be called
        # more than once (if subclasses call "super"), and it won't hurt

        self._initial_args = args
        self._initial_kwargs = kwargs
        self._initialized = False

    def _initialize(self):
        if not self._initialized:
            subclass = compute_subclass(self._initial_kwargs["url"])
            self.__class__ = subclass
            self.__init__(*self._initial_args, **self._initial_kwargs)
            self._initialized = True

    def __repr__(self):
        return f"Instance of {self.__class__.__name__}"

    def __getattribute__(self, attr):
        if attr.startswith(("_init", "__class__", "__init__")): # return real attribute, no side-effects:
            return object.__getattribute__(self, attr)
        if not self._initialized:
            self._initialize()
        return object.__getattribute__(self, attr)    


@lru_cache
def compute_subclass(url):
    # function that eagerly computes the correct subclass
    # given the url .
    # It is strongly suggested that this uses some case
    # of registry, so that classes that where computed once,
    # are readly available when the input parameter is defined.
    # Python's  lru_cache decorator can do that

    print(f"Fetching initialization data from {url!r}")
    ...
    class Derived1(Base):
        def __init__(self, *args, **kwargs):
            self.parameter = kwargs.pop("parameter", None)

        def method1(self):
            return "alive"

    ...
    subclass = Derived1

    return subclass

这可以无缝地工作实例创建之后:

>>> instance = Base(parameter=42, url="this.place")
>>> instance
Instance of Base
>>> instance.parameter
Fetching initialization data from 'this.place'
42
>>> instance
Instance of Derived1
>>> 
>>> instance2 = Base(parameter=23, url="this.place")
>>> instance2.method1()
'alive'

但是计算 subclass 所需的参数必须以某种方式传递 - 在这个例子中,我要求然后将 "url" 参数传递给基础 class - 但如果此时 url 不可用,即使这个例子也可以工作。在使用实例之前,您可以通过 instance._initial_kwargs["url"] = "i.got.it.now" 更新 url。

此外,对于演示,我不得不进入纯 Python 而不是 IPython,因为 IPython CLI 将自检新实例,触发其转换。

3 - 在运算符使用时初始化 - 专门的 __magic__ 方法。

有一个 metaclass 用装饰器包装 baseclass 魔术方法 这将计算新的 class 并执行初始化。

此代码与之前的代码非常相似,但在元class 到 Base 上,__new__ 方法必须检查所有 __magic__ 方法,然后调用 self._initialize 进行装饰。

这有一些曲折使魔法方法正常运行 无论是在 subclass 中覆盖它们还是在初始基础中调用它们。无论如何,所有可能的魔术方法 subclasses 使用必须在 Base 中定义,即使它们全部 要做的是提高 "NotImplementedError" -

from functools import lru_cache, wraps

def decorate_magic_method(method):
    @wraps(method)
    def method_wrapper(self, *args, **kwargs):
        self._initialize()
        original_method = self.__class__._initial_wrapped[method.__name__]
        final_method = getattr(self.__class__, method.__name__)
        if final_method is method_wrapper:
            # If magic method has not been overriden in the subclass
            final_method = original_method
        return final_method(self, *args, **kwargs)
    return method_wrapper


class MetaLazyInit(type):
    def __new__(mcls, name, bases, namespace, **kwargs):
        wrapped = {}
        if name == "Base":
            # Just wrap the magic methods in the Base class itself

            for key, value in namespace.items():
                if key in ("__repr__", "__getattribute__", "__init__"):
                    # __repr__ does not need to be in the exclusion - just for the demo.
                    continue

                if key.startswith("__") and key.endswith("__") and callable(value):
                    wrapped[key] = value
                    namespace[key] = decorate_magic_method(value)

            namespace["_initial_wrapped"] = wrapped
        namespace["_initialized"] = False
        return super().__new__(mcls, name, bases, namespace, **kwargs)


class Base(metaclass=MetaLazyInit):
    def __init__(self, *args, **kwargs):
        # just annotate intialization parameters that can be later
        # fed into sublasses' init. Also, this can be called
        # more than once (if subclasses call "super"), and it won't hurt
        self._initial_args = args
        self._initial_kwargs = kwargs

    def _initialize(self):
        print("_initialize called")
        if not self._initialized:
            self._initialized = True
            subclass = compute_subclass(self._initial_kwargs["url"])
            self.__class__ = subclass
            self.__init__(*self._initial_args, **self._initial_kwargs)

    def __repr__(self):
        return f"Instance of {self.__class__.__name__}"

    def __getattribute__(self, attr):
        if attr.startswith(("_init", "__class__")) : # return real attribute, no side-effects:
            return object.__getattribute__(self, attr)
        if not self._initialized:
            self._initialize()
        return object.__getattribute__(self, attr)

    def __len__(self):
        return 5

    def __getitem__(self, item):
        raise NotImplementedError()

@lru_cache
def compute_subclass(url):
    # function that eagerly computes the correct subclass
    # given the url .
    # It is strongly suggested that this uses some case
    # of registry, so that classes that where computed once,
    # are readly available when the input parameter is defined.
    # Python's  lru_cache decorator can do that

    print(f"Fetching initialization data from {url!r}")
    ...

    class TrimmedMagicMethods(Base):
        """This intermediate class have the initial magic methods
        as declared in Base - so that after the subclass instance
        is initialized, there is no overhead call to "self._initialize"
        """
        for key, value in Base._initial_wrapped.items():
            locals()[key] = value
            # Special use of "locals()" in the class body itself,
            # not inside a method, creates new class attributes

    class DerivedMapping(TrimmedMagicMethods):
        def __init__(self, *args, **kwargs):
            self.parameter = kwargs.pop("parameter", None)

        def __getitem__(self, item):
            return 42


    ...
    subclass = DerivedMapping

    return subclass

在终端上:

>>> reload(lazy_init); Base=lazy_init.Base
<module 'lazy_init' from '/home/local/GERU/jsbueno/tmp01/lazy_init.py'>
>>> instance = Base(parameter=23, url="fetching from there")
>>> instance
Instance of Base
>>> 
>>> instance[0]
_initialize called
Fetching initialization data from 'fetching from there'
42
>>> instance[1]
42
>>> len(instance)
5
>>> instance2 = Base(parameter=23, url="fetching from there")
>>> len(instance2)
_initialize called
5
>>> instance3 = Base(parameter=23, url="fetching from some other place")
>>> len(instance3)
_initialize called
Fetching initialization data from 'fetching from some other place'
5