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

Python Dictionary Black Magic

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

Problem

I am defining a subclass of a the python dictionary object mpCmd where every item is converted to a lambda. The intended usage is that every item in the dictionary can be called with a single list argument (row) that will return a value based on row. Integers and optionally strings are considered indexes of the row. When converted to indexes, strings are converted as though they were Excel style column names.

A single integer n will become lambda row: row[n]. A tuple with a function and a sequence of indexes transforms as

(func, (n1, n2, n3)) => lambda row: func(row[n1], row[n2], row[n3])

Here are some basic examples

>>>command = mpCmd({0: 0, 'B': (sum, ([1, 2, 4],)), 'C': 'SPAM'})
>>>command[0](['Idle', 'Palin', 'Cleese', 'Chapman', 'Gilliam', 'Jones']))
'Idle'
>>>command[1]([1, 2, 3, 4, 5])
10
>>>command[2](ANYTHING)
'SPAM'


I have tested it and it works. Any advice appreciated, even if it's just confirming this is black magic.

```
def rmap(func, sequence):
return [rmap(func, i) if isinstance(i, (tuple, list))
#elif isinstance(i, dict) ???
else func(i)
for i in sequence]

def name_to_index(col_name):
"""Converts Excel Style column name to zero offset index
"""
return reduce((lambda index, char: index*26 + int(char, 36) - 9),
col_name, 0) - 1

class mpCmd(dict):
"""Stores user defined maps and converts them to f(vector) = scalar

Every item stored in mpCmd will be converted to int: (lambda row: some_func)
"""

def __init__(self, map_dict, offset=0,
int_is_index=True, str_is_name=False):
self.offset = offset
self.int_is_index = int_is_index
self.str_is_name = str_is_name
super(mpCmd, self).__init__(self._convert_dict(map_dict))

# Override setters, do no override accessors
def __setitem__(self, key, val):
super(mpCmd, self).__setitem__(*self._convert_item(key, val))

def update(se

Solution

I have a few points I would like to suggest.

Firstly, I would rename the name_to_index function to fit the verb-first function naming convention. Based on your other naming conventions, I would use something like convert_name_to_index.

The next thing I would do is rename your mpCmd class. The Pythonic convention is to use PascalCase instead of camelCase for class names. Also, shortening words can be handy in some places:

management --> mgt
character --> char


While, in others places it can inhibit understanding:

# Is this a comfy cat? A comfy cot? Comfy cut?
ct.description = 'Comfy'


Now, in your context it is pretty easy to know what the name is supposed to mean. However, especially in class names, its better to have too many characters than have too few. I would rename your class to something like CommandMap.

My next points touch on your convert_val function.

Python understands the conditional in your if-statement just fine (because or has a higher precidence than and). However, to the human eye, it can be ambiguous:

# Is the grouping this?
if (part1 and part2) or (part3 and part4):

# Or this?
if ((part1 and part2) or part3) and part4:

# Or this?
if (part1 and (part2 or part3)) and part4:


I would use the structure of my first example above in your code to help the statement feel less ambiguous.

The second part of convert_val function that I would change is your assert callable(func) line. The assert keyword is generally used in testing and just feels odd here. Here is how I would structure that section of code:

elif isinstance(val, (tuple, list)):
    func, indexes = val
    indexes = rmap(self._convert_key, indexes)

    if not callable(func):
        raise TypeError('{} is not callable'.format(func))

    return (lambda row: func(*rmap((lambda i: row[i]), indexes)))


My final point is in your convert_key function. If the input is not either an int or a str then you raise TypeError. As you have it now this is what prints:

>>>raise TypeError
Traceback (most recent call last):
  File "", line 1, in 
TypeError


Instead of raising the class TypeError, raise an instance of type TypeError with its own message:

>>>raise TypeError('Key must be an int or a string.')
Traceback (most recent call last):
  File "", line 1, in 
TypeError: Key must be an int or a string.


Other than these suggestions, your code looks nice.

Code Snippets

management --> mgt
character --> char
# Is this a comfy cat? A comfy cot? Comfy cut?
ct.description = 'Comfy'
# Is the grouping this?
if (part1 and part2) or (part3 and part4):

# Or this?
if ((part1 and part2) or part3) and part4:

# Or this?
if (part1 and (part2 or part3)) and part4:
elif isinstance(val, (tuple, list)):
    func, indexes = val
    indexes = rmap(self._convert_key, indexes)

    if not callable(func):
        raise TypeError('{} is not callable'.format(func))

    return (lambda row: func(*rmap((lambda i: row[i]), indexes)))
>>>raise TypeError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError

Context

StackExchange Code Review Q#51247, answer score: 3

Revisions (0)

No revisions yet.