This article was written in September 2020. It is a small report on the interesting parts of the CPython interpreter for the “Computer Architecture Theory 2” subject of my university. The writing may be a bit messy. If you are interested, please read. :)
This report mainly uses the approach of reading source code and doing code experimental to understand the internal implementation of
Comparison operators and
I found that
comparison operators such as
< are processed as
CPython calls the
tp_compare function of either first or second operand’s type,
tp_compare results in invocation of special methods such as
__eq__ (handled in
CPython without calling special methods for the builtin types), for a default
tp_compare function is used for
!= if the classes do not define the special methods
__ne__. For ease of use, I have organized the built-in types’
Richcmp method list. I will give more detailed content and analysis process in the article.
At last, I give a simple analysis of the caching mechanism of small numbers in
int type. I found that
CPython will cache the
int numbers in the range [-5, 256], analysis process in the section <Other interesting findings>.
In the Python language, there is a pair
It can be used to determine whether two objects are the same instance, that is, whether two objects exist at the same memory address. As shown below:
However, there is a very unintuitive situation, which makes many people feel confused:
is operators do not exist in many other languages, many beginners are easy to confuse it with
== operator, and many materials are not explained thoroughly, so I want to study the implementation principles.
This article is the analysis based on the latest version of
CPython 3.10 dev version (2020-08-20) of the master branch source code, I found that the implementation is quite different from the current stable version of
CPython 3.8, please pay attention to the versionW difference. Now let’s go to the topic.
First, let’s pull the latest version of the source code to compile and run compile the Python, in the shell window, use
dis module, to analyze
== CPython disassembly code:
We can be seen from the bytecode, the latest development version 3.10,
is is using the
IS_OP process flow (
oparg = 0), and
== by using
COMPARE_OP process flow (
oparg = 2).
Following the clue, we searched in the source code
IS_OP and found the code in
It can be found that the core function of the
IS_OP operator is very simple. We know that all types in Python are from generic
PyObject, so the value on the left of the operator
left and the value on the right of the operator
right here just pointers. From this, we can find that
is operator only compares the memory pointers of the
right. If the two pointers are equal (the memory address is the same), then return
Py_True otherwise then return
What about the
== operators? I searched in the source code
COMPARE_OP and found the code in
I noticed that the
PyObject_RichCompare function is called here, and passing the value on the left of the operator as
left, the value on the right of the operator as
right and the
Rich comparison opcode(for example
oparg. And let me check the source code of this function (located at
We found that the main body of this function is a security check, and the most critical step is to call it
do_richcompare, so we continue to look down:
This function is relatively long, let’s analyze it part by part.
Take a look at this function,it uses
tp_richcompare, the constants
Py_NE, we can find the relevant code in
And look at the document of
PyObject *tp_richcompare(PyObject *self, PyObject *other, int op);
The first parameter is guaranteed to be an instance of the type that is defined by
The function should return the result of the comparison (usually
Py_False). If the comparison is undefined, it must return
Py_NotImplemented, if another error occurred it must return
NULLand set an exception condition.
So I sorted out the sheet:
|op||op method||op arg|
|Py_LT = 0|
|Py_LE = 1|
|Py_EQ = 2|
|Py_NE = 3|
|Py_GT = 4|
|Py_GE = 5|
We can override any one or more of the above methods to reload the corresponding operation symbols.
Each object in Python is associated with a type. There is a
tp_richcompare function pointer in the type to determine the behavior of rich compare between objects.
By calling the
tp_richcompare function of a given type,
CPython runs the special methods (defined by Python code in general) to do the comparisons.
For example, you can define special methods
__eq__ in your custom classes of the following example. The example show which special method is called for various combinations of the types of left and right of “==” comparison code.
The builtin types usually have
__ne__. A part of the builtin types implement all the comparisons including
__lt__. We can see that by running simple code:
Many of their special methods are implemented in C (e.g.,
long_richcompare() function of
CPython for the
int type), I will list the default implementations in
You may want to know what if
__lt__ of the builtin types such as
int is not implemented in C. The
< shows a better performance than calling
__le__ because of no invocation of Python methods. Compare the times below.
x < 2 does not need method invocation but
Then, let’s see the 3
ifs above in the
do_richcompare function, it means that there are three situations.
The first case
w are of different types.
w’s class is a subclass of
v’s class. If
w overloads a certain
richcompare method, the
richcompare method in
w is called. Here I give an example:
The second case
w are of the same type, or
w’s class is not a subclass of
v’s class, or
w does not have a
richcompare method, if
v defined a
richcompare method, then call the
richcompare method in
v. Let’s do an experiment:
The third case
w’s class is not a subclass of of
v’s class, in
v does not define or inherit the
richcompare method, but the
richcompare method is defined in
w, then the
richcompare method in
w will be called, and we continue to test with the code defined in the previous example:
Next, the function enters into a
If the above three conditions are not present, and finally the function will compare pointers through the switch branch (
!=), the result is just the same as
is operator, if not
!= , then thrown the type of error directly.
Because all types are initialized the default
class type is
object_richcompare, as an example, the mechanism of
object_richcompare will be introduced below), only if the above
tp_richcompare is called and returns
Py_NotImplemented, can this switch branch code be executed.
We can do the following experiment to verify.
The default implementation of
Why we have neither defined the
!=) methods in
class A and
class B, but we can compare them normally, and other symbols can’t? I found the relevant code in
So far we know that all class types use the built-in
object_richcompare function by default. By looking at this function, we can find that the
Py_NE has been implemented by default:
We cloud find here
case Py_EQ is just a simple pointer comparison, if the same is
Py_True, otherwise it is
Py_NotImplemented. If it returns
Py_NotImplemented, the comparison work will be handed over according to priority.
But it should be noted that in
Py_NE the function tries to call
tp_richcompare has been implemented:
This means that if the
Py_NE has not been rewrote, the function will try to call the case
Py_EQ, and get the result value, if in
True case, it will return
Py_False, else then returns
After the above analysis and experiments, I believe that you have a very clear understanding of its implementation. Let me organize a table of rules below.
do_richcompare(v, w, op)
- The following
√means that class has a not
tp_richcompareand it can return
- The following
×means that class’s
|0||baseclass of ||subclass of |
Other interesting findings
Now, we are very clear about the realization principle of Python’s
==. But do you still remember the incredible code at the beginning?
They are both digital, are there
114514 essential differences? I want to do an experiment:
According to the output result, we find that in
a is b is
True, but other numbers are
False. It obviously relates to the implementation of Python’s
int type. In Python3, the
int type is no matter the magnitudes of the number, they are all
So let’s read
Include/longobject.c. I found the
_PyLong_Init function, and found that in this function, a
small_ints array was loaded in the thread:
Read the definition of this array:
I found that the size is
_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS, and the range is [-5, 257)
In Python 3, all of the
int will call the
PyLong_FromLong function, so let’s take look at this function:
This will use the macro
IS_SMALL_INT to judge whether the number is in the range [-5, 256], if in the range then call
get_small_int and return the result.
get_small_int function, so far we can be surely known that if a number in the range, it should have existed in
CPython will not allocate a new object for it.
At this point, this strange question has also been answered: Python caches these numbers in memory, that is, the numbers in
small_ints, Python will not allocate memory for them again, but use them directly.
It can be seen that this is indeed the case.
My study analyzed
== the differences and connections, in general:
iscompare whether the memory addresses of the two objects are the same, that is, whether they are the same object.
richcompare, unless the type of the object rewrote
tp_richcompare, otherwise compare the memory addresses of two objects by default, the same approach as
Python’s commonly used built-in types such as
dict all have a default implementation of
I search the source code and read it, and find the following examples below (maybe incomplete).
|Python type||Richcmp method|
Python language has accelerated my development cycle. Personally, I like Python very much now, but I hadn’t delved into many details. After reading the
CPython source code this time, I understood many things that I thought were incredible and benefited a lot.