【问题标题】:Lazy class factory?懒人班工厂?
【发布时间】:2020-09-08 12:54:13
【问题描述】:

我有一种情况,我希望能够使用基类来构造派生类的对象。返回的特定子类取决于无法传递给构造函数/工厂方法的信息,因为它尚不可用。相反,该信息已被下载并解析以确定子类。

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

所以当第一次创建对象时,它是基类的对象。但是,当我第一次访问它时,我希望它下载它的信息,将自己转换为适当的派生类,并返回请求的信息。

我将如何在 python 中执行此操作?我在想我想要像工厂方法这样的东西,但具有某种延迟初始化功能。有这方面的设计模式吗?

【问题讨论】:

  • 通常,您不会更改对象的类型,句号。这不像在 C++ 中,对象的一部分“属于”父类,一部分属于子类。相反,相同的基本对象通过多个 __init__ 方法进行初始化,但是一旦该过程完成,对象本身就不会表明哪些属性是由哪个类创建的。
  • 也就是说,这听起来像是property 的用例,其中属性的基础值是None,直到您真正访问它,此时getter 可以访问URL 以完成该属性的初始化。
  • 一个涉及立即初始化的类的具体示例以及您想要延迟的部分会有所帮助。从那以后,提供一个可以满足您需求的答案应该很简单。

标签: python factory metaclass lazy-initialization


【解决方案1】:

这可以在 Python 中通过多种方式完成 - 甚至可能不借助元类。

如果您可以在需要的那一刻之前创建类实例,那么只需创建一个可调用的(可以是部分函数)来计算类并创建实例。

但是您的文字描述了您希望该类仅在“首次访问”时“初始化” - 包括更改其自己的类型。这是可行的——但它需要在类特殊方法中进行一些布线——如果“首次访问”是指调用方法或读取属性,那很容易——我们只需要自定义 __getattribute__ 方法即可触发初始化机制。另一方面,如果您的类将实现 Python 的“神奇”“dunder”方法,例如 __len____getitem____add__ - 那么“首次访问”可能意味着在例如,它有点棘手 - 每个 dunder 方法都必须由将导致初始化发生的代码包装 - 因为对这些方法的访问不通过__getattribute__

至于设置子类类型本身,就是将__class__属性设置为实例上正确的子类。 Python 允许,如果两个类(旧的和新的)的所有祖先都有 相同的 __slots__ 设置 - 所以,即使您使用 __slots__ 功能, 你只是不能在子类上改变它。

所以,分三种情况:

1 - 延迟实例化

将类定义本身包装到一个函数中,并预先加载它需要的 URL 或其他数据。当函数被调用时,新类被计算并实例化。


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 - 初始化属性/方法访问 - 没有特殊的 __magic__ 方法

触发类__getattribute__方法中的类计算和初始化


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'

但是懒惰地计算子类所需的参数必须以某种方式传递——在这个例子中,我需要将“url”参数传递给基类——但是如果 url 不是,即使这个例子也可以工作此时可用。在使用实例之前,您可以通过执行 instance._initial_kwargs["url"] = "i.got.it.now" 来更新 url。

此外,对于演示,我必须使用纯 Python 而不是 IPython,因为 IPython CLI 将内省新实例,触发其转换。

3 - 在操作员使用时初始化 - 专门的 __magic__ 方法。

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

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

这有一些曲折以使魔术方法正常运行 无论是在子类中覆盖它们还是在初始基类中调用它们的情况下。无论如何,所有可能的魔术方法 由子类使用必须在 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

【讨论】:

  • 哇,非常深入的回答!几个问题。我认为方法2最接近我的需要。 Q1:给子类赋值self.__class_后initialize()中的self.__init__()调用会调用子类中的init方法吗?我以前没有直接使用 self.__class__ 。 Q2:在Base类中定义“method1()”的目的是什么?
  • (q1) - 是的,子类中的 __init__ 方法被调用,因为此时 self 已经是子类的一个实例,由分配给 __class__ 引起。 method1 只是为了证明它不是被调用的方法 - 它可能根本不存在。 (与示例 3 中的魔法方法不同 - 这些方法必须存在于 Base
  • 所以我现在开始工作了,谢谢!但是,由于我的compute_subclass() 方法必须引用某些实例变量,所以我将其设为实例方法,并且对各种属性的所有调用都导致通过__getattribute__() 进行递归。所以我不得不添加一个self._initializing 成员来使self._initialize() 中断递归。
猜你喜欢
  • 1970-01-01
  • 2012-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多