Python3只保留了PyLongObject,我看的版本为3.11
结构体
源文件:Include/pytypedefs.h
typedef struct _longobject PyLongObject;
源文件:Include/cpython/longintrepr.h
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
使用了不定长对象头部,可以知道PyLongObject为变长对象。还有一个digit数组
typedef uint32_t digit;
可以看出,int对象是通过整数数组来实现的。但ob_digit数组的长度为1,这是为啥呢?翻阅资料得知由于 C 语言中数组长度不是类型信息,我们可以根据实际需要为 ob_digit 数组分配足够的内存,并将其当成长度为 n 的数组操作。这也是 C 语言中一个常用的编程技巧。
类型对象
源文件:Objects/longobject.c
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
0, /* tp_dealloc */
long_to_decimal_string, /* tp_repr */
&long_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
// ......
0, /* tp_init */
0, /* tp_alloc */
long_new, /* tp_new */
PyObject_Free, /* tp_free */
};
可以看到tp_name为int,创建整数对象调用long_new
创建整数对象
源文件:Objects/clinic/longobject.c.h
static PyObject *
long_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
static const char * const _keywords[] = {"", "base", NULL};
static _PyArg_Parser _parser = {NULL, _keywords, "int", 0};
PyObject *argsbuf[2];
PyObject * const *fastargs;
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0;
PyObject *x = NULL;
PyObject *obase = NULL;
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 0, 2, 0, argsbuf);
if (!fastargs) {
goto exit;
}
if (nargs < 1) {
goto skip_optional_posonly;
}
noptargs--;
x = fastargs[0];
skip_optional_posonly:
if (!noptargs) {
goto skip_optional_pos;
}
obase = fastargs[1];
skip_optional_pos:
return_value = long_new_impl(type, x, obase);
exit:
return return_value;
}
可以看到具体实现,调用long_new_impl
源文件:Objects/longobject.c
static PyObject *
long_new_impl(PyTypeObject *type, PyObject *x, PyObject *obase)
/*[clinic end generated code: output=e47cfe777ab0f24c input=81c98f418af9eb6f]*/
{
Py_ssize_t base;
if (type != &PyLong_Type)
return long_subtype_new(type, x, obase); /* Wimp out */
if (x == NULL) {
if (obase != NULL) {
PyErr_SetString(PyExc_TypeError,
"int() missing string argument");
return NULL;
}
return PyLong_FromLong(0L);
}
/* default base and limit, forward to standard implementation */
if (obase == NULL)
return PyNumber_Long(x);
base = PyNumber_AsSsize_t(obase, NULL);
if (base == -1 && PyErr_Occurred())
return NULL;
if ((base != 0 && base < 2) || base > 36) {
PyErr_SetString(PyExc_ValueError,
"int() base must be >= 2 and <= 36, or 0");
return NULL;
}
if (PyUnicode_Check(x))
return PyLong_FromUnicodeObject(x, (int)base);
else if (PyByteArray_Check(x) || PyBytes_Check(x)) {
const char *string;
if (PyByteArray_Check(x))
string = PyByteArray_AS_STRING(x);
else
string = PyBytes_AS_STRING(x);
return _PyLong_FromBytes(string, Py_SIZE(x), (int)base);
}
else {
PyErr_SetString(PyExc_TypeError,
"int() can't convert non-string with explicit base");
return NULL;
}
}
看源码我们可以知道
- 当x == NULL 且 obase != NULL时返回错误信息
int(base = 0) // int() missing string argument
-
当x == NULL 且 obase == NULL时,调用PyLong_FromLong(0L)
-
当x != NULL obase 为 NULL 调用 PyNumber_Long(x)
-
当x 和 obase 都不为 NULL
如果(base != 0 && base < 2) || base > 36, 报错
int('0b100', base=1) // int() base must be >= 2 and <= 36, or 0
PyUnicode 调用 PyLong_FromUnicodeObject,最终调用 PyLong_FromString
PyByteArray/PyBytes 调用_PyLong_FromBytes,最终调用 PyLong_FromString
小整数静态对象池
通过整数对象的结构体,知道整数对象是可变对象,整数运算结果都是以新对象返回的
a = 1
id(a) // 4310199024
a += 1
id(a) // 4310199056
这样如果循环上千次,就会创建上千个对象,会有大量的内存分配、销毁,性能很差。Python设计者当然不允许有这样的缺陷,它预先将常用的整数对象创建好,这就是小整数对象池
我们看看,创建整数对象调用的方法,Objects/longobject.c
PyObject *
PyLong_FromLong(long ival)
{
PyLongObject *v;
unsigned long abs_ival, t;
int ndigits;
/* Handle small and medium cases. */
if (IS_SMALL_INT(ival)) {
return get_small_int((sdigit)ival);
}
// ......
return (PyObject *)v;
}
可以看到会检测是否是小整数
#define IS_SMALL_INT(ival) (-_PY_NSMALLNEGINTS <= (ival) && (ival) < _PY_NSMALLPOSINTS)
源文件:Include/internal/pycore_global_objects.h
#define _PY_NSMALLPOSINTS 257
#define _PY_NSMALLNEGINTS 5
struct _Py_global_objects {
struct {
/* Small integers are preallocated in this array so that they
* can be shared.
* The integers that are preallocated are those in the range
* -_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (exclusive).
*/
PyLongObject small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];
// ......
};
- _PY_NSMALLPOSINTS宏规定了对象池正数个数,默认257个
- _PY_NSMALLNEGINTS宏规定了对象池负数个数,默认5个
- small_ints 是一个整数对象数组,保存预先创建好的小整数对象
Python启动后静态创建一个包含262个元素的整数数组,并初始化-5、-4、...、0、1、...、256这些整数对象。
到这我们知道,如果是小整数的话,内存地址应该是一样的
a = 2 - 1
id(a) // 4310199024
b = 1
id(b) // 4310199024
c = 0 + 1
id(c) // 4310199024
结果都是1,在小整数范围内,Python会直接从静态对象池中取出整数1。
a = 257
id(a) // 4312383408
b = 256+1
id(b) // 4312383440
这里a和b的结果257,但257不在小整数范围内,Python会分别创建对象返回,因此a和b绑定的对象id也就不同。
但是,喜欢尝试的同学,在Pycharm中运行时,发现id是一样的
a = 257
print(id(a)) // 4327165136
b = 256+1
print(id(b)) // 4327165136
这是为啥呢?查资料得到的结果:本质上是和字节码有关的,在IDLE中,每个命令都会单独编译,而在Pycharm中编译整个py文件。后续我们专门写一篇文章介绍字节码
整数的存储结构
我们可以在使用print输出整数时,输出ob_size和ob_digit。通过类型对象中的tp_repr,我们知道是调用函数long_to_decimal_string,而这个函数实际调用的是long_to_decimal_string_internal
源文件:Objects/longobject.c
static int
long_to_decimal_string_internal(PyObject *aa,
PyObject **p_output,
_PyUnicodeWriter *writer,
_PyBytesWriter *bytes_writer,
char **bytes_str)
{
PyLongObject *scratch, *a;
PyObject *str = NULL;
Py_ssize_t size, strlen, size_a, i, j;
digit *pout, *pin, rem, tenpow;
int negative;
int d;
enum PyUnicode_Kind kind;
a = (PyLongObject *)aa;
printf("ob_size = %d\n", Py_SIZE(a));
for (int index = 0; index < abs(Py_SIZE(a)); ++index) {
printf("ob_digit[%d] = %d\n", index, a->ob_digit[index]);
}
if (a == NULL || !PyLong_Check(a)) {
PyErr_BadInternalCall();
return -1;
}
// ......
}
然后我们重新编译源码,这里大概讲一下
make clean
./configure --prefix=<你要安装的路径>
make && make install
然后进入安装路径,bin目录下,即可执行运行重新编译后的*Python*
接下来,我们按照正数、负数、零来找几个案例看看输出结果
print(0) # ob_size = 0
print(5) # ob_size = 1 ob_digit[0] = 5
print(-5) # ob_size = -1 ob_digit[0] = 5
print(10000000000) # ob_size = 2 ob_digit[0] = 336323584 ob_digit[1] = 9
print(-10000000000) # ob_size = -2 ob_digit[0] = 336323584 ob_digit[1] = 9
从输出结果可以看出:
-
整数0,ob_size=0,ob_digit为空,无需分配
-
整数5,绝对值保存在ob_digit数组中,数组长度为1,ob_size=1
-
整数-5,绝对值保存在ob_digit数组中,数组长度为1,ob_size=-1
-
整数10000000000,ob_size=2,两个数组元素为336323584、9,如何得到10000000000呢?
336323584 * 2 ** (30 * 0) + 9 * 2 ** (30 * 1) = 10000000000
注:这里的 30 是由 PyLong_SHIFT 决定的,64 位系统中,PyLong_SHIFT 为 30,否则 PyLong_SHIFT 为 15
#define PyLong_SHIFT 30
- 整数-10000000000,ob_size=-2
总结:
- 整数的绝对值保存在ob_digit数组中
- ob_digit数组长度保存在ob_size字段,如果是负数,则ob_size结果为负
- 0的数组为空
整数对象的数值操作
通过类型对象中tp_as_number、tp_as_sequence、tp_as_mapping可以看到整数对象支持数值型操作long_as_number
源文件:Objects/longobject.c
static PyNumberMethods long_as_number = {
(binaryfunc)long_add, /*nb_add*/
(binaryfunc)long_sub, /*nb_subtract*/
(binaryfunc)long_mul, /*nb_multiply*/
long_mod, /*nb_remainder*/
long_divmod, /*nb_divmod*/
long_pow, /*nb_power*/
(unaryfunc)long_neg, /*nb_negative*/
long_long, /*tp_positive*/
(unaryfunc)long_abs, /*tp_absolute*/
(inquiry)long_bool, /*tp_bool*/
(unaryfunc)long_invert, /*nb_invert*/
long_lshift, /*nb_lshift*/
long_rshift, /*nb_rshift*/
long_and, /*nb_and*/
long_xor, /*nb_xor*/
long_or, /*nb_or*/
long_long, /*nb_int*/
0, /*nb_reserved*/
long_float, /*nb_float*/
0, /* nb_inplace_add */
0, /* nb_inplace_subtract */
0, /* nb_inplace_multiply */
0, /* nb_inplace_remainder */
0, /* nb_inplace_power */
0, /* nb_inplace_lshift */
0, /* nb_inplace_rshift */
0, /* nb_inplace_and */
0, /* nb_inplace_xor */
0, /* nb_inplace_or */
long_div, /* nb_floor_divide */
long_true_divide, /* nb_true_divide */
0, /* nb_inplace_floor_divide */
0, /* nb_inplace_true_divide */
long_long, /* nb_index */
};
从源码中可以看出,整数对象支持加(long_add)、减(long_sub)、乘(long_mul)、除(long_divmod)、取模(long_mod)、指数(long_pow)等
加
static PyObject *
long_add(PyLongObject *a, PyLongObject *b)
{
CHECK_BINOP(a, b);
return _PyLong_Add(a, b);
}
可以看到会调用CHECK_BINOP宏检查参数的类型,然后核心逻辑调用_PyLong_Add
PyObject *
_PyLong_Add(PyLongObject *a, PyLongObject *b)
{
if (IS_MEDIUM_VALUE(a) && IS_MEDIUM_VALUE(b)) {
return _PyLong_FromSTwoDigits(medium_value(a) + medium_value(b));
}
PyLongObject *z;
if (Py_SIZE(a) < 0) {
if (Py_SIZE(b) < 0) {
z = x_add(a, b);
if (z != NULL) {
/* x_add received at least one multiple-digit int,
and thus z must be a multiple-digit int.
That also means z is not an element of
small_ints, so negating it in-place is safe. */
assert(Py_REFCNT(z) == 1);
Py_SET_SIZE(z, -(Py_SIZE(z)));
}
}
else
z = x_sub(b, a);
}
else {
if (Py_SIZE(b) < 0)
z = x_sub(a, b);
else
z = x_add(a, b);
}
return (PyObject *)z;
}
- 开始调用IS_MEDIUM_VALUE宏检查a和b是否是中等大小的PyLong对象,如果是直接采用一种优化的方式计算并返回。就是调用_PyLong_FromSTwoDigits得到PyLong对象
- 如果a为负数、b为负数,调用x_add计算两者绝对值之后,在将结果设置为负
- 如果a为负数、b为正数,调用x_sub计算两者绝对值之差
- 如果a为正数、b为负数,调用x_sub计算两者绝对值之差
- 如果a为正数、b为正数,调用x_add计算两者绝对值之和
可以看到,加法运算实际转化为了绝对值加法x_add、绝对值减法x_add以及转为C整数想加
转为C整数想加
先来看看优化计算方式,怎么判断是否是中等大小的PyLong对象呢?
/* Is this PyLong of size 1, 0 or -1? */
#define IS_MEDIUM_VALUE(x) (((size_t)Py_SIZE(x)) + 1U < 3U)
看注释知道判断PyLong对象的大小是否是1、0或者-1
/* convert a PyLong of size 1, 0 or -1 to a C integer */
static inline stwodigits
medium_value(PyLongObject *x)
{
assert(IS_MEDIUM_VALUE(x));
return ((stwodigits)Py_SIZE(x)) * x->ob_digit[0];
}
medium_value是将中等大小的PyLong转为C整数
我们在源码中加入打印,看啥时候会转为C整数
PyObject *
_PyLong_Add(PyLongObject *a, PyLongObject *b)
{
if (IS_MEDIUM_VALUE(a) && IS_MEDIUM_VALUE(b)) {
PyObject *str = PyUnicode_FromString("IS_MEDIUM_VALUE: true");
PyObject_Print(str, stdout, 0);
printf("\n");
return _PyLong_FromSTwoDigits(medium_value(a) + medium_value(b));
}
PyLongObject *z;
if (Py_SIZE(a) < 0) {
// ......
}
重新编译后,我们执行
0 + 5 // IS_MEDIUM_VALUE: true
-5 + (-5) // IS_MEDIUM_VALUE: true
2 ** 30 -1 + (2 ** 30 -1) // IS_MEDIUM_VALUE: true
2 ** 30 -1 + (2 ** 30) // 不会输出IS_MEDIUM_VALUE: true
根据之前打印ob_size我们很容易知道,0的ob_size为0,5的ob_size为1,-5的ob_size为-1,2 ** 30 -1的ob_size为1,2 ** 30的ob_size为2,因此最后一个不满足中等PyLong对象的条件。
这样我们看到ob_size为1的整数范围(-2**30,2**30),开区间
这样设计的好处:可以将性能损耗降低,想想如果是大整数相加,值越大,底层数组越长,运算开销越大。
x_add
大整数相加是如何实现的呢?(数组长度大于1)
static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
PyLongObject *z;
Py_ssize_t i;
digit carry = 0;
/* Ensure a is the larger of the two: */
if (size_a < size_b) {
{ PyLongObject *temp = a; a = b; b = temp; }
{ Py_ssize_t size_temp = size_a;
size_a = size_b;
size_b = size_temp; }
}
z = _PyLong_New(size_a+1);
if (z == NULL)
return NULL;
for (i = 0; i < size_b; ++i) {
carry += a->ob_digit[i] + b->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
for (; i < size_a; ++i) {
carry += a->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
z->ob_digit[i] = carry;
return long_normalize(z);
}
大概解释一下:
首先,计算a、b的数组长度
然后,如果a的数组长度小于b的数组长度,进行交换,使长度大的在前面
然后,创建一个大小为 size_a+1 的新的长整数对象,并将其赋给变量 z,保存最终计算结果
如果对象创建失败(即内存分配失败),则返回空指针表示创建失败
然后,遍历数组长度小的b,carry += a->ob_digit[i] + b->ob_digit[i];:将进位标志 carry、a 中的第 i 个数字和 b 中的第 i 个数字相加,结果保存到 carry 中。z->ob_digit[i] = carry & PyLong_MASK;将 carry 的低 PyLong_SHIFT 位保存到 z 的第 i 个数字中,其中 PyLong_MASK 是一个掩码,用于仅保留低 PyLong_SHIFT 位;carry >>= PyLong_SHIFT;:将 carry 右移 PyLong_SHIFT 位,舍去已经处理过的位,以便下一轮循环使用。
然后,遍历a中剩余的数字
最后,将carry的值保存到z的最高位数字中
返回,标准化z,去除计算结果z底层数组中前面多余的o
画个图来理解
2**30 + 2**30
x_sub
static PyLongObject *
x_sub(PyLongObject *a, PyLongObject *b)
{
Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
PyLongObject *z;
Py_ssize_t i;
int sign = 1;
digit borrow = 0;
/* Ensure a is the larger of the two: */
if (size_a < size_b) {
sign = -1;
{ PyLongObject *temp = a; a = b; b = temp; }
{ Py_ssize_t size_temp = size_a;
size_a = size_b;
size_b = size_temp; }
}
// ......
}
这部分源码可以自行看看,差不多
到这里我们应该了解了整数的底层逻辑,想必接下来这道题你肯定知道答案了
看一段Java程序
class Demo{
public static void main(String[] args) {
int a = 1000000;
System.out.println(a*a);
}
}
发现输出结果不是1000000000000,而是-727379968
我们在看看Python程序
1000000 * 1000000 # 1000000000000
发现Python程序执行结果正常,这是为啥呢?
在计算机中,由于变量类型存储空间固定,它能表示的数值范围也是有限的。以 int 为例,该类型长度为 32 位,能表示的整数范围为 [-2**31, 2**31 - 1] 。一万亿显然超出该范围,也就是程序发生了 整数溢出 。
而Python的int对象是通过整数数组实现的,之前我们输出ob_size,知道大整数的ob_size>1,也就是Python将其拆成若干部分,保存到ob_digit数组中。
想要第一时间看到最新文章,可以关注公众号:郝同学的测开日记