如何重写python3 __sub__函数,以便不更改数据类型

时间:2019-08-27 19:00:10

标签: python python-3.x datetime subclass

我正在尝试对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()需要将新实例返回到主代码中。

2 个答案:

答案 0 :(得分:2)

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

self = self - self.tz_offset

您无需将self(此worldtime对象)更改为现在代表本地时间,而是将其设置为全新的对象,特别是{{1}的结果}。

那为什么不self - self.tz_offset对象的结果呢?

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

因此,Python执行datetime-datetime计算,结果是一个timedelta对象,然后将其分配给datetime。这就是为什么您的self对象似乎被“改变”为worldtime

我们如何使它起作用?

有两种选择:

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

如果我们知道偏移量总是只有几个小时,则可以执行以下操作:

datetime

起作用,是因为(与我最初的预期相反!)

  1. def to_local(self): if self.UTC is True: self.hour = self.hour + self.tz_offset.hours self.UTC = False 没有tz_offset属性(当您创建hours时,它将时间存储为天,秒和微秒)
  2. timedelta对象不允许您像这样直接设置datetime

我们可以尝试更改hour属性(这是_hour内部存储时间的方式),但是像这样更改'private'属性通常是一个坏主意。另外,我们仍然必须将datetime转换为小时才能进行计算,如果我们以后想用小时和分钟来补偿时会发生什么呢?并且我们需要确保我们的偏移量不会使我们跨越日期界限...(以及可能还有其他我们尚未想到的问题!)

最好让tz_offset做自己擅长的事情,所以:

2a)让datetime处理减法,但将结果转回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__()运算符对-对象的作用。

2b)用worldtime覆盖-运算符

让我们__sub__()离开

to_local()

但是更改def to_local(self): if self.UTC is True: self = self - self.tz_offset self.UTC = False 的行为。在这里,我们基本上是将我们在 2a 中所做的事情转移到称为-的单独方法中(如 sub 牵引)。这意味着,当Python命中__sub__()时,它将左右操作数分别作为-__sub__()传递到self特殊方法中,然后返回结果方法的

other

,当我们运行此程序时,会收到如下错误:

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

发生了什么事?

Python在RecursionError: maximum recursion depth exceeded 中击中self-self.tz_offset时,它将调用to_local()。到目前为止,一切都很好。但是当它到达__sub__(self, self.tz_offset)内的self - other时,我们仍在对__sub__()对象进行减法运算,因此Python会尽责地一次又一次地调用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

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


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

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

但是考虑到这个问题,我不确定这是否是最好的方法。内建子类化很复杂,很容易出错,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() 本身已经很复杂并且很容易出错。子类化datetime的意义不大,因为在创建后对其进行更改并不容易,创建一个新对象并将其设置为datetime感觉并不整洁。

我想知道使用 composition 代替 inheritance 是否会更好。因此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 的加减会返回一个timedelta对象,我们可以找到两个WorldTime对象之间的差异作为WorldTime

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

答案 1 :(得分:1)

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

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

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

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