Aug 31

刚刚竟然梦到了高中英语老师,那个带领我们猜测试题的意图的脾气挺好的女老师。不过这里我要说的是另一位老师,一位脾气很坏的老师,一位造成我即使对很好相处的老师也感到害怕与不友好的老师,一位部分地导致我在很长的时期内与年长的人的交际障碍的老师。

她叫刘静,和中国广播电台经济之声一位主持人重名了。武汉市蔡甸区第三小学的一名教师。1997年至2000年是我所在班级的班主任,教「数学」(我不认可那时的教学方式和教学内容是数学;算术罢了)。当时她年龄大概30来岁。脾气很坏,教课无方。最常见的教学手段有二:一、拿教鞭跳起来敲讲台,敲得讲台上的粉笔头到处乱蹦,粉笔灰弄成雾霾;二、拿教鞭打手心来惩罚学生。当然那个时代,几乎所有老师都体罚学生,有些手段还更讨厌。只是体罚加上她在讲台上的「表演」,让我们在下面的学生都噤若寒蝉、惊恐不已。更重要的是,她的女儿也在这个班级,所以我们经常可以观看到一场场惊心动魄的家祖暴力。哦对了,我当初转到她这班里时没送礼,据说要不是成绩单上的数字比较大她还不会收。所以大家看,成绩好有时候并不是什么好事。

「上帝欲使其灭亡,必先使其疯狂。」不知道她现在灭亡了没有。但愿她没有继续毁人不倦。

刘静是我印象最深刻的一位老师。同时期还有一位叫张攀的男计算机老师给了留下了深刻的印象。我从那时就很喜欢计算机了,当然能力也在同学之上。但是这位张攀老师对我的提问不理不睬,因为他偏爱另一位成绩也很好的萌妹子。不仅如此,他似乎对我怀有敌意。每个孩子都会犯错。当时课上在关机的情况下练习使用鼠标,我觉得颇为无聊,把鼠标当成非电器左右推来推去地玩儿,结果招致他很严厉的一句话。具体是什么我已经忘记了。他平时脾气挺好的,经常和学生有说有笑的。那是唯一一次我见到他生气。

也就是从那时候起,我开始对各种老师敬畏有加,生怕不小心又招惹到某位疯狂或者敌意的大人。所以,尽管后来遇到过不少挺不错的老师,我也只能辜负他们的友善和帮助了。孩童时代教育中受到的伤害,和在爱情中所犯下的错误一样,要花费数十倍的时间才能慢慢弥补。

Aug 29

刚刚看到一则消息,令我很不好受。又拍云,就是我这博客现在贴图和文件下载使用的那个 CDN,也是 CloudFlare 的中国版,依旧在招开发工程师。而两个月前,我在他们那里。他们告诉我当时他们只缺一个人。十数日之后,他们 HR 终于在数倍于承诺的时间之后告诉我,他们不缺人了

福无双至,祸不单行。这种令人很不好受的事情也并不是第一次发生了。大约两年前,我找朋友内推红帽(中国)的工作。结果被拒了,理由是,他们不缺人。大约两三个月之后,那位朋友又告诉我,他们又开始招人了。

更早一些,我毕业那年,搜狐来武汉了。我去面试,结果认为我能力不够。等我确定工作之后,他们却又有人在 Gtalk 上问我要不要去那里。

其实阿里巴巴那次也有类似的情形出现,虽然被拒的主要原因在于我严重休息不好、自由太少导致的状态欠佳。哦对了,没有预料到他们有人自己不懂 TCP 基础就问我网络相关问题也应该是原因之一。当时有人给我推荐工作的时候他们很缺人。但是因为我刚回到老家,很累,还没准备好应聘下一份工作,所以迟了几天。结果那时候他们就不怎么缺人了。

一直想不明白,为什么事情会一直是这个样子,总是错过想要的工作。也许自从走进武汉大学信息学部的那一刻就注定了如此命运吧。也许这种事情再三出现,是因为那次冲动的初恋告白。好像我总是第一次做不好呢。第一次初恋。第一次找工作,没有任何经验,根本不懂得表达自己。第一次选学校,由于信息严重不足导致选择了不适合的学校-专业组合。第一次正式学习编程语言,也是因为资源有限,学了 C++。第一次选择文本编辑器,还是由于信息不足,而选择了私有且不再支持 Linux 的 EditPad Pro。第一次选择命令行 shell,既没有选择大众的 bash,也没有选择功能强大的 zsh,却是挺随意地用上了(当时有不少问题的)fish。第一次自己买电脑,CPU 竟然不支持虚拟化。第一次租房,也是很令人不悦的方寸之地。

当然,人总是会从过去的不满和失意中吸取教训,在反复不断地练习之中臻于熟稔。可是,现在,在这里,这个社会似乎并不认同这一点。我应聘阿里巴巴的时候,他们几乎不问任何具体的技术问题,而是纠结于你之前的工作内容是什么、有没有管理服务器经验(我应聘的是运维开发)、做过的项目能多少并发(你倒是给我找来足够压垮我的项目的用户呀)。也问过我根本没有接触过的东西,比如如何管理上百万的服务器。这其实是很容易学习的事情。他们不承认人的学习能力,总希望别的公司给他们的员工培训,让他们的员工在别处赚取经验。当然,这样规避了风险,使得招进去的人的能力的标准差比较小。没办法,他们招人的人的能力不行(不懂 TCP 就别给我谈网络),所以只能招到能力不是很行的人(连重要的密码都敢往 GitHub 公开项目里扔)。(其实历史已经证明了招个王垠进去也不会出大事的说。)

曾经有人跟我说,我在群里帮忙解答问题的那些人,工资都是我的几倍。很无语。但是也没什么好说的。他们在专业技能上花的时间少,但是在交际、演讲方面花的精力多。招聘通常没办法直接测量应聘者的专业技能,于是只能通过一些表象来推测。就像让你只通过观看来比较两个物体的质量,你只能从体积、色泽、纹理等方面去揣测。然而,这其中没有一项有比较高的证明力度。经验在大多数情况下有效,仅此而已。

我还听我曾经的 boss 评价一个自称对多个方向均有兴趣的应聘者,认为一个人不可能对如此多的方向均有兴趣。言外之意是认为简历有吹嘘之嫌。可是我比那人的兴趣点还要丰富得多啊:Python、C、Linux 系统管理、网页开发、自动化运维、Lua、函数式编程,还有更远一些的,数学、物理、化学、天文、地理、语言学、经济、历史、生物、文学,这些也都是我所喜欢的。另一些人(很正确地)注意到,「喜欢」、「有兴趣」与「擅长」是两码事。于是,「喜欢XYZ」没用。你已经会了什么就继续干什么吧。和第一份工作的公司一样,开始决定结局

我注意到现在很多人喜欢「撞大运编程」。根本不理解程序在干什么。它不工作了,那么就这样试试,再那样试试。有错误消息?太晦涩难懂了,当作不存在好了。很好奇为什么我能很快看出问题来?答案很简单:因为我在尝试理解已有的信息表达了什么。在这里这个对象本应该有的方法却不存在。咦这个对象根本不是预期的对象。哦,这个对象是在初始化那里通过注入的方法弄进来的。咦你声明要注入的对象列表里怎么比初始化的参数列表少了这个出问题的对象?OK,问题解决。

这个事例也很好地说明了我为什么要用 Vim,我为什么不喜欢重复的代码,以及在必要的时候我为什么喜欢复制粘贴已有的相似代码片断而不是重新打一遍。使用 Vim 当然是因为搜索和复制以及对复制之后的代码的处理很方便。除了弱到爆的文件编辑功能(你一直在切换键鼠、一直在找需要的文件所在的那个标签、一直在输入已经输入过的单词和代码行,你没意识到么),另一个 IDE 令我吃惊的是,它们竟然鲜有支持多栏布局的!就是同时显示多个文件,通常是竖直并列,这样方便对照查看。不然用那么大的显示器多浪费啊。

我突然有点想什么时候给 CloudFlare 投简历了。虽然我从没有出过国。本来我想去又拍云就是受到了 CloudFlare 那博客的影响,而他们又一直在博客末尾说在招人。不管怎样,那会是一条非常不确定的旅程。我还没准备好。还是努力让生活慢慢变好吧。现在已经比之前好了很多了呢。虽然与我携手走过生命中剩下的旅程的那个人还没有出现,不过带我来到一个不适合我的环境、并一直阻碍着我追寻自己的人已经远去了。

又,我本来不习惯在这边写自己对非技术的想法的。但似乎只有写在这边才会有人看。那么妥协好了。坚持太累,放弃一些给自己放飞。

Aug 5

这里有人给出了自己的解法,但是我不喜,所以有了本文。注意,本文中的 shell 代码均为 zsh。如果你在用 Windows,那建议还是不要玩了,那个对付这种事情太难用了。

可惜知道这个题晚了两天,没能进前X名 :-(

0. Fuck your brain

机器上没有 brainfuck 编译器,于是 Google「brainfuck online」,得到这个。贴进去运行即可。

1. Multiply

一个值是 42。另一个要观察数列。直接把数列贴到数列百科全书即可。然后乘起来。

2. Keyboard

这个也很容易,不是把 Dvorak 键盘当成 Qwerty,那就是把 Qwerty 当成 Dvorak 了。对照着 Dvorak 的键位输入下边那串字符串,得到一 C 源码。编译、运行之即可。

3. QR Code

扫码,得到:

[abcdefghijklmnopqrstuvwxyz] <=> [pvwdgazxubqfsnrhocitlkeymj]

是个字符映射关系。Python 有现成的函数来处理这个。也是有正反两种可能,都试试就可以了。

>>> T = str.maketrans('pvwdgazxubqfsnrhocitlkeymj', 'abcdefghijklmnopqrstuvwxyz')
>>> s = 'Wxgcg txgcg ui p ixgff, txgcg ui p epm. I gyhgwt mrl lig txg ixgff wrsspnd tr irfkg txui hcrvfgs, nre, hfgpig tcm liunz txg crt13 ra "ixgff" t
r gntgc ngyt fgkgf.'
>>> s.translate(T)
'Where there is a shell, there is a way. I expect you use the shell command to solve this problem, now, please try using the rot13 of "shell" to enter next level.'

按照提示执行命令:

rot13 <<< shell

Arch 上,rot13 命令位于 bsd-games 包。或者在 Vim 里把光标移动到「shell」单词上按g?aw也能得到结果。

题目开始有趣起来了~

4. cat

这个题目更有趣了。源码里一堆乱七八糟的数据。先把它们弄到一个单独的 Vim 缓冲区,然后找到所有的五字符回文字符串:

%!grep -oP '(.)(.)(.)\2\1'

不是所有回文都被接受。仔细观察示例可以发现,中间一定是个小写字母,左边一定是一字母一数字。但是过滤后还是有太多结果。限制左边的字母为大写字母之后可以得结果。删掉不符合条件的,然后把中间的字符连起来即可。

v/\v^([A-Z][0-9]|[0-9][A-Z])\l/d

5. variables

初看,提示莫名其妙。后来注意到图片链接到了有意思的地方。访问得到另一个数字「32722」。显然是要用这个数字放在 URL 上继续访问了。直接拿 shell 访问:

$ n=1024
$ while true; do n=$(curl -sS http://fun.coolshell.cn/n/$n); echo $n; done

访问上百次之后出现一句话,给出了下一关的地址。

6. tree

这关要求从一棵二叉树的中序和后序遍历中还原其最深的路径。不知道怎么做,直接 Google「reconstruct a binary tree from in-order and post-order」,看来有不少人都在做类似的东西啊。我看的是 LeetCode 上的这篇文章。有代码,但我懒得写程序把树画出来或者是找最深的了。反正这树也不大,懂得了方法,直接在 dia 里手工构建出来了。当然,我只构建了最深的那部分。SVG 导出图片

然后就是拿密码解那个字符串了。要注意的是,不要自己去解 base64,不然 openssl 报错的……

7. N Queens

八皇后问题的变种。我直接使用了 Rosetta Code 上的代码。当然要小改一下,直接输出结果而不是打印出图案:

main = mapM_ print $ queens 9

然后找到符合那个 SHA1 值的解就可以了:

$ ./queens | tr -d ',[]' | while read code; do [[ $(sha1sum <<<zWp8LGn01wxJ7$code | awk '{print $1}') == e48d316ed573d3273931e19f9ac9f9e6039a4242 ]] && echo $code; done

8. Excel Column

26 进制转十进制:

>>> def debase26(x):
...   return sum(26 ** i * (ord(d) - ord('A') + 1) for i, d in enumerate(x[::-1]))
...
>>> debase26('COOLSHELL') // base26('SHELL')
85165

结果得到的页面说要转回 26 进制。好吧:(可惜没能在一行内搞定)

>>> def base64(x):
...   L = []
...   while True:
...     x, d = divmod(x, 26)
...     if d == 0: break
...     L.append(d)
...   return ''.join(chr(x + ord('A') - 1) for x in L[::-1])
...
>>> base64(85165)
'DUYO'

9. Fraternal Organisation

这个我没能解出来 QAQ 这两个图片看起来有些莫名其妙。我没注意到图片的名字和鼠标放上去的小提示。最后是看前边那个链接里的答案才知道原来还有个「猪圈密码」-_-|||

PS: 最近博客访问和评论速度都挺慢的,请见谅。

Jul 18

缘起

在一群文件里搜索特定的文本,第一个想到的工具是经典的 grep。自从知道 ag——The silver searcher 之后,我就只用 grep 来过滤管道啦。

ag 的优势:

  • 命令短
  • 和 ack 以及 git grep 一样,默认会忽略掉你通常不想看的文本(二进制文件、被版本控制系统忽略掉的文件)
  • C 编写的,比 ack 更快!

作者在学在乎程序的执行效率。这也很重要,因为文件多啊,几十上百兆的源码找起来可费时了。

不过在我这里,ag 取代掉的既不是 grep 也不是 ack。因为我之前用的是 cgvg,和 ack 一样也是 Perl 写的,但不一样的是,它包含两个命令:一个(cg)用来搜索,另一个(vg)用来在编辑器里打开!

不知道为什么其它工具的作者都没有想到这一点。匹配的地方找到了,大部分情况都需要用编辑器打开看看,也许再改改吧?cgvg 免去了复制路径到编辑器里并跳转到特定位置这一烦琐的步骤。

所以换用 ag 之后,我自然也希望能够很便利地使用编辑器查看匹配的地方了。为此,我做了三个工具。好吧,其实有一个是在 cgvg 时代就已经有了的。

搜索

实践一下。假设我要寻找 Vim 源码中涉及到p_enc变量的地方。使用 agg 命令来搜索并使用翻页器 less 显示结果(截图时没有显示出 less 的存在):

agg result

agg 脚本更改了 ag 的显示样式,每一项开头都有一个序号,就和 cg 命令一样。同样地,agg 也把这个结果保存在主目录下的一个文件,以供 agv 命令使用。

注意,agg 并不能用来按文件名搜索。这种情况还是用 ag -g pattern

使用编辑器打开

agv 命令不给定参数,会显示上一次搜索的结果。如果给出一个编号,就会将结果在AGV_EDITOR环境变量指定的编辑器里打开,并跳转到对应的地方。和 vg 不同的是,agg/agv 为每一个终端维护了一个结果列表,这样就可以在不同的终端里搜索不同的内容而不会相互干扰了。

比如我们要去第 14 号结果所在的地方,只要执行agv 14,然后就到了:

agv result

我这里是在已有的 gVim 里打开的哦。因为我设置的AGV_EDITOR的值是vv $file:$line:$col。这是一句包含占位符的 shell 命令。$file$line$col分别是结果所在的文件、行号、列号。而vv,则是我写的另一个工具,用来在已经打开的 gVim 里打开文件,并且跳转到特定的地方

vv 需要 Python easygui 库,以及 gVim(或者 Vim)的 +clientserver 支持。vv 不仅支持上述格式的参数,作为一名 Pythonista 所编写的工具,它也支持从 Python 报错时打印的 Traceback 的行中提取文件名和行号。比如:

vv 'File "/usr/lib/python3.4/sre_parse.py", line 358, in _parse_sub'

这里,不一定要复制一整行。包含必要的信息(文件名和行号)就可以了。

当然,手动复制粘贴比较累。所以配合我的 zsh 全局别名:

alias -g XS='"$(xsel)"'

我只需要选中那一行,然后执行

vv XS

就可以了。zsh 会帮我把XS展开成我刚刚选择的文本。

ag.vim

ag.vim 是一个 ack.vim 的修改版,用于在 Vim 中调用 ag,就像 ack.vim 在 Vim 中调用 ack、grep.vim 在 Vim 中调用 grep 一样。

因为经常搜索在当前光标下的内容,我写了这么一条自定义命令:

command Agg exe 'Ag -Q ' . expand('<cword>')

仓库地址

https://github.com/lilydjwg/search-and-view

Jul 15

准备

首先检查 CPU 支持。需要 CPU 支持虚拟化的。

grep -E "(vmx|svm|0xc0f)" --color=always /proc/cpuinfo

没输出就没戏了。现在的 CPU 一般都支持的。

然后是内核支持。

zgrep CONFIG_KVM /proc/config.gz
zgrep CONFIG_VIRTIO /proc/config.gz

官方内核是支持的。

最后是用户态软件。Arch Linux 一向不怎么分包,安装 qemu 这个包就可以了。

哦对了,要安装 Arch 的话,还要准备它的安装镜像。

开始啦

一切就绪。

先创建虚拟机所用的磁盘文件。

qemu-img create -f qcow2 ArchVM.img 15G

这样就创建了一个 15G 容量的 qcow2 格式虚拟磁盘文件。之所以选用 qcow2,是因为它支持「母镜像」功能,对应于 Virtual Box 的差分存储。

然后就可以启动系统了。为了避免老是输入一长串命令,遵循 Gentoo Wiki 的建议,我们创建一个脚本:

#!/bin/sh
exec qemu-system-x86_64 -enable-kvm \
       -cpu host \
       -drive file=$HOME/ArchVM.img,if=virtio \
       -netdev user,id=vmnic,hostname=archvm,hostfwd=tcp:127.0.0.1:2222-:22 \
       -device virtio-net,netdev=vmnic \
       -m 256M \
       -curses \
       -name "Arch VM" \
       "$@"

注意到这里我已经加上了hostfwd参数,将虚拟机的 22 端口映射到 host 的 2222 端口上,方便以后通过 ssh 连接。

我这里指定了-curses参数,它将虚拟机的显示器直接使用 curses 库显示在当前终端上。当然能显示的只有显示器处于文本模式的时候,图形模式就只能知晓当前分辨率了。因为我是在服务器上使用,所以加上这个参数。当然你也可以使用 VNC 去连。

然后执行命令:

./startvm -boot once=d -cdrom path_to_file.iso

首先从光驱启动一次(once=d),重启之后恢复到默认的从硬盘启动。

系统启动啦~然后就会发现引导器 isolinux 把显示器切换到图形模式了……

终端无法显示图形模式的内容

不过还好。Arch 的引导界面我们知道。按Tab,然后输入<Space>nomodeset并回车。不过待会进系统里,KMS 之后一直是图形模式就什么也看不到了。

然后进入系统安装啦。注意硬盘设备是/dev/vda。当然也要注意安装并让 sshd 在开机时启动,虽然说有 curses 模式的「显示器」也可以用。

装好之后、重启之前还要注意一点,把/boot/grub/grub.cfg包含gfxload_video之类的地方都去掉,不然会进图形模式的。

装好后就 reboot 吧。如果一切顺利的话就能看到已经安装好的 Arch 登录提示符了。

好不容易装好了系统,当然要把它作为母镜像,所有后续的修改放子镜像上啦:

qemu-img create -f qcow2 -b ArchVM.img ArchTest.img

然后修改一下启动脚本。以后就可以用./startvm脚本启动这个虚拟机啦。

参考文章

Jul 3

muttils 是一系列用于 mutt 等终端邮件客户端的小工具,其中 viewhtmlmsg 脚本用于在浏览器中阅读 HTML 邮件:

macro pager \eh "<pipe-entry>viewhtmlmsg<enter>" 在网页浏览器中查看

但一直以来有个问题:需要等好几秒脚本才会返回,因为它要给浏览器足够的时间来读取 HTML 等文件,然后在退出前清理掉它们。如果是 Linux 系统并且安装了 inotifyx 包会好一些,它检测到有程序读取了 HTML 文件之后就立即退出了。

不过我希望更快一些。到后台去处理,到后台去等待。我要继续阅读下一封邮件。

所以就有了这么个小脚本:读取标准输入传过来的数据之后立即 fork,父进程退出,子进程去把数据交给 viewhtmlmsg 来处理:

#!/usr/bin/env python3

import sys
import os
import subprocess

def main():
  content = sys.stdin.buffer.read()
  if os.fork() > 0:
    return
  p = subprocess.Popen(
    'viewhtmlmsg',
    stdin = subprocess.PIPE,
  )
  p.communicate(content)
  p.wait()

if __name__ == '__main__':
  main()
Jun 17

需要 Python 3.4+,一个参数用来选择测试搜索服务还是 GAE 服务。测试 GAE 服务的话需要先修改开头的两个变量。从标准输入读取 IP 地址或者 IP 段(形如 192.168.0.0/16)列表,每行一个。可用 IP 输出到标准输出。实时测试结果输出到标准错误。50 线程并发。

#!/usr/bin/env python3

import sys
from ipaddress import IPv4Network
import http.client as client
from concurrent.futures import ThreadPoolExecutor
import argparse

# 先按自己的情况修改以下两行
APP_ID = 'your_id_here'
APP_PATH = '/fetch.py'

def check_ip_p(ip, func):
  if func(ip):
    print(ip, flush=True)

def check_for_gae(ip):
  return _check(APP_ID + '.appspot.com', APP_PATH, ip)

def check_for_search(ip):
  return _check('www.google.com', '/', ip)

def _check(host, path, ip):
  try:
    conn = client.HTTPSConnection(ip, timeout=2)
    conn.request('GET', path, headers = {
      'Host': host,
    })
    response = conn.getresponse()
    if response.status < 400:
      print('GOOD:', ip, file=sys.stderr)
    else:
      raise Exception('HTTP Error %s %s' % (
        response.status, response.reason))
    return True
  except KeyboardInterrupt:
    raise
  except Exception as e:
    print('BAD :', ip, e, file=sys.stderr)
    return False

def main():
  parser = argparse.ArgumentParser(description='Check Google IPs')
  parser.add_argument('service', choices=['search', 'gae'],
                      help='service to check')
  args = parser.parse_args()
  func = globals()['check_for_' + args.service]

  with ThreadPoolExecutor(max_workers=50) as executor:
    for l in sys.stdin:
      l = l.strip()
      if '/' in l:
        for ip in IPv4Network(l).hosts():
          executor.submit(check_ip_p, str(ip), func)
      else:
        executor.submit(check_ip_p, l, func)

if __name__ == '__main__':
  main()

脚本下载地址

Jun 3

这是个虽然简单但是很有意思的问题,以前我竟然未曾想过。后来看到「宇宙的心弦」上对这个问题的回答写得太模糊(什么叫「镜子里头脚的位置没变」?「位置没变」的定义是什么?),所以这里写一个尽可能精确描述的回答。首先,我们讨论最容易引起问题的那种情景,即人站立时正面照镜子。

首先定义几个概念。

左右。以你为原点,你的左手方向为左,右手方向为右。(你知道哪只手是左手吧?)

上下。站在地球表面,在空中静止释放一物体,由于重力,它会运动起来。其运动方向为下,反之为上。

前后。这个有点奇特。你面前站了一个人,背心对着你的时候,这是那人的面。面对着你的时候,你看到那人的面。我们的问题隐含了作为观察者的你,去看外界的像,而不是考察你自己,对吧?

让我们再定义一下坐标系。

右为 x 轴正方向,上为 y 轴正方向,由你(观察者)的后背指向你的胸前为 z 轴正方向。

作为观察者,这里有一个很明白的变换:你所认为「正」的像,其坐标要绕 y 轴旋转 180°,才能与你观察时使用的坐标系一致。让我解释得更清楚一些——

拿鼠标指针选中你,按一下Ctrl-D(如果你不是 Inkscape 用户,那就按一下Ctrl-C再按一下Ctrl-V)。现在有了你的一个像。但是你看不到它,因为它和你重合了。让我们把这个像向 z 轴正方向平衡一段距离,比如 2m,你再看看?哟,它怎么背对着你呀?不行,再原地转身 180°,这样才能看到正面不是?

忽略掉平移,让我们把这个变换记作 \(T_1\),有

$$ T_1 = \begin{bmatrix} \cos{\pi} & 0 & \sin{\pi} \\ 0 & 1 & 0 \\ -\sin{\pi} & 0 & \cos{\pi} \end{bmatrix} = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \end{bmatrix} $$

我们再来考察一下镜子做了什么。

想像镜子前有一个坐标系的三根轴,就是我们刚刚定义的那个。其中 z 轴正方向指向镜面。于是乎,镜子里的 x 轴与外边的 x 轴是平行且方向一致的。y 轴也是这样。但是 z 轴的位置没有改变,方向却反了过来,箭头对箭头了。所以,这种放置法,使得像与物体的 z 轴反了,\(z\) 变成了 \(-z\)。还是忽略掉平移,让我们把这个变换记作 \(T_2\)

$$ T_2 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \\ \end{bmatrix} $$

所以,最终作为观察者的你,看到的镜中的自己经历的变换是:

$$ T_1 T_2 = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & -1 \\ \end{bmatrix} = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

即最终的像的坐标中的 \(x\) 变成了 \(-x\),即左右颠倒。

让我们再考虑另一种情况试试。如果把镜子放在头顶上,看过去会是什么感觉呢?

这时候,y 轴一头扎进了镜子,于是,我们的第三个变换 \(T_3\) 为:

$$ T_3 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

乘一下,结果是:

$$ T_1 T_3 = \begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & -1 \\ \end{bmatrix} $$

咦?这不是上下、左右、前后都颠倒了吗?找面位于天花板的镜子试试看。上下的确颠倒了不是么?再伸出左手试试,左右也和在面前的镜子里一样,也是颠倒的。可,前后感觉并没有颠倒啊?这是因为观察者和被观察对象位于同一垂直线上,感觉不一样了。仔细想想,天花板镜子里的像的前后确实与面前的镜子晨那个的前后是对着的,不是么?而我们定义后者没有颠倒,那么前者当然相对于后者是颠倒过了嘛。

个人拙见 =w=

Jun 1

听说 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.cparsenumber函数。最终的补丁如下:

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);

因为只是玩玩,所以不太认真,没仔细做错误处理;因为decimalfractions模块是从外部文件导入的,所以可能被覆盖掉,从而导致报错,并且这错误是无法通过异常处理捕获的。

不出问题的话,再次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=

May 27

这东西挺好用的,可惜我只寻到一多年以前的 Python 2 版本的,作者是 AutumnCat,不认识。但注释里提到的修改者 bones7456 是鼎鼎大名的骨头兄,现其博客已经长草……

一直以来,我都是通过子进程调用来使用的,因为我写的代码是 Python 3 版,比如这个寻找文本里的 IP 地址并标记的 ipmarkup 脚本。配合 Python 3.2 加入的 functools.lru_cache,效率还不错的样子。但近期有大量 IP 需要查询,才感到每个 IP 都开个子进程的方式实在太慢。遂将其修改为 Python 3 版,并加入了些 Python 后来才流行的 idiom。

脚本还是扔到 winterpy 仓库里了。GPLv2 授权的。

2014年8月2日更新:增加了在线更新的功能,从此不需要 Wine 就能更新数据库啦 :-) 更新方法来自微菜。更新命令如下:

python3 -m QQWry update