cloning
Clonable
¶
A base class allowing inheriting classes define how they should be cloned.
Any class inheriting from Clonable gains these behaviors:
(i) A new method named .clone()
becomes available;
(ii) __deepcopy__
and __copy__
work as aliases for .clone()
;
(iii) A new method, _get_cloned_state(self, *, memo: dict)
is now
defined and needs to be implemented by the inheriting class.
The method _get_cloned_state(...)
expects a dictionary named memo
,
which maps from the ids of already cloned objects to their clones.
If _get_cloned_state(...)
is to use deep_clone(...)
or deepcopy(...)
within itself, this memo
dictionary can be passed to these functions.
The return value of _get_cloned_state(...)
is a dictionary, which will
be used as the __dict__
of the newly made clone.
Source code in evotorch/tools/cloning.py
class Clonable:
"""
A base class allowing inheriting classes define how they should be cloned.
Any class inheriting from Clonable gains these behaviors:
(i) A new method named `.clone()` becomes available;
(ii) `__deepcopy__` and `__copy__` work as aliases for `.clone()`;
(iii) A new method, `_get_cloned_state(self, *, memo: dict)` is now
defined and needs to be implemented by the inheriting class.
The method `_get_cloned_state(...)` expects a dictionary named `memo`,
which maps from the ids of already cloned objects to their clones.
If `_get_cloned_state(...)` is to use `deep_clone(...)` or `deepcopy(...)`
within itself, this `memo` dictionary can be passed to these functions.
The return value of `_get_cloned_state(...)` is a dictionary, which will
be used as the `__dict__` of the newly made clone.
"""
def _get_cloned_state(self, *, memo: dict) -> dict:
raise NotImplementedError
def clone(self, *, memo: Optional[dict] = None) -> "Clonable":
"""
Get a clone of this object.
Args:
memo: Optionally a dictionary which maps from the ids of the
already cloned objects to their clones. In most scenarios,
when this method is called from outside, this can be left
as None.
Returns:
The clone of the object.
"""
if memo is None:
memo = {}
self_id = id(self)
if self_id in memo:
return memo[self_id]
new_object = object.__new__(type(self))
memo[id(self)] = new_object
new_object.__dict__.update(self._get_cloned_state(memo=memo))
return new_object
def __copy__(self) -> "Clonable":
return self.clone()
def __deepcopy__(self, memo: Optional[dict]):
if memo is None:
memo = {}
return self.clone(memo=memo)
clone(self, *, memo=None)
¶
Get a clone of this object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
memo |
Optional[dict] |
Optionally a dictionary which maps from the ids of the already cloned objects to their clones. In most scenarios, when this method is called from outside, this can be left as None. |
None |
Returns:
Type | Description |
---|---|
Clonable |
The clone of the object. |
Source code in evotorch/tools/cloning.py
def clone(self, *, memo: Optional[dict] = None) -> "Clonable":
"""
Get a clone of this object.
Args:
memo: Optionally a dictionary which maps from the ids of the
already cloned objects to their clones. In most scenarios,
when this method is called from outside, this can be left
as None.
Returns:
The clone of the object.
"""
if memo is None:
memo = {}
self_id = id(self)
if self_id in memo:
return memo[self_id]
new_object = object.__new__(type(self))
memo[id(self)] = new_object
new_object.__dict__.update(self._get_cloned_state(memo=memo))
return new_object
ReadOnlyClonable (Clonable)
¶
Clonability base class for read-only and/or immutable objects.
This is a base class specialized for the immutable containers of EvoTorch. These immutable containers have two behaviors for cloning: one where the read-only attribute is preserved and one where a mutable clone is created.
Upon being copied or deep-copied (using the standard Python functions),
the newly made clones are also read-only. However, when copied using the
clone(...)
method, the newly made clone is mutable by default
(unless the clone(...)
method was used with preserve_read_only=True
).
This default behavior of the clone(...)
method was inspired by the
copy()
method of numpy arrays (the inspiration being that the .copy()
of a read-only numpy array will not be read-only anymore).
Subclasses of evotorch.immutable.ImmutableContainer
inherit from
ReadOnlyClonable
.
Source code in evotorch/tools/cloning.py
class ReadOnlyClonable(Clonable):
"""
Clonability base class for read-only and/or immutable objects.
This is a base class specialized for the immutable containers of EvoTorch.
These immutable containers have two behaviors for cloning:
one where the read-only attribute is preserved and one where a mutable
clone is created.
Upon being copied or deep-copied (using the standard Python functions),
the newly made clones are also read-only. However, when copied using the
`clone(...)` method, the newly made clone is mutable by default
(unless the `clone(...)` method was used with `preserve_read_only=True`).
This default behavior of the `clone(...)` method was inspired by the
`copy()` method of numpy arrays (the inspiration being that the `.copy()`
of a read-only numpy array will not be read-only anymore).
Subclasses of `evotorch.immutable.ImmutableContainer` inherit from
`ReadOnlyClonable`.
"""
def _get_mutable_clone(self, *, memo: dict) -> Any:
raise NotImplementedError
def clone(self, *, memo: Optional[dict] = None, preserve_read_only: bool = False) -> Any:
"""
Get a clone of this read-only object.
Args:
memo: Optionally a dictionary which maps from the ids of the
already cloned objects to their clones. In most scenarios,
when this method is called from outside, this can be left
as None.
preserve_read_only: Whether or not to preserve the read-only
behavior in the clone.
Returns:
The clone of the object.
"""
if memo is None:
memo = {}
if preserve_read_only:
return super().clone(memo=memo)
else:
return self._get_mutable_clone(memo=memo)
def __copy__(self) -> Any:
return self.clone(preserve_read_only=True)
def __deepcopy__(self, memo: Optional[dict]) -> Any:
if memo is None:
memo = {}
return self.clone(memo=memo, preserve_read_only=True)
clone(self, *, memo=None, preserve_read_only=False)
¶
Get a clone of this read-only object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
memo |
Optional[dict] |
Optionally a dictionary which maps from the ids of the already cloned objects to their clones. In most scenarios, when this method is called from outside, this can be left as None. |
None |
preserve_read_only |
bool |
Whether or not to preserve the read-only behavior in the clone. |
False |
Returns:
Type | Description |
---|---|
Any |
The clone of the object. |
Source code in evotorch/tools/cloning.py
def clone(self, *, memo: Optional[dict] = None, preserve_read_only: bool = False) -> Any:
"""
Get a clone of this read-only object.
Args:
memo: Optionally a dictionary which maps from the ids of the
already cloned objects to their clones. In most scenarios,
when this method is called from outside, this can be left
as None.
preserve_read_only: Whether or not to preserve the read-only
behavior in the clone.
Returns:
The clone of the object.
"""
if memo is None:
memo = {}
if preserve_read_only:
return super().clone(memo=memo)
else:
return self._get_mutable_clone(memo=memo)
Serializable (Clonable)
¶
Base class allowing the inheriting classes become Clonable and picklable.
Any class inheriting from Serializable
becomes Clonable
(since
Serializable
is a subclass of Clonable
) and therefore is expected to
define its own _get_cloned_state(...)
(see the documentation of the
class Clonable
for details).
A Serializable
class gains a behavior for its __getstate__
. In this
already defined and implemented __getstate__
method, the resulting
dictionary of _get_cloned_state(...)
is used as the state dictionary.
Therefore, for Serializable
objects, the behavior defined in their
_get_cloned_state(...)
methods affect how they are pickled.
Classes inheriting from Serializable
are evotorch.Problem
,
evotorch.Solution
, evotorch.SolutionBatch
, and
evotorch.distributions.Distribution
. In their _get_cloned_state(...)
implementations, these classes use deep_clone(...)
on themselves to make
sure that their contained PyTorch tensors are copied using the .clone()
method, ensuring that those tensors are detached from their old storages
during the cloning operation. Thanks to being Serializable
, their
contained tensors are detached from their old storages both at the moment
of copying/cloning AND at the moment of pickling.
Source code in evotorch/tools/cloning.py
class Serializable(Clonable):
"""
Base class allowing the inheriting classes become Clonable and picklable.
Any class inheriting from `Serializable` becomes `Clonable` (since
`Serializable` is a subclass of `Clonable`) and therefore is expected to
define its own `_get_cloned_state(...)` (see the documentation of the
class `Clonable` for details).
A `Serializable` class gains a behavior for its `__getstate__`. In this
already defined and implemented `__getstate__` method, the resulting
dictionary of `_get_cloned_state(...)` is used as the state dictionary.
Therefore, for `Serializable` objects, the behavior defined in their
`_get_cloned_state(...)` methods affect how they are pickled.
Classes inheriting from `Serializable` are `evotorch.Problem`,
`evotorch.Solution`, `evotorch.SolutionBatch`, and
`evotorch.distributions.Distribution`. In their `_get_cloned_state(...)`
implementations, these classes use `deep_clone(...)` on themselves to make
sure that their contained PyTorch tensors are copied using the `.clone()`
method, ensuring that those tensors are detached from their old storages
during the cloning operation. Thanks to being `Serializable`, their
contained tensors are detached from their old storages both at the moment
of copying/cloning AND at the moment of pickling.
"""
def __getstate__(self) -> dict:
memo = {id(self): self}
return self._get_cloned_state(memo=memo)
deep_clone(x, *, otherwise_deepcopy=False, otherwise_return=False, otherwise_fail=False, memo=None)
¶
A recursive cloning function similar to the standard deepcopy
.
The difference between deep_clone(...)
and deepcopy(...)
is that
deep_clone(...)
, while recursively traversing, will run the .clone()
method on the PyTorch tensors it encounters, so that the cloned tensors
are forcefully detached from their storages (instead of cloning those
storages as well).
At the moment of writing this documentation, the current behavior of
PyTorch tensors upon being deep-copied is to clone themselves AND their
storages. Therefore, if a PyTorch tensor is a slice of a large tensor
(which has a large storage), then the large storage will also be
deep-copied, and the newly made clone of the tensor will point to a newly
made large storage. One might instead prefer to clone tensors in such a
way that the newly made tensor points to a newly made storage that
contains just enough data for the tensor (with the unused data being
dropped). When such a behavior is desired, one can use this
deep_clone(...)
function.
Upon encountering a read-only and/or immutable data, this function will
NOT modify the read-only behavior. For example, the deep-clone of a
ReadOnlyTensor is still a ReadOnlyTensor, and the deep-clone of a
read-only numpy array is still a read-only numpy array. Note that this
behavior is different than the clone()
method of a ReadOnlyTensor
and the copy()
method of a numpy array. The reason for this
protective behavior is that since this is a deep-cloning operation,
the encountered tensors and/or arrays might be the components of the root
object, and changing their read-only attributes might affect the integrity
of this root object.
The deep_clone(...)
function needs to know what to do when an object
of unrecognized type is encountered. Therefore, the user is expected to
set one of these arguments as True (and leave the others as False):
otherwise_deepcopy
, otherwise_return
, otherwise_fail
.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
x |
Any |
The object which will be deep-cloned. This object can be a standard
Python container (i.e. list, tuple, dict, set), an instance of
Problem, Solution, SolutionBatch, ObjectArray, ImmutableContainer,
Clonable, and also any other type of object if either the argument
|
required |
otherwise_deepcopy |
bool |
Setting this as True means that, when an
unrecognized object is encountered, that object will be
deep-copied. To handle shared and cyclic-referencing objects,
the |
False |
otherwise_return |
bool |
Setting this as True means that, when an unrecognized object is encountered, that object itself will be returned (i.e. will be a part of the created clone). |
False |
otherwise_fail |
bool |
Setting this as True means that, when an unrecognized object is encountered, a TypeError will be raised. |
False |
memo |
Optional[dict] |
Optionally a dictionary. In most scenarios, when this function is called from outside, this is expected to be left as None. |
None |
Returns:
Type | Description |
---|---|
Any |
The newly made clone of the original object. |
Source code in evotorch/tools/cloning.py
def deep_clone( # noqa: C901
x: Any,
*,
otherwise_deepcopy: bool = False,
otherwise_return: bool = False,
otherwise_fail: bool = False,
memo: Optional[dict] = None,
) -> Any:
"""
A recursive cloning function similar to the standard `deepcopy`.
The difference between `deep_clone(...)` and `deepcopy(...)` is that
`deep_clone(...)`, while recursively traversing, will run the `.clone()`
method on the PyTorch tensors it encounters, so that the cloned tensors
are forcefully detached from their storages (instead of cloning those
storages as well).
At the moment of writing this documentation, the current behavior of
PyTorch tensors upon being deep-copied is to clone themselves AND their
storages. Therefore, if a PyTorch tensor is a slice of a large tensor
(which has a large storage), then the large storage will also be
deep-copied, and the newly made clone of the tensor will point to a newly
made large storage. One might instead prefer to clone tensors in such a
way that the newly made tensor points to a newly made storage that
contains just enough data for the tensor (with the unused data being
dropped). When such a behavior is desired, one can use this
`deep_clone(...)` function.
Upon encountering a read-only and/or immutable data, this function will
NOT modify the read-only behavior. For example, the deep-clone of a
ReadOnlyTensor is still a ReadOnlyTensor, and the deep-clone of a
read-only numpy array is still a read-only numpy array. Note that this
behavior is different than the `clone()` method of a ReadOnlyTensor
and the `copy()` method of a numpy array. The reason for this
protective behavior is that since this is a deep-cloning operation,
the encountered tensors and/or arrays might be the components of the root
object, and changing their read-only attributes might affect the integrity
of this root object.
The `deep_clone(...)` function needs to know what to do when an object
of unrecognized type is encountered. Therefore, the user is expected to
set one of these arguments as True (and leave the others as False):
`otherwise_deepcopy`, `otherwise_return`, `otherwise_fail`.
Args:
x: The object which will be deep-cloned. This object can be a standard
Python container (i.e. list, tuple, dict, set), an instance of
Problem, Solution, SolutionBatch, ObjectArray, ImmutableContainer,
Clonable, and also any other type of object if either the argument
`otherwise_deepcopy` or the argument `otherwise_return` is set as
True.
otherwise_deepcopy: Setting this as True means that, when an
unrecognized object is encountered, that object will be
deep-copied. To handle shared and cyclic-referencing objects,
the `deep_clone(...)` function stores its own memo dictionary.
When the control is given to the standard `deepcopy(...)`
function, the memo dictionary of `deep_clone(...)` will be passed
to `deepcopy`.
otherwise_return: Setting this as True means that, when an
unrecognized object is encountered, that object itself will be
returned (i.e. will be a part of the created clone).
otherwise_fail: Setting this as True means that, when an unrecognized
object is encountered, a TypeError will be raised.
memo: Optionally a dictionary. In most scenarios, when this function
is called from outside, this is expected to be left as None.
Returns:
The newly made clone of the original object.
"""
from .objectarray import ObjectArray
from .readonlytensor import ReadOnlyTensor
if memo is None:
# If a memo dictionary was not given, make a new one now.
memo = {}
# Get the id of the object being cloned.
x_id = id(x)
if x_id in memo:
# If the id of the object being cloned is already in the memo dictionary, then this object was previously
# cloned. We just return that clone.
return memo[x_id]
# Count how many of the arguments `otherwise_deepcopy`, `otherwise_return`, and `otherwise_fail` was set as True.
# In this context, we call these arguments as fallback behaviors.
fallback_behaviors = (otherwise_deepcopy, otherwise_return, otherwise_fail)
enabled_behavior_count = sum(1 for behavior in fallback_behaviors if behavior)
if enabled_behavior_count == 0:
# If none of the fallback behaviors was enabled, then we raise an error.
raise ValueError(
"The action to take with objects of unrecognized types is not known because"
" none of these arguments was set as True: `otherwise_deepcopy`, `otherwise_return`, `otherwise_fail`."
" Please set one of these arguments as True."
)
elif enabled_behavior_count == 1:
# If one of the fallback behaviors was enabled, then we received our expected input. We do nothing here.
pass
else:
# If the number of enabled fallback behaviors is an unexpected value. then we raise an error.
raise ValueError(
f"The following arguments were received, which is conflicting: otherwise_deepcopy={otherwise_deepcopy},"
f" otherwise_return={otherwise_return}, otherwise_fail={otherwise_fail}."
f" Please set exactly one of these arguments as True and leave the others as False."
)
# This inner function specifies how the deep_clone function should call itself.
def call_self(obj: Any) -> Any:
return deep_clone(
obj,
otherwise_deepcopy=otherwise_deepcopy,
otherwise_return=otherwise_return,
otherwise_fail=otherwise_fail,
memo=memo,
)
# Below, we handle the cloning behaviors case by case.
if (x is None) or (x is NotImplemented) or (x is Ellipsis):
result = deepcopy(x)
elif isinstance(x, (Number, str, bytes, bytearray)):
result = deepcopy(x, memo=memo)
elif isinstance(x, np.ndarray):
result = x.copy()
result.flags["WRITEABLE"] = x.flags["WRITEABLE"]
elif isinstance(x, (ObjectArray, ReadOnlyClonable)):
result = x.clone(preserve_read_only=True, memo=memo)
elif isinstance(x, ReadOnlyTensor):
result = x.clone(preserve_read_only=True)
elif isinstance(x, torch.Tensor):
result = x.clone()
elif isinstance(x, Clonable):
result = x.clone(memo=memo)
elif isinstance(x, (dict, OrderedDict)):
result = type(x)()
memo[x_id] = result
for k, v in x.items():
result[call_self(k)] = call_self(v)
elif isinstance(x, list):
result = type(x)()
memo[x_id] = result
for item in x:
result.append(call_self(item))
elif isinstance(x, set):
result = type(x)()
memo[x_id] = result
for item in x:
result.add(call_self(item))
elif isinstance(x, tuple):
result = []
memo[x_id] = result
for item in x:
result.append(call_self(item))
if hasattr(x, "_fields"):
result = type(x)(*result)
else:
result = type(x)(result)
memo[x_id] = result
else:
# If the object is not recognized, we use the fallback behavior.
if otherwise_deepcopy:
result = deepcopy(x, memo=memo)
elif otherwise_return:
result = x
elif otherwise_fail:
raise TypeError(f"Do not know how to clone {repr(x)} (of type {type(x)}).")
else:
raise RuntimeError("The function `deep_clone` reached an unexpected state. This might be a bug.")
if (x_id not in memo) and (result is not x):
# If the newly made clone is still not in the memo dictionary AND the "clone" is not just a reference to the
# original object, we make sure that it is in the memo dictionary.
memo[x_id] = result
# Finally, the result is returned.
return result