为什么Python中整型不会溢出

前言

本次分析基于 CPython 解释器,python3.x版本

在python2时代,整型有 int 类型和 long 长整型,长整型不存在溢出问题,即可以存放任意大小的整数。在python3后,统一使用了长整型。这也是吸引科研人员的一部分了,适合大数据运算,不会溢出,也不会有其他语言那样还分短整型,整型,长整型...因此python就降低其他行业的学习门槛了。

那么,不溢出的整型实现上是否可行呢?

不溢出的整型的可行性

尽管在 C 语言中,整型所表示的大小是有范围的,但是 python 代码是保存到文本文件中的,也就是说,python代码中并不是一下子就转化成 C 语言的整型的,我们需要重新定义一种数据结构来表示和存储我们新的“整型”。

怎么来存储呢,既然我们要表示任意大小,那就得用动态的可变长的结构,显然,数组的形式能够胜任:

[longintrepr.h]
struct _longobject {PyObject_VAR_HEADint *ob_digit;
};

 

长整型的保存形式

长整型在python内部是用一个 int 数组( ob_digit[n] )保存值的. 待存储的数值的低位信息放于低位下标, 高位信息放于高下标.比如要保存 123456789 较大的数字,但我们的int只能保存3位(假设):

ob_digit[0] = 789;
ob_digit[1] = 456;
ob_digit[2] = 123;

低索引保存的是地位,那么每个 int 元素保存多大的数合适?有同学会认为数组中每个int存放它的上限(2^31 - 1),这样表示大数时,数组长度更短,更省空间。但是,空间确实是更省了,但操作会代码麻烦,比方大数做乘积操作,由于元素之间存在乘法溢出问题,又得多考虑一种溢出的情况。

怎么来改进呢?在长整型的 ob_digit 中元素理论上可以保存的int类型有 32 位,但是我们只保存 15 位,这样元素之间的乘积就可以只用 int 类型保存即可, 结果做位移操作就能得到尾部和进位 carry 了,定义位移长度为 15

#define PyLong_SHIFT  15
#define PyLong_BASE ((digit)1 << PyLong_SHIFT)
#define PyLong_MASK ((digit)(PyLong_BASE - 1))

PyLong_MASK 也就是 0b111111111111111 ,通过与它做位运算  的操作就能得到低位数。

有了这种存放方式,在内存空间允许的情况下,我们就可以存放任意大小的数字了。

 

长整型的运算

加法与乘法运算都可以使用我们小学的竖式计算方法,例如对于加法运算:

  ob_digit[2]ob_digit[1]ob_digit[0]
加数a 23934543
加数b+ 454632
结果z 24389175

为方便理解,表格展示的是数组中每个元素保存的是 3 位十进制数,计算结果保存在变量z中,那么 z 的数组最多只要 size_a + 1 的空间(两个加数中数组较大的元素个数 + 1),因此对于加法运算,可以这样来处理:

[longobject.c]
static PyLongObject * x_add(PyLongObject *a, PyLongObject *b) {int size_a = len(a), size_b = len(b);PyLongObject *z;int i;int carry = 0; // 进位// 确保a是两个加数中较大的一个if (size_a < size_b) {// 交换两个加数swap(a, b);swap(&size_a, &size_b);}z = _PyLong_New(size_a + 1);  // 申请一个能容纳size_a+1个元素的长整型对象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;                 // 移除低15位, 得到进位}for (; i < size_a; ++i) {                   // 单独处理a中高位数字carry += a->ob_digit[i];z->ob_digit[i] = carry & PyLong_MASK;carry >>= PyLong_SHIFT;}z->ob_digit[i] = carry;return long_normalize(z);                   // 整理元素个数}

这部分的过程就是,先将两个加数中长度较长的作为第一个加数,再为用于保存结果的 z 申请空间,两个加数从数组从低位向高位计算,处理结果的进位,将结果的低 15 位赋值给 z 相应的位置。最后的 long_normalize(z)是一个整理函数,因为我们 z 申请了 a_size + 1 的空间,但不意味着 z 会全部用到,因此这个函数会做一些调整,去掉多余的空间,数组长度调整至正确的数量,若不方便理解,附录将给出更利于理解的python代码。

竖式计算不是按个位十位来计算的吗,为什么这边用整个元素?

竖式计算方法适用与任何进制的数字,我们可以这样来理解,这是一个 32768 (2的15次方) 进制的,那么就可以把数组索引为 0 的元素当做是 “个位”,索引 1 的元素当做是 “十位”。

乘法运算

乘法运算一样可以用竖式的计算方式,两个乘数相乘,存放结果的 z 的元素个数为 size_a + size_b 即可:

 操作  ob_digit[2]ob_digit[1]ob_digit[0]
乘数a   23934543
乘数b*   454632
结果z  15126631176
  10866282522 
结果z 10881409153176

这里需要主意的是,当乘数 b 用索引 i 的元素进行计算时,结果 z 也是从 i 索引开始保存。先创建 z 并初始化为 0,这 z 上做累加操作,加法运算则可以利用前面的 x_add 函数:

// 为方便理解,会与cpython中源码部分稍有不同
static PyLongObject * x_mul(PyLongObject *a, PyLongObject *b)
{int size_a = len(a), size_b = len(b);PyLongObject *z = _PyLong_New(size_a + size_b);memset(z->ob_digit, 0, len(z) * sizeof(int)); // z 的数组清 0for (i = 0; i < size_b; ++i) {int carry = 0;          // 用一个int保存元素之间的乘法结果int f = b->ob_digit[i]; // 当前乘数b的元素// 创建一个临时变量,保存当前元素的计算结果,用于累加PyLongObject *temp = _PyLong_New(size_a + size_b);memset(temp->ob_digit, 0, len(temp) * sizeof(int)); // temp 的数组清 0int pz = i; // 存放到临时变量的低位for (j = 0; j < size_a; ++j) {carry = f * a[j] + carry;temp[pz] = carry & PyLong_MASK;  // 取低15位carry = carry >> PyLong_SHIFT;  // 保留进位pz ++;}if (carry){     //  处理进位carry += temp[pz];temp[pz] = carry & PyLong_MASK;carry = carry >> PyLong_SHIFT;}if (carry){temp[pz] += carry & PyLong_MASK;}temp = long_normalize(temp);z = x_add(z, temp);}return z}

这大致就是乘法的处理过程,竖式乘法的复杂度是n^2,当数字非常大的时候(数组元素个数超过 70 个)时,python会选择性能更好,更高效的 Karatsuba multiplication 乘法运算方式,这种的算法复杂度是 3nlog3≈3n1.585,当然这种计算方法已经不是今天讨论的内容了。有兴趣的小伙伴可以去了解下。

总结

要想支持任意大小的整数运算,首先要找到适合存放整数的方式,本篇介绍了用 int 数组来存放,当然也可以用字符串来存储。找到合适的数据结构后,要重新定义整型的所有运算操作,本篇虽然只介绍了加法和乘法的处理过程,但其实还需要做很多的工作诸如减法,除法,位运算,取模,取余等。

python代码以文本形式存放,因此最后,还需要一个将字符串形式的数字转换成这种整型结构:

[longobject.c]
PyObject * PyLong_FromString(const char *str, char **pend, int base)
{
}

这部分不是本篇的重点,有兴趣的同学可以看看这个转换的过程。

参考

  • longobject.c

附录


# 例子中的表格中,数组元素最多存放3位整数,因此这边设置1000
# 对应的取低位与取高位也就变成对 1000 取模和取余操作
PyLong_SHIFT = 1000
PyLong_MASK = 999# 以15位长度的二进制
# PyLong_SHIFT = 15
# PyLong_MASK = (1 << 15) - 1def long_normalize(num):"""去掉多余的空间,调整数组的到正确的长度eg: [176, 631, 0, 0]  ==>  [176, 631]:param num::return:"""end = len(num)while end >= 1:if num[end - 1] != 0:breakend -= 1num = num[:end]return numdef x_add(a, b):size_a = len(a)size_b = len(b)carry = 0# 确保 a 是两个加数较大的,较大指的是元素的个数if size_a < size_b:size_a, size_b = size_b, size_aa, b = b, az = [0] * (size_a + 1)i = 0while i < size_b:carry += a[i] + b[i]z[i] = carry % PyLong_SHIFTcarry //= PyLong_SHIFTi += 1while i < size_a:carry += a[i]z[i] = carry % PyLong_SHIFTcarry //= PyLong_SHIFTi += 1z[i] = carry# 去掉多余的空间,数组长度调整至正确的数量z = long_normalize(z)return zdef x_mul(a, b):size_a = len(a)size_b = len(b)z = [0] * (size_a + size_b)for i in range(size_b):carry = 0f = b[i]# 创建一个临时变量temp = [0] * (size_a + size_b)pz = ifor j in range(size_a):carry += f * a[j]temp[pz] = carry % PyLong_SHIFTcarry //= PyLong_SHIFTpz += 1if carry:    # 处理进位carry += temp[pz]temp[pz] = carry % PyLong_SHIFTcarry //= PyLong_SHIFTpz += 1if carry:temp[pz] += carry % PyLong_SHIFTtemp = long_normalize(temp)z = x_add(z, temp)   # 累加return za = [543, 934, 23]
b = [632, 454]
print(x_add(a, b))
print(x_mul(a, b))

 

本文由 hongweipeng 创作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/445513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用github中的pull request功能?

* pull request是社会化编程的象征&#xff0c;通过这个功能&#xff0c;你可以参与到别人开发的项目中&#xff0c;并做出自己的贡献。pull request是自己修改源代码后&#xff0c;请求对方仓库采纳的一种行为*–《github入门与实践》 下面具体说一下github中使用pull reque…

「假装努力」

有多少人在「假装努力」&#xff1f; 又有多少人在「真正成长」&#xff1f; 再努力努力 回想起当年毕业后&#xff0c;在北京和室友合租的日子。 那时&#xff0c;我在工作&#xff0c;室友在培训。 一天&#xff0c;我下班回来&#xff0c;听见他在电话里和家人争吵&…

如何阅读论文?

本文主要讲述了如何才能高效的阅读一篇论文&#xff01;&#xff01;

贪吃蛇js

python都学不懂&#xff0c;c又不会&#xff0c;只能写写js来维持生活了。555555 js&#xff1a; window.onload function() {var wrap document.getElementsByClassName("wrap")[0];var uls document.getElementsByClassName("sbody")[0];var hand …

Android studio安装过程中入的坑的记录与记录

Android studio安装过程中入的坑的记录与记录 * 由于最近项目的需求&#xff0c;所以最近一直在配置安卓的开发环境&#xff0c;之前用的是Eclipse ADT的模式开发的&#xff0c;配置环境也花了一些时间&#xff0c;但是由于谷歌大力扶持它的亲儿子Android Studio&#xff0c;…

动态规划基础水题提纲

提纲 汉诺塔 汉诺塔&#xff1a;汉诺塔&#xff08;又称河内塔&#xff09;问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子&#xff0c;在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新…

数据结构课上笔记8

串的概念&#xff1a;串&#xff08;字符串&#xff09;&#xff1a;是由 0 个或多个字符组成的有限序列。 通常记为&#xff1a;s ‘ a1 a2 a3 … ai …an ’ ( n≥0 )。 串的逻辑结构和线性表极为相似。 一些串的类型&#xff1a; 空串&#xff1a;不含任何字符的串&#x…

数据结构课上笔记9

数组&#xff1a;按一定格式排列起来的具有相同类型的数据元素的集合。 二维数组&#xff1a;若一维数组中的数据元素又是一维数组结构&#xff0c;则称为二维数组。 同理&#xff0c;推广到多维数组。若 n -1 维数组中的元素又是一个一维数组结构&#xff0c;则称作 n 维数组…

pySerial -- Python的串口通讯模块

pySerial Overview This module encapsulates the access for the serial port. It provides backends for Python running on Windows, Linux, BSD (possibly any POSIX compliant system), Jython and IronPython (.NET and Mono). The module named “serial” automatica…

串的堆分配实现

今天&#xff0c;线性结构基本就这样了&#xff0c;以后&#xff08;至少是最近&#xff09;就很少写线性基础结构的实现了。 串的类型定义 typedef struct {char *str;int length; }HeapString; 初始化串 InitString(HeapString *S) {S->length0;S->str\0; } 长度 …

Numpy 入门

Numpy 入门 Numpy简介 官网链接&#xff1a;http://www.numpy.org/NumPy是Python语言的一个扩充程序库。支持高级大量的维度数组与矩阵运算&#xff0c;此外也针对数组运算提供大量的数学函数库 Numpy的基本功能 快速高效的多维数组对象ndarray用于对数组执行元素级计算以…

数据结构课上笔记10

树 树的定义&#xff1a;树(Tree)是 n(n≥0)个结点的有限集。若 n0&#xff0c;称为空树&#xff1b;若 n > 0&#xff0c;则它满足如下两个条件&#xff1a; (1) 有且仅有一个特定的称为根 (Root) 的结点&#xff1b; (2) 其余结点可分为 m (m≥0) 个互不相交的有限…

pandasStudyNoteBook

pandas 入门培训 pandas简介 - 官网链接&#xff1a;http://pandas.pydata.org/ - pandas pannel data data analysis - Pandas是python的一个数据分析包 , Pandas最初被作为金融数据分析工具而开发出来&#xff0c;因此&#xff0c;pandas为时间序列分析提供了很好的支持 …

最大搜索子树

给定一个二叉树的头结点&#xff0c;返回最大搜索子树的大小。 我们先定义结点&#xff1a; public static class Node {public int value;public Node left;public Node right;public Node(int data) {this.value data;}} 分析&#xff1a; 直接判断每个节点左边小右边大是…

二叉树最长路径

分析&#xff1a; 暴力求每一段距离也可。 对于以本节点为根的二叉树&#xff0c;最远距离有三种可能&#xff1a; 1&#xff09;最远路径来自左子树 2 &#xff09;最远路径来自右子树&#xff08;图示与左子树同理&#xff09; 3&#xff09;最远路径为左右子树距离根最远…

判断完全二叉树

完全二叉树的定义: 一棵二叉树&#xff0c;除了最后一层之外都是完全填充的&#xff0c;并且最后一层的叶子结点都在左边。 https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232?fraladdin 百度定义 思路&#xff1a;层序遍历二叉树 如果…

判断二叉搜索树

二叉查找树&#xff08;Binary Search Tree&#xff09;&#xff0c;&#xff08;又&#xff1a;二叉搜索树&#xff0c;二叉排序树&#xff09;它或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的左子树不空&#xff0c;则左子树上所有结点的值均小于…

剑指offer_01

文章目录[toc]第一章 面试流程1.1 面试官谈面试1.2 面试3种形式1.3 面试的3个环节第一章 面试流程 1.1 面试官谈面试 初级的程序员谈算法和数据结构&#xff0c;高级的程序员谈项目经验要对公司近况和项目情况了解不要紧张&#xff0c;不要马上上手写代码 1.2 面试3种形式 …

判断平衡二叉树

平衡二叉树&#xff08;Balanced Binary Tree&#xff09;具有以下性质&#xff1a;它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。并且左右两个子树都是一棵平衡二叉树 &#xff08;不是我们平时意义上的必须为搜索树&#xff09; 判断一棵树是否为平衡二叉树&am…

剑指offer_02

文章目录第二章 面试需要的基础知识1.1 面试官谈基础知识1.2 编程语言1.3 数据结构1.4 算法和数据操作第二章 面试需要的基础知识 1.1 面试官谈基础知识 数据结构和算法&#xff0c;编程能力&#xff0c;部分数学能力&#xff0c;问题分析和推理能力编程基础&#xff0c;计算…