Different class instances use same memory location

I was playing around with the pickle library, when I noticed that sometimes, different class instances are at the same memory location.

Both of the below examples are showcasing said behaviour:

class DemoClass:
    def __init__(self):
        self.name = 'demoName'

#example 1
for i in range(3):
    print (DemoClass())

#example 2
[print(DemoClass()) for i in range(3)]

#Output for both example 1 and example 2
#Note that the memory locations are identical in the output
<__main__.DemoClass object at 0x00CEE610>
<__main__.DemoClass object at 0x00CEE610>
<__main__.DemoClass object at 0x00CEE610>

This was quite startling to me, so maybe you could explain why this occurs.

The way in which the program acts as I would expect it to is as follows.

demo = [DemoClass() for i in range(3)]
for i in demo:
    print (i)

#Output
<__main__.DemoClass object at 0x01F7E630>
<__main__.DemoClass object at 0x01F7ED30>
<__main__.DemoClass object at 0x01F7E670>

Solution 1:

Your question concerns how Python allocates memory. tldr; Python uses a heap to store memory. When a resource is freed, it goes to the top of the heap.

Detailed Answer

Python has to allocate memory to create an instance of an object. For memory efficiency, the Python memory manager has a heap of memory locations available to give or reserve for the instantiation of objects. Using some of your examples, you can see how this works in practice.

Example #1

>>> for i in range(3):
...     print(DemoClass())
... 
<test.DemoClass instance at 0x288b248>
<test.DemoClass instance at 0x288b248>
<test.DemoClass instance at 0x288b248>

During the first iteration of the for loop, python uses the first available address in its current heap, namely <0x288b248>, to create an instance of DemoClass for the print call. Once the print command has finished, the memory address is freed and returns to the top of the heap. During the next iteration of the loop, python utilizes the first available memory address, which again is address <0x288b248>. Etc.

Example #2

>>> for j in [DemoClass() for i in range(3)]:
...     print(j)
... 
<test.DemoClass instance at 0x288bcf8>
<test.DemoClass instance at 0x288b290>
<test.DemoClass instance at 0x288b638>

Here python generates a list that it will then iterate through. The creation of the list requires that a new instance of DemoClass be created for each element. This will take the top three addresses off the heap. After the loop is done, the list is freed from memory and if we again make a call to print(DemoClass) we will find that the python is again reusing memory.

>>> print DemoClass()
<test.DemoClass instance at 0x288bcf8>

Example 3 (My Example of Alternating Memory Allocation)

>>> for i in range(4):
...     Demo = DemoClass()
...     print(Demo)
... 
<test.DemoClass instance at 0x288bcf8>
<test.DemoClass instance at 0x288b290>
<test.DemoClass instance at 0x288bcf8>
<test.DemoClass instance at 0x288b290>

In this example, each time Demo is instantiated as an instance of DemoClass, a segment of memory is assigned to Demo. However, the call to print(Demo) does not free the memory assigned to Demo. At the beginning of the next loop, a new segment of memory is allocated to Demo and then Demo is overwritten, at which point it's original memory address returns to the top of the heap. The memory address used for Demo then alternates between two memory addresses.

Solution 2:

In your first 2 examples, once the call to print has finished, the object no longer has a reference, and so can be re-used the next time that type of object is created.

In the second, all of the instances have references at the same time, and thus must be distinct.

I wouldn't rely on this behavior, though: for example, there might be other unreferenced instances of that object lying about.

Solution 3:

When an object doesn't exist any more, its location is available for reuse immediately.
Since Python objects are reference-counted, they disappear as soon as nothing refers to them.

If things worked the way you expect, you could run out of memory rather quickly.