1

How do i reference a variable from an instance in a dict?

this code:

class Gold():
    def __init__(self):
        self.amount = 10

class Cheese():
    def __init__(self, gold):
        self.gold = gold
        self.wallet = {'gold' : self.gold.amount}
        self.charge ={'gold' : 10}

    def subtract(self):
        for cat, cost in self.charge.iteritems():
            if self.wallet[cat] >= cost:
                self.wallet[cat] -= cost

gold = Gold()
cheese = Cheese(gold)
cheese.subtract()
print 'gold amount =', cheese.gold.amount, 'wallet =', cheese.wallet

yields:

gold amount = 10 wallet = {'gold': 0}

and not

gold amount = 0 wallet = {'gold': 0}

in the actual code i dont want a whole list of 'if' statements. so i couple a dict with all the costs (charge) in this example to a dict with all the actual values so i dont have to do

if cost == 'gold':
    pass
if cost == 'action points':
    pass

etc

any tips on how to do this? in the end the quistion is:

a = 1
b = a

is there a way to update both a and b (b += 1 only updates b to 2)

Lennart Wijers
  • 225
  • 1
  • 2
  • 11
  • 2
    Because the value in the wallet *isn't* referencing the Gold instance, and integers are immutable. – jonrsharpe Oct 15 '17 at 21:44
  • 1
    pythontutor.com might help you to understand this. You don't reference self.gold.amount in your wallet dict. It is copied in there. – UpSampler Oct 15 '17 at 21:46
  • Thats a very helpful website. Now i know _why_ it doesnt work. maybe any pointers in how i _do_ reference the self.gold.amount? thx a lot! – Lennart Wijers Oct 15 '17 at 21:57
  • 2
    @UpSampler no, it isn't copied. `wallet['gold']` and `self.gold.amount` both reference the exact same object, but *that object is immutable*. – jonrsharpe Oct 15 '17 at 22:27
  • If it is not copied and points to the same object, how can it then have two different values? An object can not hold two values at the same time. (Except if it would be a quantum state). – UpSampler Oct 16 '17 at 08:57
  • 1
    @LennartWijers I got it know. You were right, the object is immutable, but the content of `wallet['gold']` can change. It found it here: https://stackoverflow.com/questions/8056130/immutable-vs-mutable-types – UpSampler Oct 16 '17 at 09:09

2 Answers2

2

In Python:

  • all variables are references
  • integers and strings are immutable

So when you do:

>>> a = 1  # a points to 1
>>> b = a  # b points to 1
>>> b = 10   # b now points to 10, a still points to 1 and 1 is unchanged.
>>> b
10

>>> a
1

If you use a mutable data type, then it works:

>>> a = []
>>> b = a
>>> b.append(10)
>>> b
[10]

>>> a
[10]

>>> a[0] = 20
>>> b
[20]

Perhaps you can create a generic container:

class GenericContainer(object):
    def __init__(self, **kwargs):
        self._keys = set()
        for k, v in kwargs.items():
            if k in ('inventory', 'charge'):
                raise ValueError('Unable to override method {}'.format(k))
            setattr(self, k, v)
            self._keys.add(k)

    def charge(self, **kwargs):
        for k, v in kwargs.items():
            if k in ('inventory', 'charge'):
                raise ValueError('Unable to override method {}'.format(k))
            if v < 0 and getattr(self, k, 0) < abs(v):
                raise ValueError("You don't have enough {}".format(k))
            setattr(self, k, getattr(self, k, 0) + v)
            self._keys.add(k)
        return self

    def inventory(self):
        return {k:getattr(self, k) for k in self._keys}

    def __repr__(self):
        return str(self.inventory())

Then your wallet can be an instance of GenericContainer:

>>> wallet = GenericContainer(gold=10, silver=5)
>>> wallet
{'gold': 10, 'silver': 5}

>>> wallet.charge(gold=-5, silver=5)
{'gold': 5, 'silver': 10}

>>> wallet.charge(gold=-20)
ValueError: You don't have enough gold

>>> wallet.gold
5

You can set and/or retrieve the attributes directly:

>>> wallet.gold
5

>>> wallet.gold += 10
>>> wallet.gold
15

>>> wallet
{'gold': 15, 'silver': 10}

You can get the attribute by name from the inventory or using getattr:

>>> wallet.inventory().get('gold', 0)
15

>>> getattr(wallet, 'gold')
15

You can set it by name using dict of parameter:value or setattr:

>>> wallet.charge(**{'gold': -10})
{'gold': 5, 'silver': 10}

>>> setattr(wallet, 'gold', 20)
>>> wallet.gold
20

Looking at the result, I see why in Python I often use just a dictionary instead of creating a class - although I do miss the Javascript object syntax where you can access properties both using either foo.bar or foo["bar"].

In Python, the builtin data structures are very powerful and Python developers tend to use a "we are all consenting adults" approach where you just pass data around without caring too much about attribute protection. Probably I would end up doing just this:

>>> wallet = {'gold': 10, 'silver': 15}

And instead of using a charge(gold=10) method:

>>> wallet['gold'] += 10

It takes time for this "it is just data" approach to sink but after that you seldom miss the more bureaucratic OO idioms so common in languages like Java or C++ specially for small projects/teams.

Paulo Scardine
  • 73,447
  • 11
  • 124
  • 153
1

It seems like the easiest way to do this is to change wallet from being an attribute to being a property.

class Cheese():
    def __init__(self, gold):
        self.gold = gold
        self.charge ={'gold' : 10}

    @property
    def wallet(self):
        return {'gold': self.gold.amount)

    def subtract(self):
        for cat, cost in self.charge.iteritems():
            if self.wallet[cat] >= cost:
                self.wallet[cat] -= cost

Properties are methods that act like attributes. They're calculated on-call every time rather than calculated when the object is created and never again. You use them the same way, though, so:

>>> print 'gold amount =', cheese.gold.amount, 'wallet =', cheese.wallet
gold amount = 0 wallet = {'gold': 0}
Adam Smith
  • 52,157
  • 12
  • 73
  • 112