【问题标题】:Custom datetime subclass that can be created from an existing datetime instance?可以从现有日期时间实例创建的自定义日期时间子类?
【发布时间】:2018-10-31 17:21:41
【问题描述】:

在给定现有datetime.datetime() 实例的情况下,我需要一种方法来轻松创建datetime.datetime 子类的实例。

假设我有以下人为的例子:

class SerializableDateTime(datetime):
    def serialize(self):
        return self.strftime('%Y-%m-%d %H:%M')

我正在使用这样的类(但有点复杂),用于 SQLAlchemy 模型;您可以告诉 SQLAlchemy 使用 TypeDecorator class 将自定义类映射到受支持的 DateTime 列值;例如:

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

我不能在这里使用return SerializableDateTime(value),因为默认的datetime.datetime.__new__() 方法不接受datetime.datetime() 实例:

>>> 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)

是否有一种快捷方式可以避免将value.yearvalue.month 等一直复制到时区到构造函数中?

【问题讨论】:

  • @MartijnPieters 问题是value 已经属于datetime 类型,但我无法用另一个datetime 对象初始化SerializableDateTime(它实际上从datetime 继承__init__) .所以SerializableDateTime(value)TypeError: an integer is required (got type datetime.datetime) 失败
  • 这不是 SQLAlchemy 问题,这纯粹是您的自定义类的问题。
  • @MartijnPieters 绝对是,我不怀疑这可能是 sqlalchemy 问题

标签: python datetime inheritance python-datetime


【解决方案1】:

虽然您可以为您的子类提供一个 __new__ 方法来检测单个 datetime.datetime 实例然后在那里进行所有复制,但实际上我会给该类一个 classmethod 来处理这种情况,因此您的 SQLAlchemy 代码看起来喜欢:

return SerializableDateTime.from_datetime(value)

我们可以利用pickle 支持已经实现的datetime.datetime() 类;类型实现了__reduce_ex__ hook(通常建立在像__getnewargs__这样的高级方法上),对于datetime.datetime()实例,这个钩子只返回datetime.datetime类型和一个args元组,这意味着只要你有一个具有相同内部状态的子类,我们可以通过将args 元组应用回您的新类型来创建具有相同状态的新副本。 __reduce_ex__ 方法可以根据 pickle 协议改变输出,但只要您传入 pickle.HIGHEST_PROTOCOL,就可以保证获得完整支持的值范围。

args 元组由一个或两个值组成,第二个是时区:

>>> 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))

args 元组中的第一个值是 bytes 值,表示对象的所有属性(时区除外),datetime 的构造函数接受相同的字节值(加上可选的时区):

>>> datetime(b'\x07\xe2\n\x1f\x12\x06\x05\rd\x8f') == value
True

由于您的子类接受相同的参数,您可以使用args 元组来创建一个副本;我们可以通过断言它仍然是我们的父类来使用第一个值来防止未来 Python 版本的变化:

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')

这使您可以创建子类的实例作为副本:

>>> 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)

虽然使用pickle __reduce_ex__ 钩子可能看起来有些骇人听闻,但这是用于创建datetime.datetime 实例副本的实际协议以及copy module,并且通过使用__reduce_ex__(HIGHEST_PROTOCOL),您可以确保所有相关状态复制您使用的任何 Python 版本。

【讨论】:

  • 太棒了,非常感谢!我以前从未见过reduce hook,真的很有趣
  • @OlehRybalchenko:提醒:我检查了日期时间源代码,并被提醒在 Python 3.6 及更高版本中,datetime objects have new .fold attribute 不会在旧的泡菜值中腌制,但你可能想要带来更新的 Python 版本。我已经更新了答案,以确保您可以在未来进行进一步的此类添加。
猜你喜欢
  • 2018-09-16
  • 2013-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多