Reputation: 8039
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?
Upvotes: 3
Views: 656
Reputation: 1121744
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.
Upvotes: 5