懒惰阶级工厂?

2024-05-16 08:13:41 发布

您现在位置:Python中文网/ 问答频道 /正文

我希望能够使用基类来构造派生类的对象。返回的特定子类依赖于无法传递给构造函数/工厂方法的信息,因为它还不可用。相反,这些信息被下载并解析以确定子类

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

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

在python中如何实现这一点?我想我想要一个类似工厂方法的东西,但是有一些延迟的初始化特性。这个有设计模式吗


Tags: 对象方法程序信息url工厂设计模式特性
1条回答
网友
1楼 · 发布于 2024-05-16 08:13:41

在Python中,这可以通过几种方式实现——甚至可能不需要使用元类

如果您可以创建类实例,直到需要时为止,那么只需要创建一个可调用的(可能是部分函数)即可计算类并创建实例

但您的文本描述了您希望类在“首次访问”时只是“初始化”——包括更改自己的类型。这是可行的——但是它需要在类特殊方法中进行一些连接——如果“first access”指的是jsut调用方法或读取属性,这很容易——我们只需要定制__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

相关问题 更多 >