Here is my solution:
def make_extendable(o):
"""
Return an object that can be extended via its __dict__
If it is a slot, the object type is copied and the object is pickled through
this new type, before returning it.
If there is already a __dict__, then the object is returned.
"""
if getattr(o, "__dict__", None) is not None:
return o
# Now for fun
# Don't take care of immutable types or constant for now
import copy
import copyreg
cls = o.__class__
new_cls = type(cls.__name__, (cls,), {"__module__": cls.__module__})
# Support only Python >= 3.4
pick = o.__reduce_ex__(4)
if pick[0] == cls:
# This is the case for datetime objects
pick = (new_cls, *pick[1:])
elif pick[0] in (copyreg.__newobj__, copyreg.__newobj_ex__):
# Now the second item in pick is (cls, )
# It should be rare though, it's only for slots
pick = (pick[0], (new_cls,), *pick[2:])
else:
return ValueError(f"Unable to extend {o} of type {type(o)}")
# Build new type
return copy._reconstruct(o, None, *pick)
It basically do the following:
- Test if the object already has a
__dict__
. In this case, there is nothing to do.
- Create a new type, based on the provided object type. This new type is not a slot class, and try to mimics the base type as much as possible.
- Reduce the provided object as done in
copy.copy
, but only supports __reduce_ex__(4)
for simplicity.
- Modify the reduced version to use the newly created type.
- Unpickle the new object using the modified reduced version.
The result for datetime
:
In [13]: d = make_extendable(datetime.datetime.now())
In [14]: d
Out[14]: datetime(2019, 3, 29, 11, 24, 23, 285875)
In [15]: d.__class__.__mro__
Out[15]: (datetime.datetime, datetime.datetime, datetime.date, object)
In [16]: d.__str__ = lambda: 'Hello, world'
In [17]: d.__str__()
Out[17]: 'Hello, world'
Caveats
In random order:
- Some types may not be reduced.
- The returned object is a copy, not the initial one.
- The class is not the same, but
isinstance(d, datetime.datetime)
will be True
.
- The class hierarchy will betray the hack.
- It may be incredibly slow.
__format__
is a bit special, because you need to change the class instance, instead of the bound method because of how format works.
- <Insert Your Negative Critic Here>.