pythonpickle

Loading python classes using pickle gives error: init missing arguments


I have a class which contains some basic information as well as other classes, when saving this class the attribute buildings gives error upon loading TypeError: Building.__init__() missing 2 required positional arguments: 'name' and 'jobs' without the saving and loading function it works perfectly fine.

the code for the classes is-

class Kingdom:
    def __init__(self, total_water=100_000, water_add=667) -> None:
        self.people = []
        self.unemployed_people = []
        self.buildings = []
        self.resources = []
        self.total_water = total_water
        self.water_added = water_add

    def add_building(self, name, x, y):
        "this function will return -1 if the kingdon does not have the required resouces"
        for value in building_info[name]["cost"]:
            for i, item in enumerate(self.resources):
                new_item = item - value
                if not isinstance(new_item, int):
                    self.resources[i] = new_item
                    break
            else:
                return -1
        width, height = assets[name].get_size()
        self.buildings.append(Building(x, y, width, height, name, building_info[name]["jobs"]))

    def display(self, window, x_offset=0, y_offset=0, resource_display_y_offset=0):
        for building in self.buildings:
            building.display(window, x_offset, y_offset)

        # items
        for i, item in enumerate(self.resources):
            blit_text(
                window,
                item.name + "   " + str(item.count),
                (
                    0,
                    i * text_size + text_size * 0.2 * i - 5 + resource_display_y_offset,
                ),
                size=text_size,
            ).get_width()
            try:
                window.blit(
                    assets[item.name],
                    (
                        resource_div_rect.x - default_item_size * 2,
                        i * text_size + text_size * 0.2 * i + resource_display_y_offset,
                    ),
                )
            except KeyError:
                window.blit(
                    assets["Missing Item"],
                    (
                        resource_div_rect.x - default_item_size * 2,
                        i * text_size + text_size * 0.2 * i + resource_display_y_offset,
                    ),
                )

    def tick(self):
        self.total_water += self.water_added
        for person in self.people:
            remaining_resources = person.job.work(self.resources)
            if not isinstance(remaining_resources, int):
                for i, item in enumerate(remaining_resources):
                    if item.name == "water" and self.total_water < item.count:
                        remaining_resources.pop(i)
                        break
                    if item.name == "water":
                        self.total_water -= item.count
                        break
                self.resources = remaining_resources
        for i, item in enumerate(self.resources):
            if item.name == "person":
                for j in range(item.count):
                    self.unemployed_people.append(Person())
                self.resources.pop(i)

    def employ_all_people(self):
        for i, person in enumerate(self.unemployed_people):
            for building in self.buildings:
                if building.jobs > 0:
                    self.unemployed_people.pop(i)
                    self.people.append(Person(building))
                    building.jobs -= 1
                    break


class Person:
    def __init__(self, job=None) -> None:
        self.job = job


class Item:
    def __init__(self, name, count, tag=None) -> None:
        self.name = name
        self.count = count
        self.tag = tag

    def __add__(self, other):
        """This function will return -1 if it cannot add the values"""
        if isinstance(other, int) or isinstance(other, float):
            return Item(self.name, self.count + other, self.tag)
        if not isinstance(other, Item):
            return -1
        if self.name == other.name or (
            (self.tag == other.tag or self.tag == other.name or self.name == other.tag)
            and self.tag is not None
        ):
            return Item(self.name, self.count + other.count, self.tag)
        return -1

    def __sub__(self, other):
        """This function will return -1 if it cannot subtract the values"""
        if (
            isinstance(other, int) or isinstance(other, float)
        ) and self.count > other.count:
            return Item(self.name, self.count - other, self.tag)
        if not isinstance(other, Item):
            return -1
        if (
            self.name == other.name
            or (
                (
                    self.tag == other.tag
                    or self.tag == other.name
                    or self.name == other.tag
                )
                and self.tag is not None
            )
        ) and self.count > other.count:
            return Item(self.name, self.count - other.count, self.tag)
        return -1

    def __repr__(self) -> str:
        return (
            "{name: "
            + str(self.name)
            + ", count: "
            + str(self.count)
            + ",  tag: "
            + str(self.tag)
            + "}"
        )


class Building(pg.Rect):
    def __init__(self, x, y, width, height, name, jobs):
        super().__init__(x, y, width, height)
        self.name = name
        self.jobs = jobs
        self.manual = True

    def display(self, window, x_offset, y_offset):
        if self.x - x_offset < resource_div_rect.x:
            return
        if self.bottom - y_offset > div_rect.y:
            return
        window.blit(assets[self.name], (self.x - x_offset, self.y - y_offset))

    def work(self, resources, called_as_click=False):
        "this function will return -1 if there are insufficent resoureces"
        if self.manual and not called_as_click:
            return -1

        for item in building_info[self.name]["in"]:
            for i, value in enumerate(resources):
                new_item = value - item
                if not isinstance(new_item, int):
                    resources[i] = new_item
                    break
            else:
                return -1
        for item in building_info[self.name]["out"]:
            for i, value in enumerate(resources):
                new_item = value + item
                if not isinstance(new_item, int):
                    resources[i] = new_item
                    break
            else:
                resources.append(item)
        return resources

The full code repository for anyone interested is - https://github.com/IGR2020/PaperCiv Note it doesn't have a bugged code

I tried to make the name and job arguments default, and it loaded it but now the arguments were defaulted so I could not change the building data. I save the data using file = open and data = pickle.load method, it works on other types of data just fine.

Also if there is some other way to save a list of the kingdom class with all the other classes which are a part of it to memory please suggest it.

The saving and loading code is -

save_name = "main.pkl"
# loading world data / creating new world
if isfile(f"kingdom data/{save_name}"):
    file = open(f"kingdom data/{save_name}", "rb")
    kingdoms = pickle.load(file)
    file.close()
    main_kingdom = kingdoms["Main"]

else:
    kingdoms = {"Main": Kingdom()}
    main_kingdom = kingdoms["Main"]
    main_kingdom.resources = [
        Item("wheat", 100, "food"),
        Item("water", 100),
        Item("wood", 50),
        Item("person", 1),
    ]

then the saving code is in the quit condition of the game loop -

for event in pg.event.get():
        if event.type == pg.QUIT:

            # saving current world data
            file = open(f"kingdom data/{save_name}", "wb")
            pickle.dump(kingdoms, file)
            file.close()

            run = False

The error comes after I open the app then place anyone building (i.e the well) then I quit and reopen the app and the error comes into play. The full code - https://github.com/IGR2020/PaperCiv/tree/Saving-and-Loading This does have the bugged code.

Here is the MRE -

import pygame as pg
import pickle


class Kingdom:
    def __init__(self, total_water=100_000, water_add=667) -> None:
        self.people = []
        self.unemployed_people = []
        self.buildings = []
        self.resources = []
        self.total_water = total_water
        self.water_added = water_add


class Person:
    def __init__(self, job=None) -> None:
        self.job = job


class Item:
    def __init__(self, name, count, tag=None) -> None:
        self.name = name
        self.count = count
        self.tag = tag


class Building(pg.Rect):
    def __init__(self, x, y, width, height, name, jobs):
        super().__init__(x, y, width, height)
        self.name = name
        self.jobs = jobs
        self.manual = True

kingdom = Kingdom()
kingdom.buildings.append(Building(0, 0, 0, 0, 0, 0))
file = open("main.pkl", "wb")
pickle.dump(kingdom, file)
file.close()
file = open("main.pkl", "rb")
kingdom = pickle.load(file)
file.close()

My computer model is DELL Inspiron Intel Core i5 7th Gen 7200U, This code is running outside of a virtual environment, using pygame 2.5.2 and python 3.12.2 on windows 10. I hope a solution can be found using this MRE.


Solution

  • You can't inherit from pygame.Rect - it has some custom pickling behaviour that interferes with your adding extra arguments to the __init__ function. Maybe you could instead store a pygame.Rect inside your Building?

    class Building:
        def __init__(self, x, y, width, height, name, jobs):
            self.rect = pg.Rect(x, y, width, height)
            self.name = name
            self.jobs = jobs
            self.manual = True