数值修约程序(包括运算过程有效数字保留,Python3实现)
2022.09更新:考虑到需要用到这个程序的朋友很可能没有计算机基础(这也是我只放在csdn而没有放github的原因),我稍微加了一点注释,并且稍微重构了一下以提高可读性。
分割线后是2020原文
今年疫情期间应物理系的朋友要求做了一个做所谓数值和运算修约的程序。
具体包括数值修约(说得简单些就是舍入,参考GB/T 8170-1987)和运算过程的有效数字的保留(就是加减乘除过程中对数值保留位数有要求。具体看我朋友给我提供的这份文件吧:https://shimo.im/docs/PH3hvV6CygW3wK36/)
不说废话了,下面直接贴完整代码吧,Python 3.7和3.8测试过是可以直接跑的。代码使用Python语言实现,所以需要Python解释器。限于篇幅,我在这里就不说怎么安装Python了。
-
如果你也在找数值运算修约程序,为什么不试试呢?我试过,这个东西手算实在是太费脑筋了,还特别容易错。
-
如果你看不懂
class
或者self
是什么,那就直接划到最底下看一下示范的用法,然后copy, paste and run吧。 -
如果本程序使用的修约规则与你所需要的不同,你可以进行更改,毕竟很多基本的逻辑里面都已经实现好了。
等我有时间再考虑要不要把实现思路给写出来吧。
# %%
import math
from warnings import warn
from decimal import *
from typing import Union
# Ver 0.5, Copyleft 2020.6 - 2022.6 Peng Lingbo
# The documentation is written in CHINESE, since the program is based on GB/T 8170-1987
# if you can read Chinese but the following docs doesn't make sense, try to reopen file with UTF-8 encoding.
# v0.3 增加debug功能,可以用于查看追踪过程
# v0.4 支持用任何类型的数字来进行初始化,然而浮点类型本身存在精度问题,所以会收到一个警告要求你使用str()或者Decimal()
# 此外,现在已经支持运算中加入非Number类对象(内置数字),不过出于上面的原因,还是
# 建议用str类型--显式的str()转换或者是直接用'单引号'括起来--或者用Decimal类型.
# v0.5 优化程序逻辑和debug的呈现方式。初始化时可以直接指定精度了。
# 由于使用者所在单位采取的运算修约标准可能与本程序所使用的有轻微不同,建议使用者先用几个测例打开debug=True检查各类运算是否符合期望
# 然后对于不符合的部分做自己的修改再使用。
# 使用本程序,即代表你已经同意程序作者对由于使用本程序所带来的一切可能后果不负任何责任。
class Number:
"""
帮你自动完成数值运算修约!
"""
def __init__(self, value:Union[Decimal, str, int, float], precision:int = None, debug:bool = False):
"""初始化
参数 value: 一个Decimal对象或一个数字的字符串形式,比如:\n
a = Number('3.51E2')\n
b = Number('-8.000') <-注意这里的引号\n
x = Decimal('3.14159')\n
c = Number(x)\n
注意:从程序的角度,直接使用浮点型小数也没问题,但是可能数值会和输入的不一样\n
参数 precision: 整数型,指定精度到 10 ^ precision 位。默认为无,会根据value自动计算\n
参数 debug: 布尔型,指定是否查看追踪过程。默认值 False,即不查看\n
"""
if isinstance(value, Number):
for attr in dir(value):
if attr[:2] != '__':
setattr(self, attr, getattr(value, attr))
if precision is not None and precision != self.effective_digits:
self.round_inplace(precision)
return
if isinstance(value, float):
warn(f"Floating type can cause numerical problems. Use '{value}' or str({value}) to initialize to suppress this warning.")
self.value = Decimal(value)
self.effective_digits = Number._eff(self.value.__str__())
self.debug = debug
self.debugger = lambda x: print('[Debug]', x) if debug else lambda x: x
if self.value != 0:
# 这个数字的最高位的log10值,也就是它最高位的幂数
self.highest_digit = math.floor(math.log10(abs(self.value)))
# 这个数字的最低位的log10值,也就是它最低位的幂数
self.lowest_digit = self.highest_digit - self.effective_digits + 1
else:
# 目前对数字0,默认有效位数为1,最高最低位幂指数为0
self.highest_digit = 0
self.lowest_digit = 0
if precision is not None and precision != self.effective_digits:
self.round_inplace(precision)
else:
self.debugger(f"{self.value} (Effectives={self.effective_digits}, Highest={self.highest_digit}, Lowest={self.lowest_digit})")
def round_inplace(self, precision:int = 1):
"""原地舍入(改变原变量)。\n
参数:precision(需要保留的有效数字数量)\n
"""
self.lowest_digit = self.highest_digit - precision + 1
old_val = self.value
self.value = self.value.quantize(Decimal('1e'+str(self.lowest_digit)), ROUND_HALF_EVEN)
self.effective_digits = precision
self.debugger(f"{old_val} --> {self.value} (rounding to 1e{self.lowest_digit}, effectives={precision})")
def round(self, precision:int = 1):
"""舍入。\n
参数:precision(需要保留的有效数字数量)\n
返回:舍入之后的新Number对象\n
"""
return Number(self.value, precision, self.debug)
@staticmethod
def _eff(num):
"""接受一个数字并返回其有效数字数量\n
参数:数字num(字符串形式)\n
返回:num的有效数字数量
"""
assert isinstance(num, str)
digits = 0
is_leading_zero = True
for d in num:
if d == 'E' or d == 'e':
break
digit = ord(d) - ord('0')
if 0 <= digit <= 9:
if digit > 0 or (digit == 0 and not is_leading_zero):
digits += 1 # 每遇到有效数字: digit++
is_leading_zero = False # 遇到第一个有效数字后所有的0都不是leading zero了,都是有效数字
return max(digits, 1)
def __add__(self, rhs):
"""加法运算符"""
if not isinstance(rhs, Number):
rhs = Number(rhs)
if self.value == 0:
return rhs
if rhs.value == 0:
return self
if rhs.debug:
self.debugger = rhs.debugger
result = self.value + rhs.value
low_digit = max(self.lowest_digit, rhs.lowest_digit)
self.debugger(f"{self} + {rhs} = {result} --> (will round to 1e{low_digit})")
res = result.quantize(Decimal('1e'+str(low_digit)), ROUND_HALF_EVEN)
res = Number(res, debug = self.debug or rhs.debug)
return res
def __sub__(self, rhs):
"""减法运算符"""
if not isinstance(rhs, Number):
rhs = Number(rhs)
if self.value == 0:
rhs.value = -rhs.value
return rhs
if rhs.value == 0:
return self
if rhs.debug:
self.debugger = rhs.debugger
result = self.value - rhs.value
low_digit = max(self.lowest_digit, rhs.lowest_digit)
self.debugger(f"{self} - {rhs} = {result} --> (will round to 1e{low_digit})")
res = result.quantize(Decimal('1e'+str(low_digit)), ROUND_HALF_EVEN)
res = Number(res, debug = self.debug or rhs.debug)
return res
def __mul__(self, rhs):
"""乘法运算符"""
if not isinstance(rhs, Number):
rhs = Number(rhs)
if rhs.debug:
self.debugger = rhs.debugger
result = self.value * rhs.value
left_high = self.value // Decimal(10**self.highest_digit)
right_high = rhs.value // Decimal(10**rhs.highest_digit)
carry = (left_high.log10() + right_high.log10()).__floor__()
precision = min(self.effective_digits, rhs.effective_digits) + carry
self.debugger(
f"{self} * {rhs} = {result} (carry={carry}, result_effectives={precision})")
result = Number(result, precision, debug = self.debug or rhs.debug)
return result #.round(precision)
def __truediv__(self, rhs):
"""除法运算符"""
if not isinstance(rhs, Number):
rhs = Number(rhs)
assert rhs.value != 0, "Division By Zero"
if rhs.debug:
self.debugger = rhs.debugger
result = self.value / rhs.value
precision = min(self.effective_digits, rhs.effective_digits)
self.debugger(
f"{self} / {rhs} = {result} (result_effectives={precision})")
result = Number(result, precision, debug = self.debug or rhs.debug)
return result #.round(precision)
def __floordiv__(self, rhs):
"""整除运算符"""
if not isinstance(rhs, Number):
rhs = Number(rhs)
assert rhs.value != 0, "Division By Zero"
if rhs.debug:
self.debugger = rhs.debugger
result = self.value // rhs.value
precision = min(self.effective_digits, rhs.effective_digits)
self.debugger(f"{self} // {rhs} = {result} (precision={precision})")
result = Number(result, precision, debug = self.debug or rhs.debug)
return result #.round(precision)
def __radd__(self, lhs):
"""反向加法运算符"""
return self + lhs
def __rsub__(self, lhs):
"""反向加法运算符"""
return Number(lhs) - self
def __rmul__(self, lhs):
"""反向乘法运算符"""
return self * lhs
def __rtruediv__(self, lhs):
"""反向除法运算符"""
return Number(lhs) / self
def __rfloordiv__(self, lhs):
"""反向整除运算符"""
return Number(lhs) // self
def sqrt(self):
"""平方根运算"""
res = self.value.sqrt()
return Number(res, self.effective_digits)
def log10(self):
"""以10为底的对数运算"""
res = self.value.log10()
result = res.quantize(Decimal('1e-'+str(self.effective_digits)), ROUND_HALF_EVEN)
return Number(result)
def __str__(self):
"""字符串化"""
return str(self.value)
def __neg__(self):
"""取负运算"""
tmp = self
tmp.value = -tmp.value
return tmp
def __repr__(self):
"""表达形式化"""
return self.value.__repr__()
# %%
if __name__ == "__main__":
a = Number('1', debug=True)
b = Number('1.44')
c = Number('2.25')
p1, p2, p3 = Number("0.3"), Number("0.2"), Number(".5")
print(a*p1+b*p2+c*p3)
# c = 52.9e6 # 浮点类型,会收到警告
# print(a*b*c/1e+9+10.00000) # 浮点类型,会收到警告
pi = Number(math.pi, precision=12) # 浮点类型,会收到警告
print(pi)
pi = pi.round(2)
print(pi)
# %%
写在最后:
她说所有要做大学物理实验的学生都要做这个东西,但是我们在网上怎么找也找不到能够满足要求的代码,大部分实现只实现了数值修约部分——然而如果你细看过程序,就会发现,Python的Decimal
里面的quantize
已经有所需要的舍入方法了(事实上,那里面什么都有!)。由于实在是找不到,而她又确实是有这样的需求,我那时(疫情期间嘛)比较闲,就干脆帮她做了,意外地并不难,第一版也就百来行。经过几次迭代,现在最终版本也只有近200行。
说实话很奇怪,如果有这么大的需求,难道不是应该早就有这样的代码了吗?但是我们俩实在找不到这样的代码。所以虽然可能和其他人撞车,但是我(隔了几个月之后)最终还是决定把它公开,能帮到多少人是多少人。
如果帮到了你,那最好咯。