patternpythonMinor
"Multi-key" dictionary
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:
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?
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
- 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- 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.maxNotes 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: 8from 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.maxContext
StackExchange Code Review Q#31907, answer score: 9
Revisions (0)
No revisions yet.