4

I am tinkering with objects' identity in Python with the following code:

def f(var1):
    print 'Within f and BEFORE modification: var1= '+str(var1)+', id= '+str(id(var1))
    var1 = 10
    print 'Within f and AFTER modification: var1= '+str(var1)+', id= '+str(id(var1))

def f2(var1):
    print 'Within f and BEFORE modification: var1= '+str(var1)+', id= '+str(id(var1))
    var1 = 1
    print 'Within f and AFTER modification: var1= '+str(var1)+', id= '+str(id(var1))

def f3(var1):
    print 'Within f and BEFORE modification: var1= '+str(var1)+', id= '+str(id(var1))
    var1 = 10
    print 'Within f and AFTER modification: var1= '+str(var1)+', id= '+str(id(var1))


var = 5

print '\n f - var1=10:'
print 'BEFORE FUNCTION CALL: var= '+str(var)+', id= '+str(id(var))
f(var)
print 'AFTER FUNCTION: var= '+str(var)+', id= '+str(id(var))

print '\n f2 - var1=1:'
var = [4,3,1,6]
print 'BEFORE FUNCTION CALL: var= '+str(var)+', id= '+str(id(var))
f2(var)
print 'AFTER FUNCTION: var= '+str(var)+', id= '+str(id(var))

print '\n f3 - var1=10 again:'
var = 7
print 'BEFORE FUNCTION CALL: var= '+str(var)+', id= '+str(id(var))
f3(var)
print 'AFTER FUNCTION: var= '+str(var)+', id= '+str(id(var))

print '\n f2 - var1=1 again:'
var='a'
print 'BEFORE FUNCTION CALL: var= '+str(var)+', id= '+str(id(var))
f2(var)
print 'AFTER FUNCTION: var= '+str(var)+', id= '+str(id(var))

Output:

 f - var1=10:
BEFORE FUNCTION CALL: var= 5, id= 18089816
Within f and BEFORE modification: var1= 5, id= 18089816
Within f and AFTER modification: var1= 10, id= 18089696
AFTER FUNCTION: var= 5, id= 18089816

 f2 - var1=1:
BEFORE FUNCTION CALL: var= [4, 3, 1, 6], id= 23884720
Within f and BEFORE modification: var1= [4, 3, 1, 6], id= 23884720
Within f and AFTER modification: var1= 1, id= 18089912
AFTER FUNCTION: var= [4, 3, 1, 6], id= 23884720

 f3 - var1=10 again:
BEFORE FUNCTION CALL: var= 7, id= 18089768
Within f and BEFORE modification: var1= 7, id= 18089768
Within f and AFTER modification: var1= 10, id= 18089696
AFTER FUNCTION: var= 7, id= 18089768

 f2 - var1=1 again:
BEFORE FUNCTION CALL: var= a, id= 140350777144584
Within f and BEFORE modification: var1= a, id= 140350777144584
Within f and AFTER modification: var1= 1, id= 18089912
AFTER FUNCTION: var= a, id= 140350777144584

I understand that the identity of an object is guaranteed to be unique during the its lifetime, and that two objects with non-overlapping lifetimes may have the same id() value.

From that I understand that I can get the same id() during the execution of the code for different variables, but I am surprised that in my code the same id() values coincide also with the variable values.

I mean that I always get the same id() value for var1=10. The same happens with the assignment var1=1 that has its own id() value. Even doing this assignment in different functions returns the same id().

So my question is: Is Python keeping a record of previous variables, values and identities even after their lifetimes have expired?

If in the code there is a variable assignment with the same value as a previously expired variable, does Python check records of the previous expired variables in memory and give priority to use the same id() for the same memory values?

I would like to understand a little bit more about id() values reuse and the memory management in a Python program.

jchanger
  • 739
  • 10
  • 29
  • I believe the answer for [this question](http://stackoverflow.com/questions/15171695/weird-integer-cache-inside-python-2-6) may help you . Short explanation , python caches integers in the range [-5 , 256] . so whenever you do - `var1=10` , you are still getting the same object back. Try this out with values greater than 257 , and you will see the difference. – Anand S Kumar Aug 05 '15 at 11:20
  • related http://stackoverflow.com/questions/28329498/why-does-a-space-effect-the-identity-comparison-of-equal-strings/28329522#28329522 – Padraic Cunningham Aug 05 '15 at 11:47
  • @PadraicCunningham I think even though the reason for the questions are same, don't think the quesiton as such is duplicate ? – Anand S Kumar Aug 05 '15 at 11:53
  • @AnandSKumar, it is a dupe. What is in your answer that is not in the dupe? The OP is seeing integer caching – Padraic Cunningham Aug 05 '15 at 11:55
  • @AnandSKumar @PadraicCunningham, I didn't come across before posting the question you mention related with the strings caching. I didn't know that the caching also occurs with strings nor about the `is`. Thanks! – jchanger Aug 05 '15 at 16:26

3 Answers3

3

Short answer for your question - Python caches the integers in the range [-5 , 256] .

So whenever you do var1 = 10 or var1 = 1 , you always get back the same object from the integer cache, and that is why you are seeing the same id even in different runs of your function.

If you try it out for values greater than or equal 257, you may be able to see different results.

A very simple example of the behavior -

>>> var1 = 257
>>> var2 = 257
>>> id(var1)
36635792
>>> id(var2)
36636304
>>> var1 = 10
>>> var2 = 10
>>> id(var1)
1642386016
>>> id(var2)
1642386016
Anand S Kumar
  • 88,551
  • 18
  • 188
  • 176
  • That may fail if you're not doing that interactively (well it may anyway for some implementations). – skyking Aug 05 '15 at 12:44
  • In case of OP's question I do not think so , since he would be using different invocations of the function right? If in same function some implementations may be optimised to use same object . – Anand S Kumar Aug 05 '15 at 12:49
  • The point is when you compile it the equal number may end up in the same object - that will mean that the equal numbers are the same object even before the file starts to execute. The difference is when you enter one line interactively (or compound statement) that is compiled and executed right away, but in non-interactive execution the entire `.py` file is compiled and then executed. – skyking Aug 05 '15 at 13:33
2

One other issue at play here is that a function caches all the constants in it.

Thus

def f():
    return 1.1

assert f() is f() # or id(f()) == id(f())

You can find the cached constants associated with a function by looking at it's code object.

>>> print(f.__code__.co_consts)
(None, 1.1)

Generally a tuple is a number, string or tuple. So as you always assign a literal integer to var1, the compiler knows that this is constant value that cannot be changed and so caches that integer object between invocations of the function.

The reason I used a float in f, is that this the only instance in which a float will be cached. Strings and integers may be cached in other circumstances as well eg.

>>> x = 1.1
>>> y = 1.1
>>> x is y
False

Update

Whilst tuples are considered immutable in Python, at the implementation level they are just another bit of mutable memory. Sometimes Python will mutate a tuple if it knows no one else has access to the tuple. eg.

>>> [id(x) for x in zip("abc", range(3))]
[12684384, 48723328, 12684384] # different tuples

>>> l = []
>>> for x in zip("abc", range(3)):
    l.append(id(x))
    del x # only the zip iterator has access to yielded tuple now

>>> l
[12684384, 12684384, 12684384]

See the appropriate lines where the zip next method is defined.

if (Py_REFCNT(result) == 1) { // if we are the only ref holder
    Py_INCREF(result);
    for (i=0 ; i < tuplesize ; i++) {
        it = PyTuple_GET_ITEM(lz->ittuple, i);
        item = (*Py_TYPE(it)->tp_iternext)(it);
        if (item == NULL) {
            Py_DECREF(result);
            return NULL;
        }
        olditem = PyTuple_GET_ITEM(result, i);
        PyTuple_SET_ITEM(result, i, item); // modify in place
        Py_DECREF(olditem);
    }
Dunes
  • 37,291
  • 7
  • 81
  • 97
2

The documentation is correct, but the lifetime and object identity may confuse a bit. When assigning to a variable (be it normal or augmented) means that the variable after it refers to the object on the right side of the assignment (which may be another object). This is the reason why id(x) changes after the assignment.

Note also that the interpreter may keep references to objects behind your back which means that the lifetime of the object may be longer than expected (even as long as the lifetime of the interpreter in some cases), or created earlier than you expect. In some cases these objects may be accessed again.

For example some (currently -5 to +256 i believe) integers are among those objects (so before you write x=1 the object 1 already exists). Note that otherwise the equal integer need not be the same (ie x==y doesn't imply that x is y), which means there's redundant objects.

Another example is interned strings, normally strings are interned by the interpreter for efficiency reasons (so before you write x="keys" the object "keys" already existed, because that's one of the strings python interns).

A third example is that objects may be created upon reading compiled python code, which means that the objects for the numbers may be created even before the code starts to execute. That means integer literals and string literals with the same value would be the same object - given that they're compiled at the same time.

Note that these are non-mutable objects so it would not hurt that you happen to get the same object again (since they are immutable they would be equal no matter if they were newly created or reused).

skyking
  • 13,817
  • 1
  • 35
  • 57