本文来自依云's Blog,转载请注明。
听说 Ruby 支持分数字面量呢——
irb(main):001:0> 1/3r => (1/3) irb(main):002:0> 1/3r+1/2r => (5/6)
可是在 Python 里只能这样子:
>>> from fractions import Fraction as R >>> R(1, 3) Fraction(1, 3) >>> R(1, 3) + R(1, 2) Fraction(5, 6)
学习《用 `accio` 代替 `import`:深入理解自举和 Python 语法》改改 Python 解释器,让它也支持分数字面量,应该会很有趣呢 =w=
去翻了一下 Grammar/Grammar
文件,有些失望。那里只有语法定义,没有词法定义呢。以为词法定义在另一个地方,所以找了找,无果。只有 C 代码。想到复数的 j
字尾,直接拿 ag 搜带引号的 "j"
和 'j'
,最终确认它在 Parser/tokenizer.c
里。也就是说,Python 的词法分析是自己实现的。
在那个四千多行的tok_get
函数里,有一部分代码在尝试解析一个数,也就是语法里的 NUMBER
。照着解析复数的办法,把 d
后缀和 r
后缀也加进去:
diff -r bf65e7db066d Parser/tokenizer.c --- a/Parser/tokenizer.c Mon Apr 14 22:27:27 2014 -0400 +++ b/Parser/tokenizer.c Fri May 30 20:12:07 2014 +0800 @@ -1528,6 +1528,10 @@ goto fraction; if (c == 'j' || c == 'J') goto imaginary; + if (c == 'd' || c == 'D') + goto decimal; + if (c == 'r' || c == 'R') + goto rational; if (c == 'x' || c == 'X') { /* Hex */ @@ -1621,6 +1625,12 @@ /* Imaginary part */ imaginary: c = tok_nextc(tok); + else if (c == 'd' || c == 'D') + decimal: + c = tok_nextc(tok); + else if (c == 'r' || c == 'R') + rational: + c = tok_nextc(tok); } } tok_backup(tok, c);
d
后缀是我给十进制数——就是会计里会用到的精确的十进制小数——准备的。
然后可以编译出来试试。这个 configure 命令是从 Arch 官方编译脚本里找的。
./configure --enable-shared --with-threads --with-computed-gotos --enable-ipv6 --with-valgrind --with-system-expat --with-dbmliborder=gdbm:ndbm --with-system-ffi --with-system-libmpdec --without-ensurepip make
因为我不执行安装步骤,而又用了共享库,所以要这样子执行:
LD_LIBRARY_PATH=. ./python
试试看:
>>> 4d ValueError: could not convert string to float: 4d
有效果,不报语法错了呢。
现在报ValueError
,因为我还没告诉 Python 如何解析我新加的字面量表示呢。解析代码位于Python/ast.c
的parsenumber
函数。最终的补丁如下:
diff -r bf65e7db066d Python/ast.c --- a/Python/ast.c Mon Apr 14 22:27:27 2014 -0400 +++ b/Python/ast.c Fri May 30 20:12:07 2014 +0800 @@ -3650,12 +3650,29 @@ long x; double dx; Py_complex compl; - int imflag; + char typeflag; + PyObject *mod, *type, *ret; assert(s != NULL); errno = 0; end = s + strlen(s) - 1; - imflag = *end == 'j' || *end == 'J'; + switch(*end){ + case 'j': + case 'J': + typeflag = 'j'; + break; + case 'd': + case 'D': + typeflag = 'd'; + break; + case 'r': + case 'R': + typeflag = 'r'; + break; + default: + typeflag = 'i'; + } + if (s[0] == '0') { x = (long) PyOS_strtoul(s, (char **)&end, 0); if (x < 0 && errno == 0) { @@ -3670,13 +3687,43 @@ return PyLong_FromLong(x); } /* XXX Huge floats may silently fail */ - if (imflag) { + if (typeflag == 'j') { compl.real = 0.; compl.imag = PyOS_string_to_double(s, (char **)&end, NULL); if (compl.imag == -1.0 && PyErr_Occurred()) return NULL; return PyComplex_FromCComplex(compl); } + else if (typeflag == 'd') { + mod = PyImport_ImportModule("decimal"); + if (mod == NULL) + return NULL; + + type = PyObject_GetAttrString(mod, "Decimal"); + if (type == NULL) { + Py_DECREF(mod); + return NULL; + } + ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1); + Py_DECREF(type); + Py_DECREF(mod); + return ret; + } + else if (typeflag == 'r') { + mod = PyImport_ImportModule("fractions"); + if (mod == NULL) + return NULL; + + type = PyObject_GetAttrString(mod, "Fraction"); + if (type == NULL) { + Py_DECREF(mod); + return NULL; + } + ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1); + Py_DECREF(type); + Py_DECREF(mod); + return ret; + } else { dx = PyOS_string_to_double(s, NULL, NULL);
因为只是玩玩,所以不太认真,没仔细做错误处理;因为decimal
和fractions
模块是从外部文件导入的,所以可能被覆盖掉,从而导致报错,并且这错误是无法通过异常处理捕获的。
不出问题的话,再次make
之后,就可以开始玩了。不过在此之前,再多做几个补丁,让 Python 把分数和十进制数显示得简洁好看一点:
diff -r bf65e7db066d Lib/decimal.py --- a/Lib/decimal.py Mon Apr 14 22:27:27 2014 -0400 +++ b/Lib/decimal.py Fri May 30 20:12:07 2014 +0800 @@ -1015,7 +1015,7 @@ def __repr__(self): """Represents the number as an instance of Decimal.""" # Invariant: eval(repr(d)) == d - return "Decimal('%s')" % str(self) + return str(self) + 'd' def __str__(self, eng=False, context=None): """Return string representation of the number in scientific notation. diff -r bf65e7db066d Lib/fractions.py --- a/Lib/fractions.py Mon Apr 14 22:27:27 2014 -0400 +++ b/Lib/fractions.py Fri May 30 20:12:07 2014 +0800 @@ -280,7 +280,7 @@ def __repr__(self): """repr(self)""" - return ('Fraction(%s, %s)' % (self._numerator, self._denominator)) + return str(self) + 'r' def __str__(self): """str(self)""" diff -r bf65e7db066d Modules/_decimal/_decimal.c --- a/Modules/_decimal/_decimal.c Mon Apr 14 22:27:27 2014 -0400 +++ b/Modules/_decimal/_decimal.c Fri May 30 20:12:07 2014 +0800 @@ -3092,18 +3092,10 @@ static PyObject * dec_repr(PyObject *dec) { - PyObject *res, *context; - char *cp; - - CURRENT_CONTEXT(context); - cp = mpd_to_sci(MPD(dec), CtxCaps(context)); - if (cp == NULL) { - PyErr_NoMemory(); - return NULL; - } - - res = PyUnicode_FromFormat("Decimal('%s')", cp); - mpd_free(cp); + PyObject *res, *str; + str = dec_str(dec); + res = PyUnicode_FromFormat("%Ud", str); + Py_DECREF(str); return res; }
下面是最终成果啦:
>>> 0.1 + 0.2 == 0.3 False >>> 0.1d + 0.2d == 0.3d True >>> 1/3r + 1/2r 5/6r >>> 0.4/1.2r 0.33333333333333337 >>> 0.4r/1.2r 1/3r
可以看到,与复数类似,分数字面量其实包含了一次除法。所以如果分子写浮点数的话,最终结果是会被转成浮点数的呢。这个和 Ruby 的行为是一样的 =w=
Jun 02, 2014 05:51:03 PM
gem多了之後pry好慢……strace -fe file看到它stat很多文件……但又不想用irb
Jun 02, 2014 06:25:13 PM
就是在输入之后加了高亮而已?连 bpython 那样的实时高亮都没有呢。
Jun 04, 2014 06:27:23 PM
">>> 0.1 + 0.2 == 0.3
False"
● perl -e 'print 0.1+0.2'
0.3%
#还是perl比较符合人类的习惯。
Jun 04, 2014 09:18:52 PM
可是 Perl 的 0.1 + 0.2 == 0.3 也是假呀:
>>> perl -e 'print "false\n" if not 0.1 + 0.2 == 0.3;'
false
反而是为了得到相似的结果我要多写好多字符。(Python 只需要 python3 -c 'print(0.1 + 0.2 == 0.3)' 就可以了)
Jun 06, 2014 07:20:07 PM
我没有说perl里0.1+0.2==0.3,
我说的perl的输出比较符合人类的习惯。
● perl -e 'print ((0.1+0.2).q// == 0.3)'
1%
● perl -e 'print (0.1 + 0.2 eq 0.3)'
1%
● perl -e 'print 0.1+0.2==0.30000000000000004'
1%
关于输出True/False的问题,是因为perl(5)本身没有bool这个类型。
刚看了一下,python在转换成字串的时候,输出的也是符合人类习惯的精确(?)数值
而perl6做(kan)的(qi)是(lai)真的准确计算
● perl6 -e 'say 0.1+0.2'
0.3
● perl6 -e 'say 0.1+0.2==0.3'
True
● perl6 -e 'say ((0.1+0.2).perl)'
0.3
Jun 06, 2014 10:14:38 PM
哇,你竟然有 Perl 6 的解释器!我曾经试过那个 Haskell 版的,记得当时没编译成功呢喵。
Jun 19, 2014 08:56:43 PM
对 Function 进行 Monkey Patch,修改一下 __repl__ 大概就可以达到相同效果了吧。
修改 Python 解释器是纯粹为了学习?
Jun 19, 2014 08:57:37 PM
s/repl/rper/
Dvorak 上 r、l 正好挨在一起,而且 REPL 也是个词,第一次没看出来错……
Jun 19, 2014 08:58:33 PM
s/rper/repr/
啊啊啊啊啊啊!评论应该允许 5 分钟内编辑啊!!!
Jun 20, 2014 11:57:54 AM
就是为了玩儿呀~
修改 __repr__ 容易,可是 Python 词法没办法的呀。
Jun 20, 2014 11:59:03 AM
去给 galeki 提需求吧。或者等我什么时候给自己写个博客系统出来?