Странное поведение при создании подклассов datetime.timedelta


Для удобства я хочу создать подклассы datetime.timedelta. Идея состоит в том, чтобы определить класс как таковой:

class Hours(datetime.timedelta):
    def __init__(self, hours):
        super(Hours, self).__init__(hours=hours)

Таким образом, я могу быстро создавать timedeltas так:

x = Hours(n)
Однако приведенный выше код производит timedelta из n дней вместо n часов. В качестве примера рассмотрим следующую сессию ipython:
In [1]: import datetime

In [2]: class Hours(datetime.timedelta):
   ...:     def __init__(self, hours):
   ...:         super(Hours, self).__init__(hours=hours)
   ...:         

In [3]: print(Hours(10))
Out[3]: 10 days, 0:00:00

Я не могу объяснить это. Есть кто-нибудь?

1 8

1 ответ:

Если вы используете __new__, вместо __init__:

import datetime as DT
class Hours(DT.timedelta):
    def __new__(self, hours):
        return DT.timedelta.__new__(self, hours=hours)
x = Hours(10)
print(x)

Выходы

10:00:00

Если вы переопределяете __init__, но не __new__, то DT.timedelta.__new__ вызывается перед вашим Hours.__init__. Примечание

import datetime as DT
class Hours(DT.timedelta):
    def __init__(self, hours):
        print(self)

x = Hours(10)

Отпечатки 10 days, 0:00:00. Это показывает, что DT.timedelta.__new__ уже установил timedelta на 10 дней, прежде чем вы даже получите возможность настроить его в Hours.__init__.

Более того, DT.timedelta является неизменяемым объектом-вы не можете изменить days или seconds или microseconds после того, как объект был создан. Python создает неизменяемые объекты с помощью метода __new__, и обычно ничего не делает в методе __init__. Изменяемые объекты делают обратное: они настраивают объект в __init__ и ничего не делают в __new__.


Per The docs :

При подклассовании неизменяемых встроенных типов, таких как числа и строки, и иногда в других ситуациях, статический метод __new__ приходит очень кстати. __new__ является первым шагом в построении экземпляра, вызываемого до __init__. Метод __new__ вызывается с классом в качестве его первый аргумент; его обязанность-вернуть новый экземпляр этого класс. Сравните это с __init__: __init__ вызывается с экземпляром как его первый аргумент, и он ничего не возвращает; его ответственность заключается в инициализации экземпляра....

Все это делается для того, чтобы неизменяемые типы могли сохранять свои неизменность при одновременном допуске подклассов.

(Если неизменяемые объекты выполняют конфигурацию в __init__, то вы можете мутировать неизменяемый вызовом immutable.__init__. Очевидно, мы этого не хотим, поэтому immutable.__init__ обычно ничего не делает.)


Обратите также внимание, что если вы не планируете добавлять новые методы в свой класс Hours, было бы проще, и поэтому лучше просто использовать функцию:

def hours(hours):
    return DT.timedelta(hours=hours)