深入分析python中整型不会溢出问题


Posted in Python onJune 18, 2018

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

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

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

不溢出的整型的可行性

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

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

[longintrepr.h]
struct _longobject {
PyObject_VAR_HEAD
int *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 23 934 543
加数b + 454 632
结果z 24 389 175

为方便理解,表格展示的是数组中每个元素保存的是 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 23 934 543
乘数b * 454 632
结果z 15 126 631 176
10 866 282 522
结果z 10 881 409 153 176

这里需要主意的是,当乘数 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 的数组清 0
  
  for (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 的数组清 0
    
    int 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)
{
}

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

参考:https://github.com/python/cpython/blob/master/Objects/longobject.c

附录

# 例子中的表格中,数组元素最多存放3位整数,因此这边设置1000
# 对应的取低位与取高位也就变成对 1000 取模和取余操作
PyLong_SHIFT = 1000
PyLong_MASK = 999

# 以15位长度的二进制
# PyLong_SHIFT = 15
# PyLong_MASK = (1 << 15) - 1

def long_normalize(num):
  """
  去掉多余的空间,调整数组的到正确的长度
  eg: [176, 631, 0, 0] ==> [176, 631]
  :param num:
  :return:
  """
  end = len(num)
  while end >= 1:
    if num[end - 1] != 0:
      break
    end -= 1

  num = num[:end]
  return num

def 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_a
    a, b = b, a

  z = [0] * (size_a + 1)
  i = 0
  while i < size_b:
    carry += a[i] + b[i]
    z[i] = carry % PyLong_SHIFT
    carry //= PyLong_SHIFT
    i += 1

  while i < size_a:
    carry += a[i]
    z[i] = carry % PyLong_SHIFT
    carry //= PyLong_SHIFT
    i += 1
  z[i] = carry

  # 去掉多余的空间,数组长度调整至正确的数量
  z = long_normalize(z)

  return z


def 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 = 0
    f = b[i]

    # 创建一个临时变量
    temp = [0] * (size_a + size_b)
    pz = i
    for j in range(size_a):
      carry += f * a[j]
      temp[pz] = carry % PyLong_SHIFT
      carry //= PyLong_SHIFT
      pz += 1

    if carry:  # 处理进位
      carry += temp[pz]
      temp[pz] = carry % PyLong_SHIFT
      carry //= PyLong_SHIFT
      pz += 1

    if carry:
      temp[pz] += carry % PyLong_SHIFT
    temp = long_normalize(temp)
    z = x_add(z, temp)  # 累加

  return z


a = [543, 934, 23]
b = [632, 454]
print(x_add(a, b))
print(x_mul(a, b))
Python 相关文章推荐
栈和队列数据结构的基本概念及其相关的Python实现
Aug 24 Python
分享一下Python数据分析常用的8款工具
Apr 29 Python
TensorFlow实现iris数据集线性回归
Sep 07 Python
使用50行Python代码从零开始实现一个AI平衡小游戏
Nov 21 Python
Python3实现腾讯云OCR识别
Nov 27 Python
python实现矩阵打印
Mar 02 Python
Python编写带选项的命令行程序方法
Aug 13 Python
python不相等的两个字符串的 if 条件判断为True详解
Mar 12 Python
宝塔面板成功部署Django项目流程(图文)
Jun 22 Python
keras 自定义loss层+接受输入实例
Jun 28 Python
python实现企业微信定时发送文本消息的实例代码
Nov 25 Python
pycharm2021激活码使用教程(永久激活亲测可用)
Mar 30 Python
Python登录注册验证功能实现
Jun 18 #Python
详解python3中zipfile模块用法
Jun 18 #Python
python爬取个性签名的方法
Jun 17 #Python
Python爬虫包BeautifulSoup学习实例(五)
Jun 17 #Python
Python爬虫包BeautifulSoup实例(三)
Jun 17 #Python
Python爬虫包BeautifulSoup异常处理(二)
Jun 17 #Python
Python爬虫包BeautifulSoup简介与安装(一)
Jun 17 #Python
You might like
DOTA2 6.87版本后新眼位详解攻略
2020/04/20 DOTA
如何选购合适的收音机
2021/03/01 无线电
一个用php实现的获取URL信息的类
2007/01/02 PHP
PHP 错误之引号中使用变量
2009/05/04 PHP
浅谈php安全性需要注意的几点事项
2014/07/17 PHP
PHP生成不重复随机数的方法汇总
2014/11/19 PHP
laravel框架中视图的基本使用方法分析
2019/11/23 PHP
JavaScript使用cookie
2007/02/02 Javascript
js 字符串操作函数
2009/07/25 Javascript
Javascript alert消息换行的方法
2013/08/07 Javascript
JS将unicode码转中文方法
2017/05/08 Javascript
Javascript调试之console对象——你不知道的一些小技巧
2017/07/10 Javascript
AngualrJs清除定时器遇到的坑
2017/10/13 Javascript
HTML5+JS+JQuery+ECharts实现异步加载问题
2017/12/16 jQuery
JavaScript 自定义html元素鼠标右键菜单功能
2019/12/02 Javascript
基于JS实现视频上传显示进度条
2020/05/12 Javascript
[51:43]OG vs LGD 2018国际邀请赛淘汰赛BO3 第五场 8.26
2018/08/30 DOTA
python模拟表单提交登录图书馆
2018/04/27 Python
Python 内置函数进制转换的用法(十进制转二进制、八进制、十六进制)
2018/04/30 Python
Python selenium实现微博自动登录的示例代码
2018/05/16 Python
Python中Numpy ndarray的使用详解
2019/05/24 Python
django项目用higcharts统计最近七天文章点击量
2019/08/17 Python
Python 异常的捕获、异常的传递与主动抛出异常操作示例
2019/09/23 Python
Python中base64与xml取值结合问题
2019/12/22 Python
python GUI库图形界面开发之PyQt5布局控件QHBoxLayout详细使用方法与实例
2020/03/06 Python
让Django的BooleanField支持字符串形式的输入方式
2020/05/20 Python
使用OpenCV获取图像某点的颜色值,并设置某点的颜色
2020/06/02 Python
谈谈python垃圾回收机制
2020/09/27 Python
CSS3 transition 实现通知消息轮播条
2020/10/14 HTML / CSS
联想瑞士官方网站:Lenovo Switzerland
2017/11/19 全球购物
你常见到的runtime exception
2016/09/05 面试题
货车司机岗位职责
2014/03/18 职场文书
反邪教教育心得体会
2016/01/15 职场文书
毕业生求职自荐信(2016最新版)
2016/01/28 职场文书
OpenCV实现普通阈值
2021/11/17 Java/Android
redis sentinel监控高可用集群实现的配置步骤
2022/04/01 Redis