懒惰 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
我有一种情况,我希望能够使用基数 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