这可以在 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