How to update a class's list or dictionary when variables change elsewhere?

Question:

How do you update a self.dictionary or self.list that contains other self.variables after being changed outside?

For instance we have the following test class:

class Test():
   def __init__(self):
       self.some_number = 0
       self.other_number = 0
       self.lst=[self.some_number, self.other_number]
       self.dic={'some_number': self.some_number, 'other_number': self.other_number}

   def update_values(self):
       self.number = 2
       self.other_number = 3

Then updating the values:

test = Test()                  

print(test.dic)
print(test.lst)

test.update_values()

print(test.dic) 
print(test.lst)

results in no change:

{'other_number': 0, 'some_number': 0}
[0, 0]
{'other_number': 0, 'some_number': 0}
[0, 0]

I actually thought, that the dictionary and list hold the reference to these variables pointing somewhere in memory. So if these change, that should change as well. But this doesn’t seem to be the case. Is there any explanation?

Asked By: xaratustra

||

Answers:

Yes, names are references. But when in update_values you do self.number = whatever, you are rebinding self.number to point to a different integer. self.lst still contains a reference to the original integer.

If integers were mutable, you could mutate the value of self.number and that would be reflected in self.lst. But they aren’t, so you can’t. If you used some other kind of object, that would be visible though. For example:

def __init__(self):
    self.first = [0]
    self.lst = [self.first]

def update_values(self):
    self.first[0] += 1

Here we are mutating self.first, not rebinding it, so self.lst will reflect the change.

Answered By: Daniel Roseman

Integers are immutable, so that won’t work. But you can achieve what you want using lists:

class Test(object):
   def __init__(self):
       self.some_number = [0]
       self.other_number = [0]
       self.lst=[self.some_number, self.other_number]
       self.dic={'some_number': self.some_number, 
           'other_number': self.other_number}

   def update_values(self):
       self.some_number[0] = 2
       self.other_number[0] = 3


test = Test()

print(test.dic)
print(test.lst)

test.update_values()

print(test.dic) 
print(test.lst)       

output

{'some_number': [0], 'other_number': [0]}
[[0], [0]]
{'some_number': [2], 'other_number': [3]}
[[2], [3]]

Note that we have to mutate the some_number and other_number lists, i.e., modify their contents. If we simply replace them with new lists, then we don’t get the desired propagation:

#This doesn't do what we want because it replaces the old list
#objects with new ones.
def update_values(self):
    self.some_number = [2]
    self.other_number = [3]

The way Python name binding works can be a little bewildering at first, especially if you’re coming from another language. IMO, it’s best to try to understand this idiom in its own terms, rather than trying to understand it in terms of references, pointers, etc.

I suggest you take a look at Facts and myths about Python names and values by SO veteran Ned Batchelder; that article’s also available in video form on YouTube.

In a comment you ask:

If python dynamically rebuilds the list / dic, what happens to the old
data? Will Python copy from memory to memory? Or just reassign?

A Python variable, whether it’s a simple variable or an attribute like self.some_number, isn’t a container that holds a value. It’s a name that’s been bound to an object. a=0 creates an int object with a value of 0 and binds that object to the name a in the local scope. If you then do a=2 an int object with a value of 2 is created and bound to a, and the previous 0 object is reclaimed.

(For efficiency, small integers in the range -5 and 256 (inclusive) are cached, integer objects outside that range are created when needed and destroyed when they’re no longer needed, i.e., when there are no longer any names bound to them).

When you do

a = 1234
b = 5678
mylist = [a, b]

Two integer objects are created and bound to the names a and b. IOW, a is a name for the 1234 object, b is a name for the 5678 object. Sometimes this is described by saying that a holds a reference to the 1234 integer object. But IMO it’s better (and simpler) to think of a as being a key in a dictionary, with the 1234 integer object as the associated value.

Then mylist = [a, b] makes mylist[0] an alternate name for the 1234 object, and mylist[1] an alternate name for the 5678 object. The 1234 and 5678 objects themselves aren’t copied in memory. Essentially, a Python list is an array of pointers to the objects it holds (with some added machinery that makes the list a proper Python object).

In mkrieger1’s code, a new list object is created every time you retrieve the Test.lst property; there’s no “old list” unless you’ve saved it somewhere by binding it, somehow.

I hope that was helpful and not too confusing. 🙂

Answered By: PM 2Ring

If you want to reflect the updated values in what you get when using self.lst or self.dic, make them a property:

class Test():
    def __init__(self):
        self.some_number = 0
        self.other_number = 0

    def update_values(self):
        self.some_number = 2
        self.other_number = 3

    @property
    def lst(self):
        return [self.some_number, self.other_number]

    @property
    def dic(self):
        return {'some_number': self.some_number,
                'other_number': self.other_number}
>>> t = Test()
>>> t.lst
[0, 0]
>>> t.dic                                                          
{'some_number': 0, 'other_number': 0}
>>> t.update_values()
>>> t.lst                                                          
[2, 3]
>>> t.dic 
{'some_number': 2, 'other_number': 3}
Answered By: mkrieger1
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.