打开Wolfram之门

第一次接触到 Wolfram 是在 Matrix67 的博客,看到他博客上的精致的演示图片惊呆了,并且很多是动态的。后来从利器这一篇博客中知道博主用的是 Wolfram Language。软件叫做 Mathematica,是一款商业软件,类似 matlab。那时候 Wolfram 就给我留下了一个“很漂亮”的印象。

后来我一直想寻找一种作图的工具,可以用一种语言描述我想表达的数据结构,让我展示在博客上或者 slides 中。Dot 语言貌似是一个不错的选择,它非常简单,表达能力也还可以,基本上花1个小时弄懂它的 AST,然后试一下,就可以谙熟于心了。并且它的实现 Graphviz 还有 Python 版本。这里还有一个 Python 的库 GraphvizAnim 可以基于 Graphviz 制作动态的图片。

然而这个库唯一的缺点就是,它的每一个点的位置是自动布局算法生成的。因为渲染出的图片位置不完美,我曾经一直在网上搜索控制节点位置的方法,后来看到维基百科明确的说明,才放弃。维基百科的一个例子渲染出的结果如下图。

dot 渲染的缺陷

首先无法渲染出正方形,其次 (gof)’ 的位置也有问题。解决这个问题,要么借助其他 svg 编辑工具调整位置,要么就忍受这种自动布局算法的缺陷。Graphviz 提供了不同的工具,它们的不同仅仅是渲染节点的布局算法不同。

  • dot – filter for drawing directed graphs
  • neato – filter for drawing undirected graphs
  • twopi – filter for radial layouts of graphs
  • circo – filter for circular layout of graphs
  • fdp – filter for drawing undirected graphs
  • sfdp – filter for drawing large undirected graphs
  • patchwork – filter for squarified tree maps
  • osage – filter for array-based layouts

最近又想起来 Wolfram 这个好东西,Matrix67 对它的评价如此之高,不去试试可能会失去很多乐趣 :)

Wolfram 是 Stephen Wolfram 耗费了 30 年的心血开发的,目的是建立一种 Knowledge Based Language,比如说每个国家的首都,每个国家的国旗,这种通用的知识。也内置了很多丰富的库函数,一行代码几乎可以做到任何事情。

Naming everything after yourself, huh?

——Youtube网友评论

了解 Wolfram 的强大,可以去 Youtube 看 Stephen 的这个视频。视频中快速展示了几种酷炫的用法,第一次看这个视频的时候让我目瞪口呆,也是这个视频勾起了我学习 Wolfram 的好奇心。有意思的是,Wolfram 是一个基于符号的编程语言,这个最初有点难以理解,比如说你引用了一个不存在的符号,解释器不会报错,而是当做一个定义的符号来对待。图片、公式、地图、地理位置、未知变量 x 等都可以叫做符号。Stephen 的视频中为了解释“面向符号的”语言,说了太多次 Symbolic,所以视频下面有很多欢乐的吐槽。

New drinking game: drink every time Stephen says the word “Symbolic”

 

没有纠结太多,我在官方注册了 15 天 TRIAL。然后按照邮件的指引下载了一个安装工具,但是安装工具一直卡,使用代理下载也不行。尝试了 10 多次都这样,于是就去用户论坛发帖求助。

Wolfram 安装提示 “Feiled to update the catalog”

有人回复说给客服提交工单,客户会邮件直接发给你全部程序下载链接,可以跳过安装器。于是我 9 月 3 日提交了一个工单,9 月 8 日收到了回复,邮件有 Mac 和 Windows 的下载链接,大约 4G 左右。Mac 上面下载完之后按照普通 dmg 安装就可以。

安装好之后,终于可以弹钢琴尝试一下了。

Plan a city tour 和钢琴 Demo

配置目前看起来有些复杂,很多稀奇古怪的问题。比如 Proxy 一直导致网络有问题,直接连接就没问题了。以及 Kernel 不知道为啥最多开启 4 个,只要多余 4 个就会有问题。

快速编程入门》很不错,还有面向 Python 程序员的解释。一个晚上就可以上手。后面打算读一下《Wolfram 全书》。

 

回来更新下,在 Wolfram 的价格页发现除了 Industry 的 license,其他的购买都只有 4 个 Kernel 可以用。我不知道这种纯粹限制用户机能的限制有什么意义。

Number of Mathematica Computation kernels available for parallel computing across an equivalent number of cores. Mathematica Core Extensions can be used to extend parallel support for machines with additional cores.

 

使用uWSGI的spooler做异步任务

最近项目上线,遇到了比较烦的问题,我们无法在线上环境使用 redis。原因貌似是 redis 对集群不太友好,高可用比较难做。所以公司没有现有的可以申请用的 redis 集群(但是有类似的替代品)。

在之前解决分布式定时任务的时候,我引入了 celery,但是很可惜,celery 目前支持的几个 Broker 在我们这里都没有。想了很多方案之后,还是决定不再用 celery + redis 的组合了。寻找一个不依赖外部 Broker 的异步队列。

其实需求主要是两部分:1 需要支持定时任务功能,并且多个节点不能重复执行,这就需要一个全局的 Lock 之类的东西。 2 能够执行一些异步的任务,比如用户发请求,直接返回 Response,表示请求成功,然后再慢慢处理任务。

找了一圈之后,发现 uWSGI 自带的 spooler 功能基本可以满足异步任务的需求。定时任务可以使用 django-cron 。这篇文章分享下 spooler,下一次再分享下 django-cron 这个项目吧。

spooler 解决的主要是这样一种场景:收到用户请求的时候要执行一个耗时比较长的任务,比如发送邮件,通过网络请求更新数据库的一些数据(我们就是这种),而用户可以不必关心任务执行的结果,只要知道任务成功开始执行了就行了。

spooler 的原理

异步任务队列的生产者可以是任何能产生 spool file 的程序,是可以跨语言的。任务用用一个文件夹下面的文件来表示的。指定一个文件夹,一个文件就是一个任务。

后端应用 app 可以往 spool 中放任务(调用 spooler 的 API 生成一个文件),然后uWSGI 启动的时候会将 spawn 出来 spooler 进程,就是 worker,处理这些异步的任务,任务处理成功就将文件删掉。如下图。

 

尝试 spooler 第一步

首先我们新建一个 django 项目来演示 spooler,方便读者阅读。依赖只有 django 和 uwsgi ,通过 pip 安装即可。然后用 django-admin 开启一个新的项目。需要执行的命令如下:

然后我们可以使用 uwsgi 来启动项目了。

可以访问下 localhost:9090 端口看是否启动成功。

将任务放入队列

将任务放入队列我们只要调用 uWSGI 的 spool 函数就可以了。可以接受一个 dict 或者直接是 keyword args。我们在上一步生成的 demo django 项目中写一个向任务队列添加任务的函数如下,直接写在 urls.py 里面了。

代码比较好懂,访问 URL add_task 的时候就会调用 write_task 往队列里面 spool 一个任务。其中要注意的是 spool 的内容在 Python3 中必须是 bytes 的。

我们使用下面的命令执行,执行之前,需要先建立 task 文件夹,我们用这个文件夹来存储任务。

如果你仔细看的话,会发现最后的输出信息如下。

确实有了 spooler 的 worker ,而不加 --spooler 参数的话是没有的。

最后的提示是说没有找到 spooler function ,这是因为我们没有写消费者,所以目前任务会被成功放进去,但是不会被执行。可以试一下,访问我们事先定义好的 localhost:9090/add_task。可以看到每访问一次,task 文件夹就会多一个文件。

其中,spool 函数还有以下特殊的参数,可以满足更多对任务定制的需求。

  • ‘spooler’ => specify the ABSOLUTE path of the spooler that has to manage this task
  • ‘at’ => unix time at which the task must be executed (read: the task will not be run until the ‘at’ time is passed)
  • ‘priority’ => this will be the subdirectory in the spooler directory in which the task will be placed, you can use that trick to give a good-enough prioritization to tasks (for better approach use multiple spoolers)
  • ‘body’ => use this key for objects bigger than 64k, the blob will be appended to the serialzed uwsgi packet and passed back to the spooler function as the ‘body’ argument

编写 spooler 函数(消费者)

spooler 相当于是 celery 的 worker,是真正将任务取出来进行处理的部分,实际就是从 uWSGI 设置的 spooler 文件夹处理每一个文件。如果你 spooler 写的不对,或者文件夹配置不对的话,这个文件夹会越来越大,相当于任务积压没有被处理。

uWSGI 是跨语言的,perl,ruby,python 都可以写 spooler。下面是一个 Python 的 spooler 的例子。

这里要注意的是,返回的值必须是以下 uWSGI 内置的 int 值:

  • -2 (SPOOL_OK) – 任务成功,spool 文件将会被删除
  • -1 (SPOOL_RETRY) – 任务失败,将会被重试
  • 0 (SPOOL_IGNORE) – 忽略任务,在多语言环境可能导致竞争,使用此返回值可以让某些语言的实例跳过此任务

我们可以将这部分代码保存在项目下面的 worker.py 中。由于这段代码在 uWSGI 启动的时候不会被执行,所以启动命令加一个 --import 参数。

启动之后可以看到 tasks 文件夹中的文件逐渐消失了,每5s 少一个,同时 uWSGI 打印出了执行记录。

另外,通过 spooler-process 参数可以控制并发量。比如下面这个命令开启 4 个 spooler 进程。

一些高级的特性

如果单机有多个 uWSGI 的实例,但是只想启动一个干活的,其他的都只负责 spool 任务。那么可以使用 External spool

另外放任务的过程,其实就是 uWSGI 打包好一个任务写到一个文件里面,所以如果我们向网络中其他 uWSGI 实例,通过 socket 写入,也是可以的,这样就可以使用 Networked spoolers

任务权重。在上面的内容中,已经介绍过 spool 函数有一个 priority 参数,可以控制任务的权重。实际上 spooler 在运行的时候,会扫面文件夹,如果扫描到数字,就会优先深度执行数字文件夹里面的内容。但是 uWSGI 执行的时候要加 --spooler-ordered 参数。

比如下面这个 spooler 文件夹的内容:

实际执行的结果会是:

其他还支持一些 Options 参数,可以参考文档

非常重要的 Tips

我是照着 uWSGI 的文档学习的,可以说这个文档很不友好…… 不是按照初学者的路线组织的,纯粹是解释项目组织的,跟一个 wiki 一样,可能是不同的人一直加 feature 然后更新文档导致的吧…… 总之原来的文档最后一段是比较重要的,本文也是。如果你看到一半就关掉这篇文章,那么你惨了……

第一点,如果要在实例之间共享内存,可以使用 uWSGI 的 cache 或者 sharedarea

第二点,也是比较重要的一点:Python 有 uwsgidecorators.py ,Ruby 有 uwsgidsl.rb 。不要直接用本文介绍的低级 API

使用优美的装饰器

如果你使用本文介绍的这些函数的话,可能已经发现,只能写一个延时任务,因为你修改的是全局的 uwsgi.spooler 的值。如果要支持多个任务,就要自己写 dispatcher,像参考资料1中做的那样。

uwsgidecorators.py 里面提供了 3 个很有用的函数。

uwsgidecorators.spool 可以帮你自动分发多个任务,用起来非常像 Celery。还可以自动帮你设置返回值(默认是 uwsgi.SPOOL_OK)。

uwsgidecorators.spoolforever 功能同上,不同的是此装饰器永远返回 uwsgi.SPOOL_RETRY ,也就意味着这个任务会永远被重试,永远被执行。

uwsgidecorators.spoolraw 这个函数需要用户自己写返回值。

有兴趣的也推荐看一下这些装饰器的源代码。可以看到它会帮你处理很多事情,所以千万不要用原始的 API 啊,装饰器就够了。

 

参考资料:

  1. 使用uwsgi实现异步任务  手把手的教程,不错
  2. uWSGI文档
  3. uwsgi_tasks 这个项目对 spooler 不太友好的 API 进行了封装
 

一些命令行工具的增强版

最近在 HN 有一篇《Cli improved》比较火,讲的是一些命令行工具的增强版,我觉得比较好,替换掉了我之前用的一些工具,在这里分享一下。内容基本上是从原文中意译过来的。

首先本文要介绍的工具基本都是原来工具的增强版,也就是说原来工具有的,增强版也都有。因为习惯很难改变,所以完全可以用 alias 替换掉。但是如果某些情况下想用原版的程序的话,可以使用下面的命令:

安装方法我就不说了,Mac 所有的软件都可以通过 brew 来安装,Linux 参考项目主页吧。

bat 替换 cat

cat 做的事情就是把文件内容打印出来,但是没有颜色高亮,很不方便(没有颜色我基本看不懂代码 > <)。ccat (Go语言写的)是有颜色的 cat。但是 bcat 不仅有颜色,还有行号、分页、git 加加减减的整合、类似 less 那样的搜索。下图是我自己的展示,最后两行带 + 的是新增的行,非常酷炫。

建议 alias cat=bat 。

prettyping 替代 ping

这个不用多介绍了,直接看下效果吧。

fzf 替换 Ctrl+R

Ctrl+R 可以在 history 搜索命令,不过用起来很反人类。fzf 使用效果如下,非常方便,从此再也不用畏惧长命令了。

除了查找历史命令,fzf 可以用来模糊查找文件,也很好用,直接设置一个命令,fzf 查找的结果调用 vim 编辑,效率很高。

htop 替换 top

这个应该很多人都知道,htop 提供的信息更明确,熟悉了快捷键效率很高,比如按 P 按照 CPU 排序,t 展示树形,k 来 kill 选中的进程等等。

diff-so-fancy 替换 diff

diff-so-fancy 带有高亮,代码的变更等,配合 git 使用可以让你的 git diff 显示效果和 github 上面的 diff 页面一样。

fd 替换 find

又一个 Rust 写出来的好东西:fd。find 的语法太难记了,fd 好用很多,显示还带高亮。效果如图。

ncdu 替换 du

ncdu 将参数配置好,显示的效果如下。我用的是原作者的的 alias,文件夹是 CPython 的源代码。

Ack 或 ag 替换 grep

这俩我都没用过,介绍一个我用的 rg 吧,主要是速度快。效果如下:

jq

操作 json 的一个命令行工具。再也不用组合复杂的 sed,awk,grep 来处理 json 了,我不确定是不是 jmespath 的语法。教程可以看下官方的,很好学。

类似的 for csv 的有一个叫 csvkit

z

一个根据你的路径历史来 fuzzy 跳转的东西,有一个竞品叫 autojump。不过我习惯了用 z 了,用了很久没有什么痛点。使用效果如图。

fpp

根据前一个命令的输入,自动识别输入中的文件名,然后可以使用快捷键打开。

比如 git status | fpp 的效果如下:

lnav

lnav 是一个日志查看工具,是一个 TUI 工具。

如上图所示,它的好处是处理了折行,对于长行的日志看起来是非常友好的。而且自动高亮了不同的内容,我们看日志的时候就可以方便地忽略每一行相同的部分,快速发现日志中的异常点。相比于 Vim,Vim 打开日志的话会有很多问题,是很危险的操作,因为 Vim 会将整个文件加载到内存,而 lnav 是用了 lseek 不会一下子占用很多内存。

未完待续…… 不定期更新,欢迎补充。

 

LBYL与EAFP漫谈

LBYL 的意思是“Look before you leap.” 指在程序执行之前做好检查。比如下面这段代码:

EAFP 的意思是“Easier to ask for forgiveness than permission.” 在编程方面指的是相信程序会正确执行,如果出错了再处理错误,比如上面这段代码用 EAFP 风格写就是下面这样:

很多情况下,两种方式的写法是可以互相替换的,但是 Python 鼓励 EAFP。原因是这种方法的可读性更高,速度也更快(只有在出错的时候才需要处理,而 LBYL 需要每次运行都检查)。

现在的大多数语言都有异常处理机制了,比如 Java, Python, Ruby 等,没有异常处理机制的都是一些古老的语言,比如 C 语言。但是没有异常处理机制并不代表程序总能运行正确,所以 C 语言需要其他形式来处理程序(函数)不能正确运行的情况,一般的方法是返回 0 表示运行正常,其他值表示运行异常。

但是这种处理形式有两个缺点(这部分可以参考《代码之髓》这本书的第 6 章):第一是可能遗漏错误。比如一个函数在大部分情况下都能正确运行,只有在很少的情况下会出错,或者只有改变了环境之后才会出错。那么很可能程序员会默认这个函数总是正确的,而忘记处理这个异常。如果是 Java 的话,程序员就必须在调用函数的地方处理掉所有可能的异常(虽然很多 Java 程序员喜欢不优雅地用 RuntimeError 处理所有的异常)。更让人头疼的事情是,一旦这“极少数”的情况发生了,那么错误经常不在错误出现的地方,而在很外层的一个调用处。你要花很多时间调试才能找到最终出错的地方。如果有异常处理机制的话,异常栈通常会直接给出 Exception 发生的地方。

第二个缺点是会使代码的可读性下降。因为要检查函数的返回值,要写很多 if-else ,程序真正的逻辑就变得难以阅读。我记得有位高人说过这么一句话,具体是谁说的记不得了,大意是:“高级语言和低级语言的区别是,需要不需要写很多与程序的逻辑无关的东西。” 很多 if-else ,很难看出这个只是判断,还是程序逻辑/业务的判断。如果用 try-catch ,那么 try 代码块里面可以只写程序的逻辑,在 except 里面处理所有的异常。

 

即使在有异常处理机制的语言中,比如 Python,很多人喜欢做的一个事情是在子函数用 if 判断,然后 logger.error + return。其实不如 raise ,然后在调用者那里 try-catch,更能表达逻辑。

此外,Python 语言内置的协议也大量使用了异常的机制。比如《fluent python》(314页)关于重载加运算符 __add__ 就提到,为了遵守鸭子精神,不要测试 other 操作数的类型,而是应该捕获异常,然后抛出 NotImplemented 。

这样的好处是,比如我们定义了一个新的数据类型,支持和 int 相加 a.__add__(4) 。但是内置的 int 可不支持和我们自定义的类型相加,4.__add__(a) 就会抛出异常。这时解释器尝试用 __radd__ 来处理(即 a.__add__(4) )。如果 int 的 __add__ 是实现是相加对象的类型,如果不符合预期就抛出一个 TypeError ,就没有这样的便利了。

 

但是 EAFP 在某些情况下可能是不可行的,它的一个问题就是等错误发生的时候,程序已经运行了一半,如果函数会造成一些副作用,那么这个时候副作用已经发生了。这种情况下,如果没有数据事务这种外部的东西来提供原子性的话,就比较麻烦了,需要手动清理副作用的状态。而 LBFY 这种风格可以尽量保证提供给程序的参数正确,可以顺利运行完成。

其实我想讨论这二者的区别的真正地方是,在生活中也会有这两种风格的处理方式。

团队管理上 try-catch 更自由,更人性化,大家可以关注自己“做事的逻辑”而没有很多条条框框,在程序上 if 可能就多了一次判断,但是在实际生活中的话,这种 if 可能是各种各样的沟通和 ask permission,效率可就不只低这么多了。但是换句话来说,严谨的系统容不得做到半路才出现 “Exception”,还是需要“Look before you leap”的。

 

参考资料:

  1. https://www.codeproject.com/Tips/490765/If-else-instead-of-try-catch
  2. https://www.quora.com/When-should-I-use-try-catch-instead-of-if-else
  3. https://en.wikiquote.org/wiki/Grace_Hopper
  4. https://stackoverflow.com/questions/12265451/ask-forgiveness-not-permission-explain
  5. https://stackoverflow.com/questions/11360858/what-is-the-eafp-principle-in-python
 

Lua 的 pairs 和 ipairs 的区别

最近在用 nginx_lua_module 模块写一个流量转发的东西,根据 Header, Body, Cookie 按照流量比例转发到另一个地方。看了前人写的代码,里面循环的时候有的用 pairs ,有的用 ipairs ,很不解。好在 Lua 官网就有电子版的《Programming in Lua》,学习非常方便。以下内容是我初学 Lua 的笔记和思考,如果不正确,欢迎指正。

一般的迭代器是在内部维护一个状态的(当前迭代的位置),但是 Lua 的迭代器是 Stateless(无状态的),这样的好处是可以重复多次迭代。不像 Python 的 Iterator 和 Iterable,如果多次迭代的话,需要从 Iterable 获得一个迭代器 Iterator。Lua 的迭代器需要循环的时候自己维护。

每一次迭代,for 都会调用迭代器函数,传入的参数有 2 个,一个是无状态的、要迭代的对象,一个就是控制参数(迭代的状态,1 2 3 …)。

比如下面这个循环:

首先 ipairs(a) 执行,返回三个值:iter 函数(从这里看出 Lua 和 Python 一样是有 “一等函数” 的),迭代的对象 a ,和迭代开始的下标 0 。然后第一次 for 循环调用 iter(a, 0) (参数如我们上面所说),得到返回值当前下标 i 和 a[i] 的值 v ,将这两个值赋值给 for 循环定义时候的变量 i 和 v 。用 Lua 实现这个逻辑,如下:

那么上面的 for 循环调用的逻辑类似下面这样,首先调用 ipairs 函数得到 iter 函数,然后每次调用 iter 函数。

另外一个要注意的点是,上面的 Lua 代码判断了 v ,如果不为 nil 才继续。而实际的 for 循环中也是这样的。比如我们下面这个循环,因为第二个值是 nil ,所以打印只会出现第一个元素。

然后我们在来说说 pairs 。其实从上面的描述中也可以发现,ipars 是从 1 开始取值到 nil 截止,那么如果 table 中如果有 nil 但是又想取出所有的元素,就很不方便了。这个时候就可以用 pairs 。

for 循环的逻辑在上面已经说了, pairs 在这里的不同是,它返回的三个元素是 next 函数,迭代的对象 a ,开始的状态 nil 。可以看到不同点主要有两个:第一个是函数 next ,它和 iter 的不同是,它返回的是下一个 key value ,并且顺序固定,直到没有任何 key value 对了,迭代结束。

我们可以通过几个例子看它们的区别。

打印值如下:

两个结果一样,因为在这个 table 中 key 都是 1 2 3 ,所以 pair 用 iter 循环(下标从 1 开始到第一个不是 nil 的值),还是 ipairs 用 next 循环(下标从 nil 开始遍历所有的 key value ),效果都是一样的。

结果是 pairs 可以打印出来结果,ipairs 打印的结果为空。因为 t[1] 的值是 nil,所以 ipairs 循环刚开始就停止了。

再来看最后一组例子(从参考资料1抄来的):

结果如注释中所示,就不必解释了吧。

了解了它们的区别,用起来就非常简单了。ipairs 一般用于需要下标、迭代 array 形式的 table;pairs 可以用来迭代字典形式的 table

 

参考资料:

  1. table 使用手册
  2. Programming in lua
  3. table tutorials