Create a dictionary with two distinct set of keys

Viewed 64

Ok, so I have a dictionary of superheroes, with their name as keys, and a dictionary with a bunch of additional info:

heroes = {
    "Superman": {
        "weaknesses": ["kryptonite", "Martha"],
        "alter_ego": "Clark Kent",
        ...},
    "Batman": {
        "weaknesses": [],
        "alter_ego": "Bruce Wayne",
        ...},
    ...
   }

As I may plan to break into some manors in the city, I'd like to check that the owner of the house is not in fact a hero, or at least have enough info so that I can prepare accordingly.

With this data structure, the search is a bit cumberstone

victim_name = "Clark Kent"

# Check if the victim is not the alterego of an hero
def check_victim_is_hero(victim, heroes):
    alter_egos = [hero["alter_ego"] for  hero in heroes.values()]
    return victim in alter_egos

# I can also get the name of the specific hero
def check_which_hero_is_victim(victim, heroes):
    for hero_name, hero in heroes.items():
        if hero["alter_ego"] == victim:
            return hero_name

I can get the weakness by first getting the hero's name, then simply calling get on the original dictionary, or change a bit check_wich_hero_is_victim to return hero["weaknesses"] rather than hero_name.

However, either solution forces me to iterate over the dictionary, which as the information I gather grow, get more and more costly.

What I would like is to have an object from which I could call get either from superhero or alterego name, with a cost of O(1).

Something like:

heroes_double_index = ...

heroes_double_index.get_from_hero_name("Superman")
>>> {"name": "Superman", "alter_ego": "Clark Kent", "weaknesses":...}

heroes_double_index.get_from_alterego("Clark Kent")
>>> {...} Same dictionary as above

My idea for the moment is to have a class which two dictionaries with each set of key that I like:

class DoubleIndexDict:

    # Assuming we get the data as a list of records
    def __init__(self, data):
         self.dict_heroes = {hero["name"]: hero for hero in data}
         self.dict_alterego = {hero["alter_ego"]: hero for hero in data}

    def get_from_hero_name(self, name):
         return self.dict_heroes.get(name)

    def get_from_alter_ego(self, name):
         return self.dict_alterego.get(name)

However this does not fully satisfy me, most notably because I can change the values from inside each dictionary without affecting the way the query is done.

heroes_double_index.get_from_hero_name("Captain America")
>>> {"name": "Captain America", "alter_ego": "Steve Rogers"}


# Make the change
heroes_double_index.dict_heroes["Captain America"]["alter_ego"] = "Isaiah Bradley"

# The difference appears inside of the dictionary
heroes_double_index.get_from_hero_name("Captain America")
>>> {"name": "Captain America", "alter_ego": "Isaiah Bradley"}

# But I cannot use the new alterego for my search
heroes_double_index.get_from_alterego("Isaiah Bradley")
>>> None

Is there any established and more efficient way to do what I am trying to do in python ?

1 Answers

If you just want to be able to update both alter_ego and name simultaneously on O(1) (it wouldn't be a scalable solution for more keys, I think), then you could do something like this:

class Heroes:
def __init__(self, data):
    self.id_by_hero = {}
    self.id_by_alter_ego = {}
    self.data = {}

    for i, h in enumerate(data):
        self.id_by_hero[h["name"]] = str(i)
        self.id_by_alter_ego[h["alter_ego"]] = str(i)
        self.data[str(i)] = HeroDict(self, h)

def __getitem__(self, key):
    id = self.id_by_hero.get(key) or self.id_by_alter_ego.get(key)
    return self.data[id] if id else None

def update_key(self, key, old, new):
    if key == "name":
        self.id_by_hero[new] = self.id_by_hero.pop(old)
    if key == "alter_ego":
        self.id_by_alter_ego[new] = self.id_by_alter_ego.pop(old)

class HeroDict(object):
def __init__(self, parent, init=None):
    self.parent = parent
    if init is not None:
        self.__dict__.update(init)

def __setitem__(self, key, value):
    old = self.__dict__[key]
    self.__dict__[key] = value
    self.parent.update_key(key, old, value)

def __repr__(self):
    r = self.__dict__.copy()
    del r['parent']
    return repr(r)

by keeping the real data on just one dictionary, you wouldn't have to update both every time you change a value or choose where to get it from. Then, when you update a value, it would call update_key and just update the reference. This way, you'd get:

heroes_double_index["Captain America"]
>>> {'name': 'Captain America', 'weaknesses': [], 'alter_ego': 'Steve Rogers'}

heroes_double_index["Captain America"]["alter_ego"] = "Isaiah Bradley"

heroes_double_index["Captain America"]
>>> {'name': 'Captain America', 'weaknesses': [], 'alter_ego': 'Isaiah Bradley'}

heroes_double_index["Isaiah Bradley"]
>>> {'name': 'Captain America', 'weaknesses': [], 'alter_ego': 'Isaiah Bradley'}
Related