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

标签 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()需要将新实例返回到主代码。

最佳答案

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

self = self - self.tz_offset


您实际上没有将self(此worldtime对象)更改为现在的本地时间,而是将其设置为全新的对象,特别是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


但是这将无法工作,因为(与我最初的预期相反!):


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


我们可以尝试更改_hour属性(这是datetime在内部存储时间的方式),但是像这样更改'private'属性通常不是一个好主意。另外,我们仍然必须将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__()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,
    )


但是,当我们运行此命令时,会出现如下错误:

RecursionError: maximum recursion depth exceeded


发生了什么?

当Python在self中命中self.tz_offset-to_local()时,它将调用__sub__(self, self.tz_offset)。到目前为止,一切都很好。但是当它到达self - other中的__sub__()时,我们仍在对worldtime对象进行减法运算,因此Python会尽职尽责地反复调用__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子类化的问题。

但是考虑到这个问题,我不确定这是否是最好的方法。内建子类化很复杂,很容易出错,datetime本身已经很复杂并且很容易出错。子类化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对象,您可以随后将其打印出,或随后执行任何操作。




这个问题涵盖了类似的内容:
Convert a python UTC datetime to a local datetime using only python standard library?
此参考相当不错:
https://howchoo.com/g/ywi5m2vkodk/working-with-datetime-objects-and-timezones-in-python




编辑

我还做了另一个从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对象之间的差异作为timedelta

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

关于python - 如何重写python3 __sub__函数,以便不更改数据类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57680778/

相关文章:

python - 将发布数据传递到脚本 flask

python - 导入错误 : cannot import name _args_from_interpreter_flags

javascript - 在 Bokeh 交互式线图中从 ColumnDataSource 选择行

python - Python 中的 str.join() 和 str().join() 有什么区别?

python - 使用 Python3 在 AWS lambda 中进行多线程处理

php - 在 PHP 中与 MySQL 日期时间分开获取年/月/日

php - 如何查看时间是否与MYSQL中已存在的记录重叠?

Python date.today() 不返回本地日期

python - 在 python 中写入文件时分割内容错误

python - 是否可以让装饰器在 asyncio 执行器中运行阻塞函数?