Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using python's property while loading old objects

I have a rather large project, including a class Foo which recently needed to be updated using the @property decorator to create custom getter and setter methods.

I also stored several instances of Foo on my hard drive that at some point I might need to reload. My problem is, that I cannot access the attributes decoreted with property on these old objects.

Consider the following example:

import pickle

# define Class and create instance
class Foo:
    def __init__(self):
        self.val = 1
foo = Foo()

# dump foo into file
with open("foo.pickle", 'wb') as handle:
    pickle.dump(foo, handle, pickle.HIGHEST_PROTOCOL)

# overwrite and add @property in the class definition
class Foo:
    def __init__(self):
        self._val = "new_foo"

    @property
    def val(self):
        return self._val

    @val.setter
    def val(self, val):
        self._val = val

foo_new = Foo()
print(foo_new.val)

# reload foo
with open("foo.pickle", "rb") as handle:
    foo_old = pickle.load(handle)

# try to access attributes
print(foo_old.val)

The last line raises:

NameError: name '_val' is not defined

What options do I have to still access the attributes of my archived instances?

Edit: Changed self.val to self._val in the constructor of the second Foo-definition.

like image 888
physicsGuy Avatar asked Mar 09 '18 15:03

physicsGuy


3 Answers

The pickle documentation says:

When a class instance is unpickled, its __init__() method is usually not invoked.

Which is why the _val attribute wasn't defined You can workaround that by defining a __new__ method in the replacement Foo class and setting the instance attribute there:

import pickle

# define Class and create instance
class Foo:
    def __init__(self):
        self.val = 1
foo = Foo()

# dump foo into file
with open("foo.pickle", 'wb') as handle:
    pickle.dump(foo, handle, pickle.HIGHEST_PROTOCOL)

# overwrite and add @property in the class definition
class Foo:
    def __new__(cls, val=None):
        inst = super().__new__(cls)
        inst._val = "new_foo"  if val is None else val
        return inst

    @property
    def val(self):
        return self._val

    @val.setter
    def val(self, val):
        self._val = val

foo_new = Foo()
print(foo_new.val)  # -> new_foo

# reload foo
with open("foo.pickle", "rb") as handle:
    foo_old = pickle.load(handle)

print(foo_old.val)  # -> new_foo
like image 157
martineau Avatar answered Oct 12 '22 16:10

martineau


A possibility is to use a custom Unpickler, although you would need to keep the old class around (with a different name, hidden if you want) and define the logic to convert an object of the old class to the new one. Here's a basic example:

import pickle

# define Class and create instance
class Foo:
    def __init__(self):
        self.val = 1
foo = Foo()

# dump foo into file
with open("foo.pickle", 'wb') as handle:
    pickle.dump(foo, handle, pickle.HIGHEST_PROTOCOL)

# Old class is kept with a new name
FooOld = Foo

# overwrite and add @property in the class definition
class Foo:
    def __init__(self):
        self._val = "new_foo"

    @property
    def val(self):
        return self._val

    @val.setter
    def val(self, val):
        self._val = val

foo_new = Foo()
print(foo_new.val)

# Custom Unpickler
class FooOldUnpickler(pickle.Unpickler):
    def __init__(self, *args, **kwargs):
        super(FooOldUnpickler, self).__init__(*args, **kwargs)

    def load(self):
        obj = super(FooOldUnpickler, self).load()
        if type(obj) is FooOld:
            # Object conversion logic
            newObj = Foo()
            newObj.val = obj.val
            obj = newObj
        return obj

    def find_class(self, module, name):
        # Use old class instead of new for loaded objects
        if module == __name__ and name == 'Foo':
            return FooOld
        return super(FooOldUnpickler, self).find_class(module, name)

# reload foo
with open("foo.pickle", "rb") as handle:
    # Use custom unpickler
    foo_old = FooOldUnpickler(handle).load()

# try to access attributes
print(foo_old.val)
like image 1
jdehesa Avatar answered Oct 12 '22 14:10

jdehesa


This may be a total hack -- I'm not certain. However, I was able to reconstruct an object pickled from your first "Foo" class using the following code;

import pickle

class Foo:
    def __init__(self):
        self._val = "new_foo"

    @property
    def val(self):
        try:
            return self._val
        except AttributeError:
            self._val = self.__dict__['val']
            self.__dict__.pop('val')
            return self._val

    @val.setter
    def val(self, val):
        self._val = val


with open("foo.pickle", "rb") as handle:
    foo_old = pickle.load(handle)

print(foo_old.val)
like image 1
SteveJ Avatar answered Oct 12 '22 16:10

SteveJ