HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

"Multi-key" dictionary

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
dictionarymultikey

Problem

I was making a relatively large-scale project in Python as a learning experiment, and in the process I found that it would be useful to have a map (or, in Python, I guess they're called dictionaries) where a value could have multiple keys. I couldn't find much support for this out there, so this is the implementation I ended up going with:

def range_dict(*args):
    return_val = {}
    for k,v in args:
        for i in k:
            return_val[i] = v
    return return_val

COINS = range_dict((range(1,15), None),
                     (range(15,30), "1d6x1000 cp"),
                     (range(30,53), "1d8x100 sp"),
                     (range(53,96), "2d8x10 gp"),
                     (range(96,101), "1d4x10 pp"))


I was told that this wasn't really a good way to achieve what I wanted to do, but I don't really understand why. It works perfectly in my application. What's wrong with this? Is there a better, more Pythonic way to do this?

Solution


  1. Comments on your code



-
No docstrings! What does your code do and how am I supposed to use it?

-
No test cases. This ought to be a good opportunity to use doctests.

-
Given that your purpose here is to implement RPG lookup tables, your design seems a bit hard to use. The printed RPG table probably says something like this:

1-8     giant spider
9-12    skeleton
13-18   brass dragon
...


but you have to input it like this (adding one to the limit of each range):

range_dict((range(1, 9), "giant spider"),
           (range(9, 13), "skeleton"),
           (range(13, 18), "brass dragon"),
           ...)


which would be easy to get wrong.

-
The design also seems fragile, because it doesn't ensure that the ranges are contiguous. If you made a data entry mistake:

d = range_dict((range(1, 8), "giant spider"),
               (range(9, 13), "skeleton"),
               (range(13, 18), "brass dragon"),
               ...)


then the missing key would later result in an error:

>>> d[8]
Traceback (most recent call last):
  File "", line 1, in 
KeyError: 8


  1. An alternative approach



Here's how I'd implement this lookup table:

from bisect import bisect_left
from collections.abc import Mapping

class LookupTable(Mapping):
    """A lookup table with contiguous ranges of small positive integers as
    keys. Initialize a table by passing pairs (max, value) as
    arguments. The first range starts at 1, and second and subsequent
    ranges start at the end of the previous range.

    >>> t = LookupTable((10, '1-10'), (35, '11-35'), (100, '36-100'))
    >>> t[10], t[11], t[100]
    ('1-10', '11-35', '36-100')
    >>> t[0]
    Traceback (most recent call last):
      ...
    KeyError: 0
    >>> next(iter(t.items()))
    (1, '1-10')

    """
    def __init__(self, *table):
        self.table = sorted(table)
        self.max = self.table[-1][0]

    def __getitem__(self, key):
        key = int(key)
        if not 1 <= key <= self.max:
            raise KeyError(key)
        return self.table[bisect_left(self.table, (key,))][1]

    def __iter__(self):
        return iter(range(1, self.max + 1))

    def __len__(self):
        return self.max


Notes on this implementation:

-
The constructor takes the maximum for each range (not the maximum plus one), making data entry less error-prone.

-
The minimum of each range is taken from the end of the previous range, thus ensuring that there are no gaps between ranges.

-
The collections.abc.Mapping abstract base class provides much of the functionality of a read-only dictionary. The idea is that you supply the __getitem__, __iter__ and __len__ methods, and the Mapping class supplies implementations of __contains__, keys, items, values, get, __eq__, and __ne__ methods for you. (Which you can override if you need to, but that's not necessary here.)

-
I've used bisect.bisect_left to efficiently look up keys in the sorted table.

Code Snippets

1-8     giant spider
9-12    skeleton
13-18   brass dragon
...
range_dict((range(1, 9), "giant spider"),
           (range(9, 13), "skeleton"),
           (range(13, 18), "brass dragon"),
           ...)
d = range_dict((range(1, 8), "giant spider"),
               (range(9, 13), "skeleton"),
               (range(13, 18), "brass dragon"),
               ...)
>>> d[8]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 8
from bisect import bisect_left
from collections.abc import Mapping

class LookupTable(Mapping):
    """A lookup table with contiguous ranges of small positive integers as
    keys. Initialize a table by passing pairs (max, value) as
    arguments. The first range starts at 1, and second and subsequent
    ranges start at the end of the previous range.

    >>> t = LookupTable((10, '1-10'), (35, '11-35'), (100, '36-100'))
    >>> t[10], t[11], t[100]
    ('1-10', '11-35', '36-100')
    >>> t[0]
    Traceback (most recent call last):
      ...
    KeyError: 0
    >>> next(iter(t.items()))
    (1, '1-10')

    """
    def __init__(self, *table):
        self.table = sorted(table)
        self.max = self.table[-1][0]

    def __getitem__(self, key):
        key = int(key)
        if not 1 <= key <= self.max:
            raise KeyError(key)
        return self.table[bisect_left(self.table, (key,))][1]

    def __iter__(self):
        return iter(range(1, self.max + 1))

    def __len__(self):
        return self.max

Context

StackExchange Code Review Q#31907, answer score: 9

Revisions (0)

No revisions yet.