I need to have a method to easily create an instance of a datetime.datetime subclass, given an existing datetime.datetime() instance.
Say I have the following contrived example:
class SerializableDateTime(datetime):
def serialize(self):
return self.strftime('%Y-%m-%d %H:%M')
I'm using a class like this (but a bit more complex), to use in a SQLAlchemy model; you can tell SQLAlchemy to map a custom class to a supported DateTime column value with a TypeDecorator class; e.g.:
class MyDateTime(types.TypeDecorator):
impl = types.DateTime
def process_bind_param(self, value, dialect):
# from custom type to the SQLAlchemy type compatible with impl
# a datetime subclass is fine here, no need to convert
return value
def process_result_value(self, value, dialect):
# from SQLAlchemy type to custom type
# is there a way have this work without accessing a lot of attributes each time?
return SerializableDateTime(value) # doesn't work
I can't use return SerializableDateTime(value) here because the default datetime.datetime.__new__() method doesn't accept a datetime.datetime() instance:
>>> value = datetime.now()
>>> SerializableDateTime(value)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type datetime.datetime)
Is there a shortcut that avoids having to copy value.year, value.month, etc. all the way down to the timezone into a constructor?
Although you can give your subclass a __new__ method that detects a single datetime.datetime instance then does all the copying there, I'd actually give the class a classmethod just to handle this case, so your SQLAlchemy code would look like:
return SerializableDateTime.from_datetime(value)
We can make use of the pickle support the datetime.datetime() class already implements; types implement the __reduce_ex__ hook (usually building on higher-level methods like __getnewargs__), and for datetime.datetime() instances this hook returns just the datetime.datetime type and an args tuple, meaning that as long as you have a subclass with the same internal state we can create a new copy with the same state by applying the args tuple back to your new type. The __reduce_ex__ method can vary output by pickle protocol, but as long as you pass in pickle.HIGHEST_PROTOCOL you are guaranteed to get the full supported range of values.
The args tuple consists of one or two values, the second being the timezone:
>>> from pickle import HIGHEST_PROTOCOL
>>> value = datetime.now()
>>> value.__reduce_ex__(HIGHEST_PROTOCOL)
(<class 'datetime.datetime'>, (b'\x07\xe2\n\x1f\x12\x06\x05\rd\x8f',))
>>> datetime.utcnow().astimezone(timezone.utc).__reduce_ex__(value.__reduce_ex__(HIGHEST_PROTOCOL))
(<class 'datetime.datetime'>, (b'\x07\xe2\n\x1f\x12\x08\x14\n\xccH', datetime.timezone.utc))
That first value in the args tuple is a bytes value that represents all attributes of the object (excepting the timezone), and the constructor for datetime accepts that same bytes value (plus an optional timezone):
>>> datetime(b'\x07\xe2\n\x1f\x12\x06\x05\rd\x8f') == value
True
Since your subclass accepts the same arguments, you can make use of the args tuple to create a copy; we can use the first value to guard against changes in future Python versions by asserting it's still a parent class of ours:
from pickle import HIGHEST_PROTOCOL
class SerializableDateTime(datetime):
@classmethod
def from_datetime(cls, dt):
"""Create a SerializableDateTime instance from a datetime.datetime object"""
# (ab)use datetime pickle support to copy state across
factory, args = dt.__reduce_ex__(HIGHEST_PROTOCOL)
assert issubclass(cls, factory)
return cls(*args)
def serialize(self):
return self.strftime('%Y-%m-%d %H:%M')
This lets you create instances of your subclass as a copy:
>>> SerializableDateTime.from_datetime(datetime.now())
SerializableDateTime(2018, 10, 31, 18, 13, 2, 617875)
>>> SerializableDateTime.from_datetime(datetime.utcnow().astimezone(timezone.utc))
SerializableDateTime(2018, 10, 31, 18, 13, 22, 782185, tzinfo=datetime.timezone.utc)
While using the pickle __reduce_ex__ hook may seem somewhat hackish, this is the actual protocol used to create copies of datetime.datetime instances with the copy module as well, and by using __reduce_ex__(HIGHEST_PROTOCOL) you ensure that all relevant state is copied whatever the Python version you are using.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With