Dave
Dave

Reputation: 6554

Python: linking/binding variables together in a dictionary

first post, so play nice!

I have a fairly basic question about Python dictionaries. I would like to have some dict value that updates when another variable is changed (or at least recalculated when it is next called) - eg:

mass = 1.0
volume = 0.5
mydict = {'mass':mass,'volume':volume}
mydict['density'] = mydict['mass']/mydict['volume']

So in this case, mydict['density'] just returns 2.0. If I change mydict['mass'] = 2.0, the density will not be updated. Fine - I can kind of understand why - the density is defined by the values when they were passed to the declaration. So I thought maybe I could approach this with a lambda expression, eg (apologies for the horrid code!):

mydict['density_calc'] = lambda x,y: x/y
mydict['density'] = mydict['density_calc'](mydict['mass'],mydict['volume'])

But again, this only returns the original density, despite changing mydict['mass']. As a final attempt, I tried this:

def density(mass,volume): return mass/volume
mydict['density_calc'] = lambda x,y: density(x,y)
mydict['density'] = mydict['density_calc'](mydict['mass'],mydict['volume'])

Again, no dice. This seems like a really simple problem to solve, so apologies in advance, but if anyone could help me out, I'd be very appreciative!

Cheers,

Dave

Upvotes: 2

Views: 14005

Answers (8)

DougP
DougP

Reputation: 1

A self-referential dictionary would almost match your second attempt (from 7 years ago) and would play nicely with any other code that uses this dictionary. In other words it will still look and act like a duck.

class MyDict(dict):
        def __getitem__(self,key):
            value=dict.__getitem__(self,key)        
            if callable(value):
                value=value(self)
            return value

mydict=MyDict({'mass':1.0,'volume':0.5})
mydict['density'] = lambda mydict: mydict['mass']/mydict['volume']

print(mydict['density'])

2.0

mydict['mass']=2.0
print(mydict['density'])

4.0

Upvotes: 0

scvalex
scvalex

Reputation: 15345

Short answer is you can't.

The assigned expression is evaluated strictly when it's encountered so any changes to the terms later on don't matter.

You could solve this with a custom dict class that overwrites the getitem and setitem methods and does something special (i.e. computes the value) for some of the keys (say density and a finite number of others).

class MyDict(dict):
    def __getitem__(self, key):
            if key == "density":
                    return self["mass"] / self["volume"]
            return dict.__getitem__(self, key)

d = MyDict()
d["mass"] = 2.0
d["volume"] = 4.0
d["density"]    # 0.5

Have a look at this.

Upvotes: 3

Adrian Panasiuk
Adrian Panasiuk

Reputation: 7353

This is so easy:

>>> mydict = {'mass':100, 'volume': 2, 'density': lambda: mydict['mass']/mydict['volume']}
>>> mydict['density']()
50
>>> mydict['mass']=200
>>> mydict['density']()
100
>>> mydict['volume'] = 4
>>> mydict['density']()
50

My lambda references mydict and later on retrieves the newest data hold in mydict, so sparing me the trouble of passing the mass and volume to the function each time I want to call it, like in your solution:

def density(mass,volume): return mass/volume
mydict['density_calc'] = lambda x,y: density(x,y)
mydict['density'] = mydict['density_calc'](mydict['mass'],mydict['volume'])

However mydict['density'] is a function, not the value itself.

Upvotes: 1

Otto Allmendinger
Otto Allmendinger

Reputation: 28288

The problem is that dictionaries aren't the right tool for your problem. Your question is like "how can I hammer a nail in with a screwdriver" ;-)

Dictionaries are supposed to map keys to values and don't care about other values in the dictionary. What you want is a class

class PhysicalObject:
    def __init__(self, mass, volume):
         self.mass = float(mass)
         self.volume = float(volume)
    def setMass(self, mass):
         self.mass = float(mass)
    def setVolume(self, volume):
         self.volume = float(volume)
    def getDensity(self):
         return self.mass/float(self.volume)

 v = PhysicalObject(1.0, 2.0)
 print v.getDensity() # prints 0.5
 v.setMass(2.0)
 v.setVolume(1.0)
 print v.getDensity() # prints 2.0

This example recalculates the density every time you want to get it, buy you can also calculate it in the setMass and setVolume functions.

Upvotes: 3

Mark Rushakoff
Mark Rushakoff

Reputation: 258408

Here's a quick hack on how to subclass dict to meet your specs, but probably not to meet Python's specs for a dictionary:

class MyDict(dict):
    def __getitem__(self, key):
        if key == 'density':
           return self['mass'] / self['volume']
        else:
            return dict.__getitem__(self,key)
    def keys(self):
        return ['density'] + dict.keys(self)

x = MyDict()
x['mass'] = 1.0
x['volume'] = 0.5

print x

print x.keys()

print x['density']

x['mass'] = 2.0

print x['density']

which prints

{'volume': 0.5, 'mass': 1.0}
['density', 'volume', 'mass']
2.0
4.0

But that doesn't account for dict.iterkeys(), among other things.

Upvotes: -2

John Montgomery
John Montgomery

Reputation: 9058

You would be best off creating a class for this in this case and using a dynamic property. e.g.:


class Body(object):
    def __init__(self):
        self.mass=1.0
        self.volume=0.5

    @property
    def density(self):
        return self.mass/self.volume

This will give you mass, volume and density properties, where density is calculated based on the other two values. e.g.


b=Body()
b.mass=1
b.volume=0.5
print b.density # should be 2

b.mass=2.0
print b.density # should be 4

However if you are wedded to using a dictionary you should probably extend it and override the __getitem__ and __setitem__ "magic" methods to and detect when mass has changed or else when density is being accessed and recalculate as needed.

Upvotes: 6

RichieHindle
RichieHindle

Reputation: 281795

A class would do this better:

class Thing:
    def __init__(self, mass, volume):
        self._mass = mass
        self._volume = volume
        self._update_density()

    def _update_density(self):
        self._density = self._mass / self._volume

    def get_density(self):
        return self._density

    density = property(get_density)

    # You're unlikely to need these, but to demonstrate the point:
    def set_mass(self, mass):
        self._mass = mass
        self._update_density()

    def set_volume(self, volume):
        self._volume = volume
        self._update_density()

brick = Thing(mass=2, volume=0.8)
print brick.density
# Prints 2.5

brick.set_mass(4)
print brick.density
# Prints 5.0

I'm taking you at your word that you want the density updated when you set the other values - an easier way would be to simply calculate it on the fly when asked for it:

    def get_density(self):
        return self._mass / self._volume

Then you wouldn't need _update_density() at all.

Upvotes: 4

FogleBird
FogleBird

Reputation: 76842

Seems like an abuse of a dictionary structure. Why not create a simple class?

class Entity(object):
    def __init__(self, mass, volume):
        self.mass = mass
        self.volume = volume
    def _density(self):
        return self.mass / self.volume
    density = property(_density)

Upvotes: 9

Related Questions