Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behaviour when subclassing datetime.timedelta

for convenience I want to create subclasses of datetime.timedelta. The idea is to define a class as such:

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

so I can quickly create timedeltas like that:

x = Hours(n)

However, the code above produces a timedelta of n days instead of n hours. As an example, look at the following ipython session:

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

I'm not able to explain this. Is anybody?

like image 772
jan Avatar asked Mar 20 '14 11:03

jan


1 Answers

If you use __new__, instead of __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)

yields

10:00:00

If you override __init__, but not __new__, then DT.timedelta.__new__ gets called before your Hours.__init__. Notice

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

x = Hours(10)

prints 10 days, 0:00:00. This shows that DT.timedelta.__new__ has already set the timedelta to 10 days before you even get a chance to configure it in Hours.__init__.

Moreover, DT.timedelta is an immutable object -- you can't change the days or seconds or microseconds after the object has been instantiated. Python creates immutable objects by using the __new__ method, and generally don't do anything in the __init__ method. Mutable objects do the reverse: they configure the object in __init__ and don't do anything in __new__.


Per the docs:

When subclassing immutable built-in types like numbers and strings, and occasionally in other situations, the static method __new__ comes in handy. __new__ is the first step in instance construction, invoked before __init__. The __new__ method is called with the class as its first argument; its responsibility is to return a new instance of that class. Compare this to __init__: __init__ is called with an instance as its first argument, and it doesn't return anything; its responsibility is to initialize the instance....

All this is done so that immutable types can preserve their immutability while allowing subclassing.

(If immutable objects performed configuration in __init__, then you could mutate an immutable by calling immutable.__init__. Obviously, we don't want that, so immutable.__init__ generally does nothing.)


Note also that unless you plan to add new methods to your Hours class, it would be simpler, and therefore better to just use a function:

def hours(hours):
    return DT.timedelta(hours=hours)
like image 126
unutbu Avatar answered Nov 15 '22 15:11

unutbu