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. :)
Summary
This report mainly uses the approach of reading source code and doing code experimental to understand the internal implementation of CPython
’s Comparison operators
and Identity operators
.
I found that comparison operators
such as =
and <
are processed as COMPARE_OP
bytecode, 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 =
and !=
if the classes do not define the special methods __eq__
and __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 CPython
’s int
type. I found that CPython
will cache the int
numbers in the range [-5, 256], analysis process in the section <Other interesting findings>.
Introduction
In the Python language, there is a pair Identity operator
, is
and is not
.
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:
|
|
Since 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.
Analysis process
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 is
and ==
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
).
is
operator
Following the clue, we searched in the source code IS_OP
and found the code in Python/ceval.c
:
|
|
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 left
and right
. If the two pointers are equal (the memory address is the same), then return Py_True
otherwise then return Py_False
.
==
operator
What about the ==
operators? I searched in the source code COMPARE_OP
and found the code in Python/ceval.c
:
|
|
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 Py_EQ
) as oparg
. And let me check the source code of this function (located at Objects/object.c
):
|
|
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.
Rich compare
Take a look at this function,it uses tp_richcompare
, the constants Py_EQ
and Py_NE
, we can find the relevant code in Include/object.h
:
|
|
From Python 3.8, A new concept is proposed — tp slots
. Search it in Python C-API document (https://docs.python.org/3/c-api/typeobj.html), we can find what method tp_richcompare
corresponds to.
PyTypeObject Slot Type special methods/attrs Info tp_richcompare
richcmpfunc
__lt__, __le__, __eq__, __ne__, __gt__, __ge__
X G
And look at the document of tp_richcompare
:
1
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
PyTypeObject
.The function should return the result of the comparison (usually
Py_True
orPy_False
). If the comparison is undefined, it must returnPy_NotImplemented
, if another error occurred it must returnNULL
and set an exception condition.
So I sorted out the sheet:
op | op method | op arg |
---|---|---|
< | __lt__ | Py_LT = 0 |
<= | __le__ | Py_LE = 1 |
== | __eq__ | Py_EQ = 2 |
!= | __ne__ | Py_NE = 3 |
> | __gt__ | Py_GT = 4 |
>= | __ge__ | 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 __eq__
and __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 Conclusion
.
You may want to know what if __lt__
of the builtin types such as int
is not implemented in C. The int
’s <
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 x.__lt__(2)
does.
|
|
Then, let’s see the 3 if
s above in the do_richcompare
function, it means that there are three situations.
The first case
|
|
v
and 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
|
|
v
and 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:
|
|
Other situations
Next, the function enters into a switch
branch:
|
|
If the above three conditions are not present, and finally the function will compare pointers through the switch branch (==
and !=
), the result is just the same as is
operator, if not ==
or !=
, then thrown the type of error directly.
Because all types are initialized the default tp_richcompare
(For example, 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 richcompare
Why we have neither defined the __eq__
(==
) or __ne__
( !=
) methods in class A
and class B
, but we can compare them normally, and other symbols can’t? I found the relevant code in Objects/typeobject.c
:
|
|
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_EQ
and 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 Py_EQ
if 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 Py_True
.
Rules of do_richcompare
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.
Python do_richcompare(v, w, op)
- The following
√
means that class has a notNULL
tp_richcompare
and it can returnPy_True
orPy_False
. (tp_richcompare
Implemented) - The following
×
means that class’stp_richcompare
isNULL
ortp_richcompare
returnsPy_NotImplemented
. (tp_richcompare
Not Implemented)
Priority | v ’s class (Cv) | w ’s class (Cw) | Do |
---|---|---|---|
0 | baseclass of w ’s class | subclass of v ’s class | Py_TYPE(w)->tp_richcompare |
1 | √ | × | Py_TYPE(v)->tp_richcompare |
2 | × | √ | Py_TYPE(w)->tp_richcompare |
3 | × | × | switch branch |
Other interesting findings
Now, we are very clear about the realization principle of Python’s is
and ==
. But do you still remember the incredible code at the beginning?
|
|
They are both digital, are there1
and 114514
essential differences? I want to do an experiment:
|
|
According to the output result, we find that in range[-5, 256]
, 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 PyLongObject
.
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.
|
|
Look at get_small_int
function, so far we can be surely known that if a number in the range, it should have existed in small_ints
, so 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.
Conclusion
My study analyzed is
and ==
the differences and connections, in general:
- The
is
compare whether the memory addresses of the two objects are the same, that is, whether they are the same object. ==
is arichcompare
, unless the type of the object rewrotetp_richcompare
, otherwise compare the memory addresses of two objects by default, the same approach asis
consistent.
Python’s commonly used built-in types such as int
, str
, list
, and dict
all have a default implementation of tp_richcompare
.
I search the source code and read it, and find the following examples below (maybe incomplete).
Python type | Richcmp method |
---|---|
array.array | array_richcompare |
bytearray | bytearray_richcompare |
bytes | bytes_richcompare |
cell | cell_richcompare |
code | code_richcompare |
collections.deque | deque_richcompare |
collections.OrderedDict | odict_richcompare |
complex | complex_richcompare |
datetime.timezone | timezone_richcompare |
dict | dict_richcompare |
dict_items | dictview_richcompare |
dict_keys | dictview_richcompare |
float | float_richcompare |
frozenset | set_richcompare |
instancemethod | instancemethod_richcompare |
int | long_richcompare |
list | list_richcompare |
mappingproxy | mappingproxy_richcompare |
method | method_richcompare |
method-wrapper | wrapper_richcompare |
range | range_richcompare |
re.Pattern | pattern_richcompare |
set | set_richcompare |
slice | slice_richcompare |
str | PyUnicode_RichCompare |
tuple | tuplerichcompare |
weakref | weakref_richcompare |
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.