Normally, Python internally deletes an object from the memory when it is no longer referenced by any other part of the program. This process is known as Garbage Collection. Once an object has been garbage-collected, it can no longer be used or accessed. 

Whenever an object in a Python program is referenced elsewhere, the reference count of that objects gets incremented, this ensures that an object that is still being referenced is not deleted or garbage collected. This is referred to as strong reference because it ensures that an object stays in memory as long as it is being used and referenced by other parts of the program.

In strong references, deleting an object, for example using the del statement, will not actually delete the object if the reference is still held. 

ExampleEdit & Run
class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})" 

bear1 = Bear("Teddy")

#reference bear1
bear2 = bear1
print(bear2)

#delete bear1
del bear1

#the object still exists  since it is still referenced by t2
print(bear2)
Output:
Bear(Teddy)Bear(Teddy)[Finished in 0.010917163919657469s]

Weak references

In weak references, a reference to an object does not prevent the garbage collector from reclaiming it. This makes them useful for implementing certain caching for large objects, or when attempting to prevent memory leaks due to circular references in an application.

The weakref module provides support for weak references. The ref class in the module provide the framework for creating weak references. 

Weak references can be used to track an object without preventing it from being garbage collected.

The process of creating weak references is as outlined below:

ExampleEdit & Run
import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})" 

bear1 = Bear("Teddy")

#weak reference the object
bear2 = weakref.ref(bear1)
print(bear2())

#delete bear1
del bear1

#the object has been garbage collected
print(bear2())
Output:
Bear(Teddy)None[Finished in 0.011198276886716485s]

In the above example:

  1. We imported the weakref module 
  2. Created the Bear class. 
  3. created an instances of Bear called bear1
  4. Created a weak reference of bear1 called bear2 i.e weakref.ref(bear1).

Note that the weak reference(bear2) is no longer accessible after we deleted the original object, bear1. Instead, the default value None is returned if the original object has been deleted/garbage collected.

Note that we are calling the weak reference object as if it were a function, as in bear2(). This is because ref returns the weak reference instance as a callable object. Consider the fallowing example:

ExampleEdit & Run
import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})" 

bear1 = Bear("Teddy")

bear2 = weakref.ref(bear1)
print(bear2)
Output:
<weakref at 0x7fb2652fbce0; to 'Bear' at 0x7fb2650beb90>[Finished in 0.011055269977077842s]

As you can see above, using bear2 without the parentheses does not access the weak reference object, we have to call it with parentheses to access the actual object. As in below:

ExampleEdit & Run
import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})" 

bear1 = Bear("Teddy")

bear2 = weakref.ref(bear1)
print(bear2())
Output:
Bear(Teddy)[Finished in 0.010598613880574703s]

Reference Callbacks

The ref() constructor allows us to provide a callback function that will be called if the object has been deleted from memory.

ExampleEdit & Run

reference with callback

import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})" 

#the callback function
def callback(reference):
   print('The object has been deleted')

bear1 = Bear("Teddy")
#weak reference the object
bear2 = weakref.ref(bear1, callback)
print(bear2())

#delete bear1
del bear1

#invokes the callback since the object has been deleted
bear2()
Output:
Bear(Teddy)The object has been deleted[Finished in 0.011690349085256457s]

In the above example, we tried to access a weak reference object bear2, while the origin object(bear1) has already been deleted from memory. Since we provided a callback function to ref(), the callback is called instead of returning the default value None

Proxies

The ref() class creates a simple weak reference to an object, this means that if the object is garbage collected, the weak reference will simply be set to None. It does not provide any additional behavior or properties.

The proxy class , on the other hand, can be used to create a proxy for an object. This proxy object provides a way to access the object's attributes and methods even after the object is garbage collected. However, any access to a garbage-collected object's attributes or methods will result in a ReferenceError exception.

ExampleEdit & Run

using proxies

import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})" 
    def get_name(self):
        return 'My name is %s'%self.name

t1 = Bear("Teddy")
print(t1)

#create a proxy
ref = weakref.proxy(t1)
print(ref.get_name())

#delete t1
del t1
print(ref.get_name())
Output:
Bear(Teddy)My name is TeddyTraceback (most recent call last):  File "<string>", line 20, in <module>ReferenceError: weakly-referenced object no longer exists[Finished in 0.012022405862808228s]

Weak references in dictionary elements

The module provides two important classes which can be used to implement weak references in dictionaries i.e the WeakValueDictionary and the WeakKeyDictionary classes.

WeakValueDictionary

The WeakValueDictionary class creates dictionary objects in which the values are held weakly. An item is kept in the dictionary as long as its value is strong-referenced elsewhere. 

ExampleEdit & Run
import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})"
    def __repr__(self):
         return f"Bear({self.name})" 

b1 = Bear('Teddy')
b2 = Bear('Mike')
b3 = Bear('Joe')
b4 = Bear('Jim')
b5 = Bear('Ben')

D = weakref.WeakValueDictionary({'teddy': b1, 'mike': b2, 'joe': b3, 'jim': b4, 'ben': b5})

print('Before: ')
print(*D.values(), sep = ', ')

#delete some values
del b1
del b3
del b5

print('After: ')
print(*D.values(), sep = ', ') #Only items whose values are strong referenced stays in the dictionary
Output:
Before: Bear(Teddy), Bear(Mike), Bear(Joe), Bear(Jim), Bear(Ben)After: Bear(Mike), Bear(Jim)[Finished in 0.01214966387487948s]

In the above example, we created a WeakValueDictionary called D. As you can see from the outputs, whenever a value is deleted the dictionary item with that value is removed from the dictionary.

WeakKeyDictionary

The WeakKeyDictionary class works like the WeakValueDictionary but with keys rather than values.

It creates a dictionary in which items are kept only if they are strong referenced elsewhere in the program.

ExampleEdit & Run
import weakref 

class Bear: 
    def __init__(self, name):
         self.name = name 
    def __str__(self):
         return f"Bear({self.name})"
    def __repr__(self):
         return f"Bear({self.name})" 

key1 = Bear('Teddy')
key2 = Bear('Mike')
key3 = Bear('Joe')
key4 = Bear('Jim')
key5 = Bear("Ben")

D = weakref.WeakKeyDictionary({key1: 'teddy', key2: 'mike', key3: 'joe', key4: 'jim', key5:'ben'})

print("Before: ")
print(*D.keys(), sep = ', ')

#delete some keys
del key1
del key2
del key3

#deleted keys are now removed
print('After: ')
print(*D.keys(), sep = ', ')
Output:
Before: Bear(Teddy), Bear(Mike), Bear(Joe), Bear(Jim), Bear(Ben)After: Bear(Jim), Bear(Ben)[Finished in 0.011452395003288984s]

As shown above, item whose key has been deleted and is no longer strong referenced anywhere else in the program, is automatically removed from the WeakKeyDictionary.