Search code examples
pythonjsonserializationpicklemarshmallow

Marshmallow Creating Duplicate Python Custom Objects on De-Serialization


I am a beginner and I am trying to teach myself object oriented programming by writing a simple text adventure in Python. My goal is to eventually host the game on the web so I can invite friends to try it.

Entities in the game are represented by Python objects. For example, there is a Door class which includes the world state attributes open_state (i.e. is the door open). In turn, Door objects are attributes of Room objects.

The app runs at the console with no issues. Now my goal is to re-arrange the code for web usage. To do this I have broken my code into two pieces:

  1. the front-end code that gathers user input, sends it to the back-end execution code, and then presents the resultant output.

  2. the back-end execution code itself which interprets user input and determines the results of the user actions. As part of the back-end coding I also need to persist my custom objects between back-end execution calls.

I eventually plan to use sqlalchemy to persist state but to start with I’m just saving my custom objects to text files. My approach is to serialize and deserialize to JSON using Marshmallow. However, because I am referencing objects within my objects I end up with duplicate objects when I deserialize from Marshmallow. This is wreaking havoc with my game and renders it unplayable.

Is there a way to deserialize using Marshmallow (or Pickle or any similar package) that allows me to just reference existing object instances rather than instantiating new, duplicate object instances? Am I simply going about this the entirely wrong way? I have done a lot of searching on this topic and have found no similar posts so I suspect I am doing something fundamentally wrong?

Many thanks for any help or wisdom anyone can provide!

# Dark Castle - Minimum Workable Exampe
# Demonstrates Marshmallow duplication issue
# July 16, 2021

# imports
from marshmallow import Schema, fields, post_load
import gc

# classes
class Door(object):
        def __init__(self, name, desc, open_state):
                self.name = name
                self.desc = desc
                self.open_state = open_state # True if door is open

        def __repr__(self):
                return f'Object { self.name } is of class { type(self).__name__ } '

class Room(object):
        def __init__(self, name, desc, room_doors):
                self.name = name
                self.desc = desc
                self.room_doors = room_doors # list of door objs in room

        def __repr__(self):
                return f'Object { self.name } is of class { type(self).__name__ } '

# object instantiation
front_gate = Door('front_gate', "An imposing iron front gate", False)
entrance = Room('entrance', "You are at the castle entrance.", [front_gate])

# marshmallow schemas
class DoorSchema(Schema):
        name = fields.String()
        desc = fields.String()
        open_state = fields.Boolean()

        @post_load
        def create_door(self, data, **kwargs):
                return Door(**data)

class RoomSchema(Schema):
        name = fields.String()
        desc = fields.String()
        room_doors = fields.List(fields.Nested(DoorSchema), allow_none=True)

        @post_load
        def create_room(self, data, **kwargs):
                return Room(**data)

# check initial Door object count
print("Initial list of door objects:")
for obj in gc.get_objects():
        if isinstance(obj, Door):
                print(obj, obj.open_state, id(obj))
print()

# serialize to text file
schema_door = DoorSchema()
door_json = schema_door.dumps(front_gate)
schema_room = RoomSchema()
room_json = schema_room.dumps(entrance)
json_lst = [door_json, room_json]
with open('obj_json.txt', 'w') as f:
    for item in json_lst:
        f.write("%s\n" % item)
print("JSON output")
print(json_lst)
print()

# delete objects
del json_lst
del front_gate
del entrance
print("Door objects have been deleted:")
for obj in gc.get_objects():
        if isinstance(obj, Door):
                print(obj, obj.open_state, id(obj))
print()

# de-serialize from text file
with open('obj_json.txt', 'r') as f:
        new_json_lst = f.readlines()
print("JSON input")
print(new_json_lst)
print()
new_door_json = new_json_lst[0]
new_room_json = new_json_lst[1]
front_gate = schema_door.loads(new_door_json)
entrance = schema_room.loads(new_room_json)
print("Duplicate de-serialized Door objects:")
for obj in gc.get_objects():
        if isinstance(obj, Door):
                print(obj, obj.open_state, id(obj))

Output:

Initial list of door objects:    
Object front_gate is of class Door  False 4648526904

JSON output
['{"open_state": false, "name": "front_gate", "desc": "An imposing iron front gate"}', '{"room_doors": [{"open_state": false, "name": "front_gate", "desc": "An imposing iron front gate"}], "name": "entrance", "desc": "You are at the castle entrance."}']

Door objects have been deleted:

JSON input    
['{"open_state": false, "name": "front_gate", "desc": "An imposing iron front gate"}\n', '{"room_doors": [{"open_state": false, "name": "front_gate", "desc": "An imposing iron front gate"}], "name": "entrance", "desc": "You are at the castle entrance."}\n']

Duplicate de-serialized Door objects:
Object front_gate is of class Door  False 4710446696
Object front_gate is of class Door  False 4710446864

Solution

  • I’d love to see an answer from a more authoritative source than myself but testing shows that pickle solves this problem nicely. I had been keen on a human-readable output format like JSON. But python-native appears to be the way to go if your custom objects contain other custom objects. As a side benefit, it’s a lot less work than Marshmallow too - no schemas to define or @post_load statements needed.

    Test code:

    # Dark Castle - Minimum Viable Example 2
    # Will attempt to solve Marshmallow to json object duplication issue w/ pickle
    # July 24, 2021
    
    # imports
    import pickle
    import gc
    
    # classes
    class Door(object):
            def __init__(self, name, desc, open_state):
                    self.name = name
                    self.desc = desc
                    self.open_state = open_state # True if door is open
    
            def __repr__(self):
                    return f'Object { self.name } is of class { type(self).__name__ } '
    
    class Room(object):
            def __init__(self, name, desc, room_doors):
                    self.name = name
                    self.desc = desc
                    self.room_doors = room_doors # list of door objs in room
    
            def __repr__(self):
                    return f'Object { self.name } is of class { type(self).__name__ } '
    
    # object instantiation
    front_gate = Door('front_gate', "An imposing iron front gate", False)
    entrance = Room('entrance', "You are at the castle entrance.", [front_gate])
    obj_lst = [front_gate, entrance]
    
    # check initial Door object count
    print("Initial list of door objects:")
    for obj in gc.get_objects():
            if isinstance(obj, Door):
                    print(obj, obj.open_state, id(obj))
    print()
    
    
    # serialize to pickle file
    with open('obj_pickle', 'wb') as f:
            pickle.dump(obj_lst, f)
    
    # delete objects
    del obj_lst
    del front_gate
    del entrance
    
    # check Door object count post delete
    print("Door objects have been deleted:")
    for obj in gc.get_objects():
            if isinstance(obj, Door):
                    print(obj, obj.open_state, id(obj))
    print()
    
    # de-serialize from pickle file
    with open('obj_pickle', 'rb') as f:
            obj_lst_2 = pickle.load(f)
    front_gate = obj_lst_2[0]
    entrance = obj_lst_2[1]
    
    # check initial Door object count post de-serialize:
    print("de-serialized Door objects:")
    for obj in gc.get_objects():
            if isinstance(obj, Door):
                    print(obj, obj.open_state, id(obj))
    

    Output:

    Initial list of door objects:
    Object front_gate is of class Door  False 4624118840
    
    Door objects have been deleted:
    
    de-serialized Door objects:
    Object front_gate is of class Door  False 4624118840