This article discusses reference counting and object deallocation based on the CPython implementation. Other Python interpreters may behave differently.
When learning Python or other object-oriented languages, we often hear statements like:
"Create a
catobject, then destroy it withdel."
This kind of expression is convenient for beginners, but it hides many important details.
Strictly speaking, a variable is not equal to an object, and del does not necessarily "destroy an object." If you can't distinguish between names, references, and objects, you'll easily get confused when trying to understand shallow copy, deep copy, mutable objects, circular references, and garbage collection.
This article starts with the following line of code:
cat = Cat("white")
It analyzes the relationship between classes, objects, variables, and garbage collection from both the Python language semantics and the CPython implementation levels.
1. What Exactly Are Classes, Objects, and Variables?
1.1 Class: The Rules for Creating Objects, and Itself an Object
A class describes the attributes and behaviors that a category of objects possesses.
class Cat:
def __init__(self, color):
self.color = color
def meow(self):
print("Meow")
Here, Cat defines:
- How instances are initialized;
- What attributes instances can hold;
- What methods instances can execute.
You can think of a class as a blueprint for a building, but this analogy isn't entirely precise.
In Python, the class itself is also an object:
print(type(Cat))
The output is:
<class 'type'>
Therefore, a class is not an abstract description that takes up no memory. When you create a class, Python also needs to store information like the class name, methods, attributes, and inheritance relationships.
1.2 Object: A Data Entity That Exists During Runtime
Executing the following code:
cat = Cat("white")
Creates an instance of the Cat class. This instance holds its own state:
print(cat.color)
Output:
white
If you create another instance:
cat_2 = Cat("black")
Then cat and cat_2 point to two different objects:
print(cat is cat_2)
Output:
False
They can share the methods defined in the class, but each has its own independent instance attributes.
1.3 Variable: A Name Bound to an Object
In Python, a more accurate statement is not "the variable contains the object," but rather:
A name is bound to an object.
Executing:
cat = Cat("white")
Can be understood as completing two main steps:
Cat("white")creates a new instance object;- The name
catestablishes a binding relationship with this object.
This can be represented by the following diagram:
Namespace Python Object
cat ───────────────────────▶ Cat Instance
color = "white"
We find the object through the name cat, and then access its attributes and methods:
cat.color
cat.meow()
It's important to note that simply describing a Python variable as a "physical memory address stored on the stack" is not rigorous.
"References on the stack, objects on the heap" can serve as an introductory analogy for understanding the memory model of high-level languages, but the Python language specification does not require names to be stored on the stack. The storage methods for local variables, global variables, closure variables, and object attributes are not all the same.
Therefore, a more general and accurate expression is:
A Python name exists within a certain namespace and is bound to the corresponding object.
2. What Actually Happens with cat = Cat("white")?
Let's first look at the complete code:
class Cat:
def __new__(cls, color):
print("1. Allocate and create instance")
return super().__new__(cls)
def __init__(self, color):
print("2. Initialize instance")
self.color = color
cat = Cat("white")
Execution result:
1. Allocate and create instance
2. Initialize instance
Conceptually, this process can be broken down into the following steps.
Step 1: Look Up the Name Cat
The interpreter first looks up Cat in the current scope and finds the corresponding class object.
Step 2: Call the Class Object
Executing Cat("white") is essentially calling the Cat class. In common cases, this involves:
__new__()creates and returns the instance;__init__()initializes the instance.
It's crucial to note:
__new__()is responsible for creating the instance;__init__()is responsible for initializing the instance;__init__()is not actually responsible for allocating the object.
Step 3: Establish Name Binding
After the instance is created, the assignment statement binds the name cat to this instance. At this point, you can manipulate the object through cat.
3. Multiple Variables Can Point to the Same Object
Execute the following code:
cat = Cat("white")
another_cat = cat
This does not create a second cat; instead, it binds two names to the same object:
cat ───────────────┐
├────▶ Cat Instance
another_cat ───────┘ color = "white"
You can verify the object's identity with is:
print(cat is another_cat)
Output:
True
Therefore, modifying the object through one name will be observable through the other name:
another_cat.color = "black"
print(cat.color)
Output:
black
The reason is not that the data of the two variables is automatically synchronized, but that they have always pointed to the same object. This is also the foundation for understanding shallow copy, mutable objects, and function argument passing.
4. What Does del cat Actually Delete?
Assume the following code exists:
cat = Cat("white")
another_cat = cat
del cat
After executing del cat, what is deleted is the binding between the name cat and the object. At this point, the object can still be accessed through another_cat:
print(another_cat.color)
Output:
white
This shows that:
deldeletes a name binding; it is not equivalent to directly destroying the object.
Only when an object no longer has any strong references can it enter the deallocation process.
5. CPython's Reference Counting
CPython is the most commonly used Python implementation. It primarily manages object lifecycles through reference counting.
Each object records how many strong references currently point to it. You can observe this using sys.getrefcount():
import sys
cat = Cat("white")
print(sys.getrefcount(cat))
The actual output is usually 1 more than expected because passing cat to getrefcount() temporarily increases the reference count by one for the function call itself.
Continue executing:
another_cat = cat
print(sys.getrefcount(cat))
The reference count will increase accordingly. After deleting a name, the reference count decreases:
del another_cat
print(sys.getrefcount(cat))
When an object's reference count drops to 0, CPython typically enters the object deallocation process immediately.
Therefore, describing this process as "the garbage collector patrols periodically and then discovers the object" is inaccurate. For ordinary, non-cyclically referenced objects, CPython's reference counting usually handles them immediately.
6. Why Isn't Reference Counting Enough?
Reference counting alone cannot solve the problem of circular references.
class Node:
def __init__(self):
self.next = None
node_a = Node()
node_b = Node()
node_a.next = node_b
node_b.next = node_a
At this point, a cycle is formed between the objects:
node_a object ─────▶ node_b object
▲ │
└─────────────────┘
Even if you delete the external names:
del node_a
del node_b
The two objects still reference each other internally. To solve this problem, CPython also provides a cyclic garbage collection mechanism, which is used to find objects that are no longer reachable from the program but haven't had their count reach zero due to mutual references.
Therefore, CPython's memory management can be summarized as:
Reference Counting: Handles most ordinary objects
Cyclic Garbage Collection: Supplementally handles circular references
They are not substitutes for each other.
7. __del__() Is Not the Same as del
These two names are very similar, but their functions are completely different.
del
del is a Python statement used to delete names, container elements, or object attributes:
del cat
del numbers[0]
del user.name
__del__()
__del__() is an object's finalizer. When an object is about to be reclaimed, the interpreter may call it:
class Cat:
def __init__(self, name):
self.name = name
def __del__(self):
print(f"{self.name} object is about to be reclaimed")
cat = Cat("Xiao Bai")
del cat
In a simple CPython scenario, it might output immediately:
Xiao Bai object is about to be reclaimed
However, you should not rely on __del__() as a reliable resource management tool. Reasons include:
- When exactly an object is reclaimed can depend on the interpreter implementation;
- Circular references can make the object's lifecycle more complex;
- When the interpreter exits, global objects may have already been partially cleaned up;
- Exceptions occurring within
__del__()are not easy to handle correctly; - An object could even re-establish references within
__del__().
Therefore, you should not rely on __del__() to close files, database connections, or network connections.
8. Use Context Managers for Resource Release
When managing external resources like files, it's recommended to use with:
with open("example.txt", "r", encoding="utf-8") as file:
content = file.read()
After leaving the with block, even if an exception occurs, the file will be closed correctly.
For custom resources, you can implement the context management protocol:
class Connection:
def __enter__(self):
print("Establishing connection")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Closing connection")
with Connection() as connection:
print("Using connection")
Output:
Establishing connection
Using connection
Closing connection
This is clearer, safer, and easier to maintain than relying on __del__().
9. Object Identity and Memory Address
Python provides the id() function, which returns a unique identifier for an object during its current lifetime:
cat = Cat("white")
print(id(cat))
In CPython, id() is usually related to the object's current memory address, but the Python language specification only guarantees:
An object has a unique and unchanging
idduring its lifetime.
After an object is deallocated, its identifier may be reused by a new object. Therefore, you should not treat id() as a permanent serial number.
To check if two names point to the same object, use is; to compare whether the values of two objects are equal, use ==:
print(object_a is object_b)
print(object_a == object_b)
is: Are they the same object?==: Are the values of the objects equal?
10. Complete Experiment: Observing Object Lifecycle
You can observe name binding and object reclamation with the following code:
import sys
class Cat:
def __init__(self, name):
self.name = name
print(f"Creating object: {self.name}")
def __del__(self):
print(f"Reclaiming object: {self.name}")
cat = Cat("Xiao Bai")
print("First reference count:", sys.getrefcount(cat))
another_cat = cat
print("Second reference count:", sys.getrefcount(cat))
del cat
print("After deleting cat, the object still exists")
print("another_cat.name =", another_cat.name)
del another_cat
print("The last name binding has been deleted")
You might get a similar result:
Creating object: Xiao Bai
First reference count: 2
Second reference count: 3
After deleting cat, the object still exists
another_cat.name = Xiao Bai
Reclaiming object: Xiao Bai
The last name binding has been deleted
This experiment shows:
- After creating an instance, the name
catis bound to the object; another_cat = catdoes not copy the object;del catonly deletes one name binding;- As long as another strong reference exists, the object will not be deallocated;
- After the last strong reference disappears, CPython usually deallocates the object immediately.
11. Summary
When understanding the Python object model, keep the following points in mind:
11.1 Classes Are Also Objects
Classes are not just for describing instances; they themselves are managed by Python's object system.
11.2 Variables Are Name Bindings
A Python variable is not a box that holds an object; it is a name within a namespace that is bound to an object.
11.3 Assignment Usually Does Not Copy Objects
b = a
Usually means making b and a point to the same object, not automatically creating a copy of the data.
11.4 del Deletes the Binding Relationship
del a
Does not mean the object is immediately destroyed. The object can only be deallocated if no other valid references exist.
11.5 CPython Primarily Uses Reference Counting
Ordinary objects are usually deallocated immediately when their reference count reaches zero; cyclic garbage collection is a supplement for handling circular references.
11.6 Do Not Rely on __del__() to Manage Critical Resources
Resources like files, locks, database connections, and network connections should be managed using context managers or explicit cleanup logic.
Conclusion
Object-oriented languages help us hide a lot of memory management details, but these details haven't disappeared.
When we move beyond surface-level statements like "creating a variable" and "deleting an object" and explore deeper, we find that what really happens is:
Create an object
↓
Establish a name binding
↓
Increase or decrease references
↓
Reference count reaches zero
↓
Deallocate the resources occupied by the object
Mastering the relationship between objects, names, references, and lifecycles not only helps us understand Python but also builds a more solid foundation for learning data structures, operating systems, compiler principles, and garbage collection algorithms.
Truly understanding a language means not just memorizing its syntax, but also understanding the operating mechanisms behind that syntax.