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

Replace keys in values with key->values from same dictionary

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

Problem

I have a dictionary whose values may contain keys that are already in the dictionary. What we are actually talking about are parameters. Sometimes a parameter is defined by a number plus another parameter, etc.

It looks something like this:

paramDict = {'ad': '2*dmcg/factor*(xw/factor)',
'dmcg': 2.05e-07,
'dxwn_3vna': 10.0,
'factor': 1,
'xw': '0+dxwn_3vna'}


If there is another parameter in the value it is of type string, otherwise it is an int or a float. After the replacing I want the value to be a float.

The code I wrote to solve this problem is this:

for _ in range(10):
        for key, value in paramDict.items():
            if type(value) is str:
                matchList = re.findall('[a-z]\w*', value)
                matchList = [match for match in matchList if match != 'e']
                for match in matchList:
                    param = paramDict[match]
                    paramDict[key] = value.replace(match, str(param))
                try:
                    paramDict[key] = eval(paramDict[key])
                except:
                    pass


It works as far as I can tell, but it just doesn't feel right to just repeat the whole process a finite number of times and hope that all the strings have been replaced. Is there a safer way?

Solution

Well what you want there is evaluating expressions while keeping track
of dependencies, so you should definitely not implement it via regular
expressions and hoping that running it ten times is enough.

The best way in Python would probably be using the
ast module to parse and
figure out the referenced variables, then calculate dependencies and
evaluate the parsed expressions via
compile
and eval
while passing in the referenced variables in the local parameter.

That way you make sure that the expressions are actually valid, you can
use Python expressions and you can (and should) also detect cycles in
the specification.

Below is a sketch how to do that, not saying that is the best code,
please edit or comment with improvements, I couldn't find a good example
while searching.

import ast, _ast

params = {
    'ad': '2*dmcg/factor*(xw/factor)',
    'dmcg': 2.05e-07,
    'dxwn_3vna': 10.0,
    'factor': 1,
    'xw': '0+dxwn_3vna',
    'foo': 'bar',
    'bar': 'foo'
}

parsed = {}

for key, value in params.iteritems():
    parsed[key] = value

    if type(value) in (str, unicode):
        parsed[key] = ast.parse(value, mode="eval")

evaluated = {key: value for key, value in parsed.iteritems()}


So at this point the evaluated dictionary is mostly a copy of the
params, except that expressions have been parsed.

def referenced(code):
    return [node.id for node in ast.walk(code) if "id" in node._fields]


referenced returns a list with the names of referenced variables in
the expression.

evaluating = []

def resolve(key, value):
    if key in evaluating:
        raise Exception("Loop while evaluating {}".format(key))

    if type(value) is not _ast.Expression:
        evaluated[key] = value
        return value

    evaluating.append(key)

    locals = {name: resolve(name, evaluated[name]) for name in referenced(value)}

    result = eval(compile(value, "", "eval"), globals(), locals)

    evaluated[key] = result

    evaluating.pop()

    return result


resolve evaluates the expression recursively but also keeps track of
the stack (in evaluating), so it can detect cycles by looking up the
current variable. It also assigns the results immediately, so in
further invocations it will return early (the not _ast.Expression
case).

for key, value in parsed.iteritems():
    try:
        resolve(key, value)
    except Exception as exception:
        print ("Error while evaluating {}: {}".format(key, exception))

print(evaluated)


Lastly, iterate over all entries and try to evaluate them.

Code Snippets

import ast, _ast

params = {
    'ad': '2*dmcg/factor*(xw/factor)',
    'dmcg': 2.05e-07,
    'dxwn_3vna': 10.0,
    'factor': 1,
    'xw': '0+dxwn_3vna',
    'foo': 'bar',
    'bar': 'foo'
}

parsed = {}

for key, value in params.iteritems():
    parsed[key] = value

    if type(value) in (str, unicode):
        parsed[key] = ast.parse(value, mode="eval")

evaluated = {key: value for key, value in parsed.iteritems()}
def referenced(code):
    return [node.id for node in ast.walk(code) if "id" in node._fields]
evaluating = []

def resolve(key, value):
    if key in evaluating:
        raise Exception("Loop while evaluating {}".format(key))

    if type(value) is not _ast.Expression:
        evaluated[key] = value
        return value

    evaluating.append(key)

    locals = {name: resolve(name, evaluated[name]) for name in referenced(value)}

    result = eval(compile(value, "<unknown>", "eval"), globals(), locals)

    evaluated[key] = result

    evaluating.pop()

    return result
for key, value in parsed.iteritems():
    try:
        resolve(key, value)
    except Exception as exception:
        print ("Error while evaluating {}: {}".format(key, exception))

print(evaluated)

Context

StackExchange Code Review Q#80273, answer score: 6

Revisions (0)

No revisions yet.