2

我正在尝试对 datetime 类进行子类化,以便我的主要代码看起来更干净。但是,对我的子类进行任何算术运算会将数据类型更改回 datetime.datetime。

我拿了我的原始代码并将其缩减为一个最小的示例。

from datetime import datetime, timedelta

class worldtime(datetime):
   UTC = True
   tz_offset = timedelta(hours = 4)

   def __new__(cls, *args, **kwargs):
      #kwargs['tzinfo'] = dateutil.tz.tzutc()
      return super().__new__(cls, *args, **kwargs)

   def is_UTC(self):
      return self.UTC

   def to_local(self):
      print(f"type(self): {type(self)}")
      if self.UTC is True:
         self = self - self.tz_offset
         print(f"type(self): {type(self)}")
         self.UTC = False
         return self

dt = worldtime(2019, 8, 26, 12, 0, 0)
print (f"dt = {dt}   is_UTC(): {dt.is_UTC()}")
print (f"type(dt): {type(dt)}")
print (f"dir(dt): {dir(dt)}")
dt = dt.to_local()

在我减去 tz_offset timedelta 的那一刻,对象的类型变回 datetime.datetime:

dt = 2019-08-26 12:00:00   is_UTC(): True
type(dt): <class '__main__.worldtime'>
dir(dt): ['UTC', '__add__', '__class__', '__delattr__', '__dict__', 
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', 
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__radd__', 
'__reduce__', '__reduce_ex__', '__repr__', '__rsub__', '__setattr__', 
'__sizeof__', '__str__', '__sub__', '__subclasshook__', '__weakref__', 
'astimezone', 'combine', 'ctime', 'date', 'day', 'dst', 'fold', 
'fromisoformat', 'fromordinal', 'fromtimestamp', 'hour', 'is_UTC', 
'isocalendar', 'isoformat', 'isoweekday', 'max', 'microsecond', 'min', 
'minute', 'month', 'now', 'replace', 'resolution', 'second', 'strftime', 
'strptime', 'time', 'timestamp', 'timetuple', 'timetz', 'to_local', 
'today', 'toordinal', 'tz_offset', 'tzinfo', 'tzname', 'utcfromtimestamp', 
'utcnow', 'utcoffset', 'utctimetuple', 'weekday', 'year']
type(self): <class '__main__.worldtime'>
type(self): <class 'datetime.datetime'>
Traceback (most recent call last):
  File "testwt.py", line 33, in <module>
    dt.to_local()
  File "testwt.py", line 27, in to_local
    self.UTC = False
AttributeError: 'datetime.datetime' object has no attribute 'UTC'

我可以承认对 python 中的子类不熟悉。虽然我看过其他似乎在谈论这个问题的帖子,但没有可遵循的示例。我见过的最好的是我必须重写 __sub__ 运算符,但我不确定如何做到这一点并确保返回的对象是正确的类型。同样,没有任何清晰的代码示例可以使用......

更新:更正了示例代码中的一个小错误,因为 worldtime.to_local() 需要将新实例返回给主代码。

4

2 回答 2

2

重要的一行是这一行,在to_local()方法中:

self = self - self.tz_offset

而不是更改self(this worldtimeobject) 以使其现在代表本地时间,您实际上是将其设置为一个全新的对象,特别是self - self.tz_offset.

那么为什么那个结果不是一个worldtime对象呢?

请注意,此计算中的对象类型是worldtime- timedelta。目前你还没有做任何事情来指定如何对你的worldtime类执行减法,所以自动从它的父类( )worldtime继承它的减法行为。datetime但这意味着它被视为普通datetime对象(毕竟,它实际上是一个datetime,只是带有几个额外的属性和方法)。

所以 Python 进行了一个datetime-timedelta计算,结果是一个datetime对象,然后将其赋值给self. 这就是为什么您的worldtime对象似乎正在“更改”为datetime.

我们怎样才能让它发挥作用?

有两种选择:

1) 更新我们的对象而不是创建一个新对象

如果我们知道我们的偏移量总是只有几个小时,我们可以这样做:

def to_local(self):
    if self.UTC is True:
        self.hour = self.hour + self.tz_offset.hours
        self.UTC = False

但这不起作用,因为(与我最初的预期相反!):

  1. tz_offset没有hours属性(当您创建timedelta它时,它会将时间存储为天、秒和微秒)
  2. datetime对象不允许您hour像这样直接设置

我们可以尝试更改_hour属性(这是datetime在内部存储时间的方式),但是像这样更改“私有”属性通常是个坏主意。另外,我们仍然需要tz_offset回到小时来进行计算,如果我们稍后想要偏移小时和分钟会发生什么?我们需要确保我们的偏移量不会让我们跨越日期边界......(可能还有其他我们没有想到的问题!)

最好让datetime做它擅长的事情,所以:

2a) 让我们datetime处理减法,但将结果转换回worldtime

def to_local(self):
    if self.UTC is True:
        new_time = self - self.tz_offset
        self = worldtime(
            new_time.year,
            new_time.month,
            new_time.day,
            new_time.hour,
            new_time.minute,
            new_time.second,
        )
        self.UTC = False

或者,正如您所提到的,您可以定义__sub__()特殊方法来定义-运算符对我们的worldtime对象执行的操作。

2b)-__sub__()

to_local()让我们离开

def to_local(self):
    if self.UTC is True:
        self = self - self.tz_offset
        self.UTC = False

但是改变它的-行为方式。在这里,我们基本上是将我们在2a中所做的事情转移到一个单独的方法__sub__()中,称为(如减法)。这意味着当 Python 命中 时-,它会将左右操作数作为和(分别)传递给__sub__()特殊方法,然后返回该方法的结果。selfother

    def __sub__(self, other):
    new_time = self - other
    return worldtime(
        new_time.year,
        new_time.month,
        new_time.day,
        new_time.hour,
        new_time.minute,
        new_time.second,
    )

但是当我们运行它时,我们会收到如下错误:

RecursionError: maximum recursion depth exceeded

发生了什么?

当 Python 进入时self,它会调用. 到目前为止,一切都很好。但是当它到达 inside 时,我们仍在对一个对象进行减法运算,因此 Python 尽职尽责地一次又一次地调用……一次又一次,然后陷入无限循环!self.tz_offsetto_local()__sub__(self, self.tz_offset)self - other__sub__()worldtime__sub__(self, other)

我们不希望那样。相反,一旦我们进入,__sub__()我们只想做正常的datetime减法。所以它应该是这样的:

    def __sub__(self, other):
    new_time = super().__sub__(other)
    return worldtime(
        new_time.year,
        new_time.month,
        new_time.day,
        new_time.hour,
        new_time.minute,
        new_time.second,
    )

在这里,super().__sub__(other)意味着我们正在使用__sub__()父类上的方法。在这里,就是datetime,所以我们得到一个datetime对象,并可以从中创建一个新worldtime对象。


整个事情(与您的打印语句)现在看起来像这样:

from datetime import datetime, timedelta


class worldtime(datetime):
    UTC = True
    tz_offset = timedelta(hours = -4)

    def __new__(cls, *args, **kwargs):
        #kwargs['tzinfo'] = dateutil.tz.tzutc()
        return super().__new__(cls, *args, **kwargs)

    def is_UTC(self):
        return self.UTC

    def to_local(self):
        print(f"type(self): {type(self)}")
        if self.UTC is True:
            self = self - self.tz_offset
            print(f"type(self): {type(self)}")
            print(self)
            self.UTC = False

    def __sub__(self, other):
        new_time = super().__sub__(other)
        return worldtime(
            new_time.year,
            new_time.month,
            new_time.day,
            new_time.hour,
            new_time.minute,
            new_time.second,
        )


dt = worldtime(2019, 8, 26, 12, 0, 0)
print (f"dt = {dt}   is_UTC(): {dt.is_UTC()}")
print (f"type(dt): {type(dt)}")
print (f"dir(dt): {dir(dt)}")
dt.to_local()

(我改为 4 空格制表符,这是 Python 中的标准)


但是......这是最好的方法吗?

希望这能回答您关于 Python 中的子类化的问题。

但考虑到这个问题,我不确定这是否是最好的方法。子类化内置函数可能很复杂并且容易出错,而datetimes 本身已经很复杂并且容易出错。子类化datetime的意义不大,因为在创建后更改它们并不简单,并且创建一个新对象并将其设置为self感觉不是很整洁。

我想知道使用组合而不是继承是否会更好。所以worldtime会在内部存储一个datetime对象,您可以对其进行操作,并使用datetime模块中的时区支持来管理您的时区转换,也许只是即时执行以返回本地时间。

就像是:

from datetime import datetime, timedelta, timezone


class WorldTime:
    OFFSET = timedelta(hours=-4)

    # assumes input time is in UTC, not local time
    def __init__(self, year, month=None, day=None, hour=0, minute=0, second=0,
                 microsecond=0, tzinfo=timezone.utc, *, fold=0):
        self.dt_in_utc = datetime(year, month, day, hour, minute, second,
                                  microsecond, tzinfo, fold=fold)

    # convert to our timezone, and then make naive ("local time")
    def to_local(self):
        return self.dt_in_utc.astimezone(timezone(self.OFFSET)).replace(tzinfo=None)


dt = WorldTime(2019, 8, 26, 12, 0, 0)
print(dt.to_local())

# Gives:
# 2019-08-26 08:00:00

我已经做到了,它to_local()返回一个datetime对象,然后你可以打印出来,或者以后做任何你想做的事情。



编辑

我有另一个关于继承的实验datetime,我认为以下应该可行:

from datetime import datetime, timedelta, timezone


class WorldTime(datetime):
    OFFSET = timedelta(hours=-4)

    def __new__(cls, *args, tzinfo=timezone.utc, **kwargs):
        return super().__new__(cls, *args, tzinfo=tzinfo, **kwargs)

    def __add__(self, other):
        result = super().__add__(other)
        return WorldTime(*result.timetuple()[:6], tzinfo=result.tzinfo,
                          fold=result.fold)

    def __sub__(self, other):
        "Subtract two datetimes, or a datetime and a timedelta."
        if not isinstance(other, datetime):
            if isinstance(other, timedelta):
                return self + -other
            return NotImplemented
        return super().__sub__(other)

    def to_local(self):
        return self.astimezone(timezone(self.OFFSET)).replace(tzinfo=None)

dt = WorldTime(2019, 8, 26, 12, 0, 0)
print(dt)
print(dt.to_local())  # local time
print(dt + timedelta(days=20, hours=7))  # 20 days, 7 hours in the future
print(dt - timedelta(days=40, hours=16))  # 40 days, 16 hours in the past
print(dt - WorldTime(2018, 12, 25, 15, 0, 0))  # time since 3pm last Christmas Day


# Output:
# 2019-08-26 12:00:00+00:00  # WorldTime
# 2019-08-26 08:00:00  # datetime
# 2019-09-15 19:00:00+00:00  # WorldTime
# 2019-07-16 20:00:00+00:00  # WorldTime
# 243 days, 21:00:00  # timedelta

所以看起来timedeltas 的加法和减法返回一个WorldTime对象,我们可以找到两个WorldTime对象之间的差异作为 a timedelta

但是,这没有经过严格测试,因此请谨慎操作!

于 2019-08-28T01:12:22.890 回答
1

减去(子)类的结果datetime将始终返回一个datetime实例。在查看模块中的实现时会变得很明显__add__(self, other)datetime本质上只是在从实例中减去实例__sub__(self, other)时将计算转发到加法函数):timedeltadatetime

class datetime(date):

    ...

    def __sub__(self, other):
        "Subtract two datetimes, or a datetime and a timedelta."
        if not isinstance(other, datetime):
            if isinstance(other, timedelta):  # This is True in our case
                return self + -other  # This is calling the __add__ function
            return NotImplemented

        # The remainder of the __sub__ function is omitted as we are 
        # focussing on the case in which a timedelta instance is subtracted 
        # from a datetime instance.

    def __add__(self, other):
        "Add a datetime and a timedelta."
        if not isinstance(other, timedelta):
            return NotImplemented
        delta = timedelta(self.toordinal(),
                          hours=self._hour,
                          minutes=self._minute,
                          seconds=self._second,
                          microseconds=self._microsecond)
        delta += other
        hour, rem = divmod(delta.seconds, 3600)
        minute, second = divmod(rem, 60)
        if 0 < delta.days <= _MAXORDINAL:
            return type(self).combine(date.fromordinal(delta.days),
                                      time(hour, minute, second,
                                           delta.microseconds,
                                           tzinfo=self._tzinfo))
        raise OverflowError("result out of range")

这里的关键是该_add__函数创建一个新timedelta实例,然后使用该.combine()函数创建一个新输出。

我将向您展示如何解决此行为的两个示例:

  1. 覆盖类方法combine(cps, date, time, tzinfo=True)

    class worldtime
    
        ...
    
        @classmethod
        def combine(cls, date, time, tzinfo=True):
            "Construct a datetime from a given date and a given time."
            if not isinstance(date, _date_class):
                raise TypeError("date argument must be a date instance")
            if not isinstance(time, _time_class):
                raise TypeError("time argument must be a time instance")
            if tzinfo is True:
                tzinfo = time.tzinfo
            return cls(date.year, date.month, date.day,
                       time.hour, time.minute, time.second, time.microsecond,
                       tzinfo, fold=time.fold)
    

    这现在应该调用构造函数worldtime而不是父类datetime并返回worldtime. 由于该combine函数是从许多现有的魔术方法中调用的,因此它应该有望涵盖其他情况(和算术运算)。

  2. 覆盖__sub__(self, other)方法:

    class worldtime:
    
        ...
    
        def __sub__(self, other):
            # the subtraction will turn sub into an instance of datetime
            # as we‘re calling the original subtraction function of datetime
            sub = super(worldtime, self).__sub__(other)
    
            # timetuple returns the parameters (year, month, day, etc.) 
            # and we need the first six parameters only to create a new instance.
            return worldtime(*sub.timetuple()[:6])
    

    这会将 and 之间的差异selfother已变成datetime)转换回worldtime使用其构造函数的实例。

第一个选项可能更简洁,因为它将应用于datetime. 第二个选项将要求您向其他算术运算添加更多特殊情况,并可能导致更大的实施和维护工作。

于 2019-08-28T01:37:52.817 回答