4

I am using repl.it for python3 - Here is a sequence of assignments I did

  x34=20
   x34
=> 20
   hex(id(x34))
=> '0x7fdb5d318960'
   x34=30
   hex(id(x34))
=> '0x7fdb5d318aa0'
   hex(id(x34))
=> '0x7fdb5d318aa0'
   x34
=> 30
   hex(id(x34))
=> '0x7fdb5d318aa0'
   x34=20
   hex(id(x34))
=> '0x7fdb5d318960'

why does the address of the variable x34 change when I reassign it value 30?

2 Answers2

7

The fundamental misunderstanding you have is that id's belong to Python objects, not the variables. So, any reassignment to a new object, would cause the id's to change:

>>> x = (1,2)
>>> hex(id(x))
'0x107526e48'
>>> x = (2, 4)
>>> hex(id(x))
'0x107526e88'

Note, every time you create a new object, this happens:

>>> x = (1, 2)
>>> hex(id(x))
'0x107526e48'
>>> x = (1, 2)
>>> hex(id(x))
'0x107526d48'
>>> x = (1, 2)
>>> hex(id(x))
'0x107526dc8'

Immutability or lack thereof has nothing to do with it. Note, tuple objects are immutable. However, there is a CPython implementation detail that small ints are cached. This is why you'll see the following behavior:

>>> x = 10
>>> hex(id(x))
'0x10714d920'
>>> x = 10
>>> hex(id(x))
'0x10714d920'
>>> x = 10
>>> hex(id(x))
'0x10714d920'

Essentially, in CPython, int objects between -5 and 256 are singletons. Just like None:

>>> x = None
>>> id(x)
4413491112
>>> x = None
>>> id(x)
4413491112

However, this wont work the same way with large ints, and works just like it does with tuples:

>>> x = 888
>>> hex(id(x))
'0x107550070'
>>> x = 888
>>> hex(id(x))
'0x10736aef0'
>>> x = 888
>>> hex(id(x))
'0x107550030'

Be careful, though, since id is only guaranteed unique for the lifetime of the object. So, sometimes, you'll see the same memory being returned from the heap, since the Python interpreter tries to be efficient with memory:

>>> x = 888
>>> hex(id(x))
'0x107550050'
>>> x = 888
>>> hex(id(x))
'0x10736aef0'
>>> x = 888
>>> hex(id(x))
'0x107550090'
>>> x = 888
>>> hex(id(x))
'0x10736aef0'
>>> x = 888
>>> hex(id(x))
'0x107550070'

Again, this might happen with any object:

>>> x = object()
>>> hex(id(x))
'0x1072db120'
>>> x = object()
>>> hex(id(x))
'0x1072db110'
>>> x = object()
>>> hex(id(x))
'0x1072db120'

And the value of the object doesn't matter,

>>> x = 888
>>> hex(id(x))
'0x107549fd0'
>>> x = 999
>>> hex(id(x))
'0x107550090'
>>> x = 777
>>> hex(id(x))
'0x107549fd0'

Indeed, neither does the type necessarily matter either:

>>> class A:
...    pass
...
>>> class B:
...    pass
...
>>> class C:
...    pass
...
>>> for i in range(12):
...     if i%3 == 1:
...         print(A())
...     elif i%3 == 2:
...         print(B())
...     else:
...         print(C())
...
<__main__.C object at 0x107535a58>
<__main__.A object at 0x107535a58>
<__main__.B object at 0x107535a58>
<__main__.C object at 0x107535a58>
<__main__.A object at 0x107535a58>
<__main__.B object at 0x107535a58>
<__main__.C object at 0x107535a58>
<__main__.A object at 0x107535a58>
<__main__.B object at 0x107535a58>
<__main__.C object at 0x107535a58>
<__main__.A object at 0x107535a58>
<__main__.B object at 0x107535a58>

This is all at the discretion of the interpreter implementation.

However, if you keep another reference to an object, the original will not die during reassignment, and that is guaranteed:

>>> hex(id(x))
'0x1072db140'
>>> y = x
>>> hex(id(y)) # guaranteed to be the same as x
'0x1072db140'
>>> x = object()
>>> hex(id(x)) # guaranteed to be new
'0x1072db130'
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
2

Basically what you are doing with each assignment is creating a new object of type int, de-referencing the older one (20 in this case), and referencing with the variable (x34) to the new one (30).

Still be careful, because implementations could be tricky, look this:

>>> x34=20
>>> x34
20
>>> hex(id(x34))
'0x107d67f70'
>>> x34=30
>>> x34
30
>>> hex(id(x34))
'0x107d680b0'
>>> x35=30
>>> hex(id(x35))
'0x107d680b0'

As you can see, two different variables (x34 and x35) are pointing to the same object ( 30 in this case) when they "theoretically" shouldn't because the two 30 are different objects even they have the same values.

The Python interpreter is smart enough to realize that 30 is an immutable object, so it avoids wasting memory creating two 30 when with just one is enough because it (the interpreter) will never modify (due to the immutability rules) one object and such modification reflected in the other variable that point to the same object.

  • Nice explanation! To complement: `>>> a=30 >>> hex(id(a)) '0x7fcba6c0b4f0' >>> hex(id(30)) '0x7fcba6c0b4f0' ` **:-)** – Savir Aug 28 '17 at 04:14
  • No. This *is not generalizable to all immutable objects*. This is an implementation detail for CPYthon, which caches small ints as an optimization. – juanpa.arrivillaga Aug 28 '17 at 04:30
  • @juanpa.arrivillaga Never said *is generalizable to all immutable objects*, I even said *Still be careful, because implementations could be tricky*. Pls, help me to improve my answer if was not clear to you. You can edit it if you want. –  Aug 28 '17 at 04:39
  • @juanpa.arrivillaga I re-read my answer and find out that the first sentences imply a relationship between immutability and the behavior described, and that is not correct as you pointed it out. That could be a factor if variables were being assigned values using as a right side other variables instead of object literals. I was just thinking about those cases even I did not put examples related to them. Thanks for your comments that make improve my answer. BTW, I learn details about the caching of the objects by CPython , I did not knew that was so limited. –  Aug 28 '17 at 05:30
  • @gsi-frank yes, Python is fundamentally much too dynamic to allow for true optimizations from immutability that you might see in a statically typed, functional programming language. The interpreter does special case `int` and actually `str` with string-interning. Other Python built-in containers have lots of optimizations for literals, too. The rules for string-interning are rather arcane, though. – juanpa.arrivillaga Aug 28 '17 at 05:43