Search code examples
pythondatetimepicklepython-datetime

Unable to pickle datetime subclass


I am trying to pickle and unpickle datetime.datetime subclass object. However, it always yields error and I have no clue why it is and how to solve it. Here is the minimum example:

from datetime import datetime, date, time 
class A(datetime):
    def __new__(cls, year = 2016, month=1, day=1, hour=0, minute=0, leap_year=False):
        return datetime.__new__(cls, year ,month, day, hour, minute)

import pickle
obj = A( month=1, day=1, hour=1, leap_year = False)
serial = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
unpickle = pickle.loads( serial, encoding='bytes')

This will give the following error:

TypeError                                 Traceback (most recent call last)
<ipython-input-2-12195a09d83d> in <module>()
      5 
      6 
----> 7 unpickle = pickle.loads( serial, encoding='bytes')

<ipython-input-1-605483566b52> in __new__(cls, year, month, day, hour, minute, leap_year)
      2 class A(datetime):
      3     def __new__(cls, year = 2016, month=1, day=1, hour=0, minute=0, leap_year=False):
----> 4         return datetime.__new__(cls, year ,month, day, hour, minute)
      5 

TypeError: an integer is required (got type bytes)

Does anyone know where the problem might be and how to solve it?


Solution

  • I was able to get the following to work based on the information in the Pickling Class Instances section of the pickle module's documentation. The tuple of values returned from the __reduce_ex__() method that has been added will cause the __new__() constructor to be called when the class instance is unpickled by the pickle.loads() call — just like what normally happens when you call a class.

    Note that I didn't need to know whether or how datetime customizes pickling nor understand its C implementation.

    Also note that since the leap_year argument you had was being ignored by the implementation in your question (and it's unclear why it would need to be passed to the initializer anyway), I've replaced it with a Python property that computes a boolean value for it dynamically based on the current instance's year value.

    from datetime import datetime, date, time
    import pickle
    
    
    class A(datetime):
        def __new__(cls, year=2016, month=1, day=1, hour=0, minute=0):
            return datetime.__new__(cls, year, month, day, hour, minute)
    
        def __reduce_ex__(self, protocol):
            return (type(self), (self.year, self.month, self.day, self.hour, self.minute))
    
        @property
        def is_leapyear(self):
            ''' Determine if specified year is leap year. '''
            year = self.year
            if year % 4 != 0:
                return False
            elif year % 100 != 0:
                return True
            elif year % 400 != 0:
                return False
            else:
                return True
    
    obj = A(month=1, day=1, hour=1, leap_year=False)
    print('calling pickle.dumps()')
    serial = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
    print('calling pickle.loads()')
    unpickle = pickle.loads(serial, encoding='bytes')
    print('unpickled {!r}'.format(unpickle))  # -> unpickled A(2016, 1, 1, 1, 0)
    print('unpickle.is_leapyear: {}'.format(unpickle.is_leapyear))  # -> True