Home > Back-end >  Sorting a list while program is running vs after loading from file
Sorting a list while program is running vs after loading from file

Time:07-23

I have been hacking away at a dungeon crawler for a few months now. The player has a pack, self.pack =[], which eventually contains a list of weapon objects that are bought or accumulated during the game. The items are chosen from a dictionary and then appended:

    def blacksmith_sale(self):
        sale_items_dict = {'1': short_sword,
                           '2': broad_sword,
                           '3': quantum_sword
                           }
    ...
    sale_item_key = input("Enter item number to buy, your pack (I)nventory, or (E)xit the 
    blacksmith:").lower()
    ...
    sale_item = (sale_items_dict[sale_item_key])
            if self.gold >= sale_item.sell_price and self.level >= sale_item.minimum_level:
                print(f"You buy a {sale_item}")
                self.gold -= sale_item.buy_price
                self.pack.append(sale_item)
                time.sleep(1)
                self.hud()
                continue

Then I have an inventory function to view self.pack contents:

    def inventory(self):
        while True:
            if len(self.pack):
                self.hud()
                print("Your pack contains:")
                print("Item                       Quantity")
                print()
                self.pack.sort(key=lambda x: x.damage_bonus)  # sort each weapon by damage_bonus
                stuff_dict = {}
                for item in self.pack:
                    stuff_dict[item] = self.pack.count(item)
                for key, value in stuff_dict.items():
                    print(key, 's', ':    ', value, sep='')  # add an 's' and show quantity
                print()
            else:
                print("Your pack is empty")
            ...
           

This seems to work well while the game is running. However, after saving and reloading the player object with pickle, I am having weird sorting issues. While the game is running I get this kind of output:

Your pack contains:
Item                       Quantity

Short Swords:    1
Broad Swords:    1

However, after saving and reloading, if I were to append a 'short_sword' instance of the Sword class to self.pack, I get something like this:

Item                       Quantity

Short Swords:    1
Short Swords:    1
Broad Swords:    1

The problem gets ever more weird as more items are added and removed and the player is saved/loaded.

CodePudding user response:

self.pack.append(sale_item)

In above statement sale_item refers an object stored in a dictionary. and when you append sale_item/short_sword again and again, you are actually appending same object multiple times.

Hence, in below loop, the dict will treat all short_swords as same. and work fine

for item in self.pack:
    stuff_dict[item] = self.pack.count(item)

But when you dump player profile to persistence and re-load, it will create different object for different entry from file, in pack list. and when added to the dictionary, they will be treated different. so short_sword will be displayed multiple times.

Example:

>>> class test:
...     def __repr__(self):
...         return "test"
... 
>>> t1=test()
>>> t2=test()
>>> 
>>> # Case 1: same object added multiple times
>>> dict={}
>>> dict[t1]=1
>>> dict[t1]=2
>>> 
>>> print(dict)
{test: 2} <-- Note second one replaced first
>>> 
>>> # Case 2: different objects added
>>> dict={}
>>> dict[t1]=1
>>> dict[t2]=2
>>> 
>>> print(dict)
{test: 1, test: 2} <-- Note both objects added.
>>> 

CodePudding user response:

The items in the inventory are instances of custom classes. By default, such instances are hashable, that is they can be used as keys in dictionaries. However by default they also compare unequal, so if pack is

[short_sword, dagger, short_sword]

then your dictionary will end up with three keys.

A simple workaround is to use collections.Counter to build stuff_dict:

from collections import Counter

...

# I use name here, but use whatever attribute should be 
# used to mark instances as being the same thing.
stuff_dict = Counter(item.name for item in self.pack)

The resulting object will be a dict mapping name (or whatever) to the count of items in self.pack with that name.

Another approach would be to fix the way self.pack.count works. As it is, it will return 1 for each element in the list as they all compare unequal. Adding an __eq__ method to the Item class would fix this:

class Item:
...
    def __eq__(self, other):
        # See comment on using "name" in the Counter example
        return self.name == other.name

This will result in self.pack.count(item) returning the correct result, but instances will no longer be hashable unless you add a __hash__ method, so the key in stuff_dict would been to be the item's name (or equivalent). Adding __eq__ and __hash__ methods can lead to some tricky bugs if done incorrectly; I would be inclined to avoid this approach for now.

  • Related