计算缓存、优化算法和加速 Python 执行 第三部分

用列表计算进行优化

Python 性能的确一般,但是 Python 的常规函数的性能还是不错的, Python 解释器是用 C 语言实现的,在基本操作层面,已经优化了很多年,所以虽然有 requests 这样石破天惊的扩展包来取代 Python 内部的函数包,但是基本上没有人会去自己实现 Python 的列表计算等最常规的功能。在我们实践中的大部分场景,包括以 server 服务为主的场景,Python 的性能、稳定性都非常在线,也没有 Java 的 JVM 带来的很多不可控制的问题。我们用程序是去解决更多的业务逻辑,所以易学、易维护这些特性也是要关注的。

我们来看看用列表操作实现斐波那契数列的版本:

def list_fib(n):

    list_f = []
    f = 1
    list_f.append(f)
    list_f.append(f)  
    for i in range(n-2):
        f = list_f[-1] + list_f[-2]  
        list_f.append(f)
    return f


if __name__ == "__main__":
    x = 47
    start = time.time()
    res = list_fib(x)
    elapsed = time.time() - start
    print("Python Computed fib(%s)=%s in %0.8f seconds" % (x, res, elapsed))

执行结果就是秒开:

Python Computed fib(47)=2971215073 in 0.00002408 seconds

基本是所有执行方法中最快的之一,因为执行几十次的列表运算对于 Python 来说实在很轻松。

Python 的列表运算我们在《Python 机器学习》一书中有专门章节讨论过,列表是 Python 中非常重要的数据类型,不能小看,像这样一个数学运算的场景,用列表也可以简单而高效的完成。

谈谈 PyPy

CPython,也就是我们平时使用的 Python 解释器,因为这个解释器是用 C 写的,所以我们称之为 CPython,它并不是 Python 的唯一发行版,比如还有 PyPy http://pypy.org ,它通过即时编译器(JIT)来加快代码执行速度。在之前斐波那契数列的计算例子中有过比较,要比使用 CPython 快 5 倍左右。PyPy 完全支持 Python 标准库,但它不是支持所有的第三方扩。其平均可以提升 2-5 倍性能,并且你的应用所使用的扩展包都兼容,那么 PyPy 是可以尝试一下的。

维基百科中文版 https://zh.wikipedia.org/wiki/PyPy 关于 PyPy 的资料比较老,可以参考英文版 https://en.wikipedia.org/wiki/PyPy

PyPy 是 Armin Rigo 开发的,一己之力,能做这么多,真的很厉害!

PyPy 的前端是个严格的 Python 子集,称之为 RPython,这里的 R 是严格(Restricted)的意思。RPython 对 Python 语言做了一些约束,以便在之后编译时可以推断出变量类型等诸多优化的前提。PyPy 项目开发了一个工具链,用于分析 RPython 代码并将其转换为字节代码形式,最后用 C 语言来编译,达到将大部分代码编译成机器代码以提高执行速度的目的。它有设计精良的垃圾收集器和内存管理。最后,它包含了 JIT 引擎,在解释器层面上构造“即时的优化代码”。

概念很多,我们一一道来。

什么是 RPython

RPython 有自己的文档站点:https://rpython.readthedocs.io/en/latest/

你写的 Python 程序首先会经过一个 PyPy 的解释器来生成 RPython 代码,前面说了,生成的 RPython 代码比较严格,这样后面的编译工作就好开展,真正的 CPython 是用 C 写的解释器,但是并没有将你的 Python 代码编译成机器码,所以 Python 一般情况下会被诟病执行速度慢。

所以说 PyPy 实际上是包含两个组件:

  1. Python 解释器,输入是常规的 Python 程序,输出 RPython 代码,且这个 Python 解释器本身使用 RPython 写的。
  2. RPython 编译工具链,包括将 Python 代码转换成字节码和 C 代码,编译成本地二进制代码的一系列工具和步骤。

所以大家理解为什么说 PyPy 对 Python 兼容性很好,但是安装一些复杂的本身通过 C 优化过的扩展包兼容性又不太好的原因了。

RPython 编译过程

在官方文档中,PyPy 的整个工作不叫“编译”,而是叫“翻译”(translate)。RPython 编译器是一组工具链,将 RPython 程序编译到目标语言,比如 C。这个编译器本身是用 Python 写的。流程如下:

编译器读入转换到 RPython 样式的代码。RPython 会对普通的、带有一些动态性能的 Python 代码做一些限制,比如函数不能动态被生成、变量不能够不确定到底存储什么类型等,一些在解释执行时候没有问题的细节,在这里都会被严格对待。

编译器通过一个称之为抽象解释器(abstract interpretation)的工具构建 RPython程序的流程图。这个抽象解释器使用 PyPy 的 Python 解释器来解析 RPython 程序,这里开始的流程非常复杂,涉及到很多编译相关的技术细节,在 RPython 官方文档这里有一个更加详细的流程图:

笔者才疏学浅,就不再逐一展开了,有兴趣的朋友可以参考这些资料:

https://rpython.readthedocs.io/en/latest/translation.html

https://www.aosabook.org/en/pypy.html

有更加完整的说明和解释。

编译器一步步通过生成的流程图,最后生成基于 C 语言的代码,当然,在生成 C 代码的时候还要加入很多异常处理、内存管理等。通过下面这一连串的动作,终于可以将 Python 代码转换到 C 代码了。

什么是 JIT

什么是即时编译 JIT,Wiki 的定义(https://zh.wikipedia.org/wiki/即时编译) 很清晰:即时编译是动态编译的一种形式,是一种提高程序运行效率的方法。通常,程序有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,而动态解释执行则是一句一句边运行边翻译。

即时编译器则混合了这二者,一句一句编译源代码,但是会将编译过的代码缓存起来以提高性能。相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。

即时编译器有两种类型,一是字节码翻译,二是动态编译翻译。微软的.NET Framework,还有绝大多数的 Java 实现,都依赖即时编译以提供高速的代码执行。Ruby 的第三方实现 Rubinius 和 Python 的第三方实现 PyPy 也都通过 JIT 来明显改善了解释器的性能。

PyPy 的 JIT 机制比较与众不同的地方在于,别的编译器直接执行程序代码,其编译器中支持 JIT 特性,而 PyPy 则是在将 Python 代码转换为 RPython 代码的时候加入的,然后将其编译成可执行代码后就自带 JIT 了。这样的方式也算是比较巧妙的设计了,我理解,PyPy 对性能提升很大程度就在这里了。只是,通过这么复杂的两次转换,Python 代码的性能虽然有了几倍的提升,但和 Java、C 等还是有不小的差距。而随着硬件服务器资源的摩尔定律,引入 PyPy 本身增加的项目复杂度以及其对于第三方扩展包的支持问题,似乎还是使用 Python 标准的 CPython 解释器比较好,综合使用成本低很多。PyPy 解决问题的思路非常不错,但还是有很长的路要走。

fishbase v1.1.6 发布

昨天,我的同事们发布了 fishbase v1.1.6。可能是一种习惯,将项目开发中的所有公用函数都封装起来,Python 的包是一种非常好用的格式,加上有 github 和 CI 工具,都可以提升开发这些公用函数的质量。同时对于我们整理思路、学习 Python 知识、单元测试、文档编写、项目管理等都有不少提升。写一个可以放在 github 上的项目和自己在项目组里折腾,压力还是略有不同的。

现在 fishbase 还是比较简单,坚持,用心,就会越来越好。

自 2016/3 初次发布以来,我们坚持不断更新,先后发布了 20 余个版本。近一年来,我们逐步形成每月更新 1 到 2 个版本的频率,抽象出了很多通用的方法,主要分为以下模块:

模块功能函数
fish_common基本函数包
fish_crypt加密数据函数包
fish_csvcsv 处理增强函数包
fish_data数据信息处理函数包,含银行卡、身份证等
fish_date日期处理增强函数包
fish_file文件处理增强函数包
fish_logger日志记录增强函数包
fish_projectproject 目录结构生成函数包
fish_random随机数据生成函数包
fish_system系统增强函数包

可以到通过以下单元中查找具体的函数列表和使用说明。


fishbase 扩展包 for python:
源代码:https://github.com/chinapnr/fishbase
文档:https://fishbase.readthedocs.io/en/latest/
pypi :https://pypi.org/project/fishbase/ 

计算缓存、优化算法和加速 Python 执行 第二部分

引入 Nim 语言

我们可以将计算密集型的函数移动到 C 扩展模块,来弥补 Python 这方面作为解释型脚本语言的不足,但是用 C 编写 Python 扩展模块并不容易,大多数人都选择使用Python,就是因为它不是 C。虽然为了性能提升引入一种新技术(语言)的代价其实有点大,但是如果这个语言和 Python 很接近,但是性能几乎和 C 语言一样,并且可以很方便的为 Python 提供扩展包,这样的话,这个引入还是值得一试的。

Nim 语言似乎还是比较小众,在它的官网网站,https://nim-lang.org/ 是这样介绍自己的:

Nim是一种系统和应用程序编程语言,支持 静态类型和编译,它通过优雅的方式提供无与伦比的性能。

Nim 具有以下特性:

  • 高性能,自动垃圾回收
  • 可以编译成 C、C++ 或者 JavaScript 语言
  • 生成无依赖的二进制代码
  • 可以运行在 Windows、macOS 和 Linux 等

是不是听上去很神奇,可以编译成二进制代码不算稀奇,可以编译成 JavaScript 还是很不错的,这样就可以全栈通吃了。(这一点很像 Kotlin 语言)我猜测 Nim 的工作原理可能也是将自己编译成 C 语言,然后再将 C 语言编译成二进制代码,同样,也可以将自己转换为 C++ 或者 JavaScript 的等价语言,当然通过 lex 和 yacc 的分析加上应该非常复杂的模板和优化技术。

Nim 语言的安装

Nim 的安装看上去有点繁琐,可以参考 Nim 的官方文档:https://nim-lang.org/install_unix.html

在 macOS 上,最容易的安装方法是 brew install nim

然后推荐几乎万能的 Visual Studio Code ,免费好用。可以到这里下载:https://code.visualstudio.com/。微软这几年还是做出了很多非常优秀的产品,且有开放的心态,才会有这样一个功能超强、支持很多种开发语言,并且还是开源免费的优秀开发工具。

打开 Visual Studio Code 后,安装 Nim 扩展,以及 Code Runner 扩展。

Nim 扩展使得 VS Code 具备支持 Nim 语法高亮。

而 Code Runner 扩展则功能强大,支持在 VS Code 中运行几乎所有主流语言。

按照 Nim 官方介绍,你可以运行一段最简单的 Hello World 程序来测试一下。输入 echo "Hello World!",保存为 helloworld.nim,然后点击运行按钮,就会看到 Hello World!,如果没有出来正确结果的话,可以参考 Nim 官方文档。

我们可以运行一个复杂的,其实也就是刚才那个斐波那契数列的 Nim 程序版本:

import math, strformat, times

proc fib(n: int): int =
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

when isMainModule:
    let x = 47
    let start = epochTime()
    let res = fib(x)
    let elapsed = (epochtime() - start).round(2)
    stderr.writeLine(&"Nim Computed fib({x})={res} in {elapsed} seconds")

用 Nim 制作 Python 扩展包

好,重点来了,我们通过 Nim 的扩展程序安装一个 nimpy 的包,就可以将 nim 编译后的程序作为 C 的二进制程序给 Python 程序使用了。

安装 nimpy 包:nimble install nimpy

下面 Nim 程序实现了算法:

import nimpy

proc fib(n: int): int {.exportpy.} =
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

假设这个程序存盘文件名为 fib_nimpy.nim

然后我们执行 nim c -d:release --app:lib --gc:regions --out:fib_nimpy.so fib_nimpy.nim ,

稍等片刻,看到一堆提示信息和 operation successful (29124 lines compiled; 1.862 sec total; 47.984MiB peakmem; Release Build) [SuccessX] ,编译已经成功,我们会看到一个 fib_nimpy.so 文件。so 文件是 unix 的动态连接库,是二进制文件,作用相当于windows下的 .dll 文件。我没有测试,估计在 Windows 环境下应该可以生成 dll 文件。

然后将 so 文件复制到和 Python 程序同样的路径,import 刚才生成的 so 文件。

import time
from fib_nimpy import fib

if __name__ == "__main__":
    x = 47
    start = time.time()
    res = fib(x)
    elapsed = time.time() - start
    print("Py3+Nim Computed fib(%s)=%s in %0.8f seconds" % (x, res, elapsed))

我们可以看到 from fib_nimpy 就是刚才的 so 文件,其中的 fib 是我们在前面 Nim 语言中具体定义的函数。

执行上述代码,也就是没有任何优化的直接用递归硬算,性能是 Python 的 1000 倍,因为最慢的计算代码已经是和 C 语言同样速度的代码了。

沿袭这个思路,我们可以将所有消耗 CPU 资源的函数,特别是涉及到复杂计算的函数,都可以用 Nim 实现函数功能,然后被 import 到主 Python 程序来获得性能的大大提升。从前面的例子可以看到 Nim 语言可比 C 要简单好学得多。不过 Nim 语言还比较小众,甚至注定一直比较小众,没问题,聪明勤奋的你,为了性能优化是值得去学习的,再说 Nim 语言的优势还远远不止这点。

计算缓存、优化算法和加速 Python 执行 第一部分

从斐波那契数列谈起

这里 https://robert-mcdermott.gitlab.io/posts/speeding-up-python-with-nim/讨论通过一种称之为 Nim 的技术框架来进行 Python 的加速(后面会对 Nim 技术详细介绍)。文章从计算斐波那契数列开始举例,用递归方式来计算,并且可以看到同样的计算方式,Python 和其他语言的速度上有不小的差异。

上述文章中提到,的确 Python 是一种优秀的编程语言,针对程序员的工作效率进行了优化;令人惊讶的是,你可以非常快速实现的从创意到最低可工作的一个解决方案。它通过其非常灵活的特性和易于编写和阅读的语法,大大缩短了代码开发时间。某种程度上,我们一直说 Python 的实现方式非常接近大脑的思考方式。(从机器学习的发展史来看,大脑的运转速度,比如计算加法的能力远远比不上现在的电脑,但是大脑在复杂推理的场景下有很大的优势。所以很多时候我觉得 Python 的直观易学要比速度快重要的多,如果必须牺牲其中一个特性的话。)

虽然 Python 具有很低的“代码开发时间”,但它具有很高的“代码执行时间”。为了解决Python非常低的执行性能,Python 的许多扩展模块都是用C / C++ 等高性能语言编写的。像 C / C++ 这样的语言与 Python 完全相反;,它们有很高的“代码开发时间”和非常低的“代码执行时间”。对于每种可能需要的计算密集型任务,不可能都有现成的扩展模块,并且在C / C++ 中编写自己的扩展模块以加速Python代码的慢速部分对于大多数Python程序员来说是遥不可及的。好在我们有不少方式可以改变这点。

为了了解 Python 如何执行 CPU 密集型任务,我们使用一个非常耗时的递归 Fibonacci 算法来确定序列中的第 47 个数字,以模拟计算密集型任务,计算复杂度是 (O(2^n)) 。

斐波那契数列: 根据高德纳(Donald Ervin Knuth)的《计算机程序设计艺术》(The Art of Computer Programming),1150年印度数学家 Gopala 和金月在研究箱子包装对象长宽刚好为1和2的可行方法数目时,首先描述这个数列。在西方,最先研究这个数列的人是比萨的列奥那多(意大利人斐波那契 Leonardo Fibonacci),他描述兔子生长的数目时用上了这数列。斐波那契数列就是这样:0, 1, 1, 2, 3, 5, 8, 13, 21……

摘自 https://zh.wikipedia.org/wiki/斐波那契数列

斐波那契数列可以表示如下:

用递归方式非常容易实现:

def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

if __name__ == "__main__":
    x = 47
    start = time.time()
    res = fib(x)
    elapsed = time.time() - start
    print("Python Computed fib(%s)=%s in %0.4f seconds" % (x, res, elapsed))

这里的 47 是指计算到斐波那契梳理的第 47 位,在我的电脑上计算结果是

Python Computed fib(47)=2971215073 in 667.7838 seconds

将近 11 分钟,电脑配置 Macbook Pro, 2.5 GHz Intel Core i7,16 GB 1600 MHz DDR3,这台电脑是2015年年中的,算是中等计算水平吧。Python 版本 3.7.2。

原文作者的机器性能要更加好一点,Ubuntu 16.04LTS,Intel Xeon E5-2667v3 CPUs 3.20GHz.

如下表对比,我们可以看到 Python 3 在速度上要比 C 语言慢了将近 100 倍,比 Java 也慢了将近 70倍。即便是 PyPy,依然和 C 、Java 语言相比不是一个数量级的。

C Computed fib(47)=2971215073 in 4.58 seconds

Java Computed fib(47)=2971215073 in 7.74 seconds

Go Computed fib(47)=2971215073 in 10.94 seconds

JavaScript Computed fib(47)=2971215073 in 21.384 seconds

PyPy Computed fib(47)=2971215073 in 93.63 seconds

Ruby Computed fib(47)=2971215073 in 191.57 seconds

Python3 Computed fib(47)=2971215073 in 504.55 seconds

Perl5 Computed fib(47)=2971215073 in 980.24 seconds

R Computed fib(47)=2971215073 in 2734.70 seconds

递归计算虽然简洁明了,实际上有包含大量的重复计算,因此称之为计算密集型,下图可以说明递归过程计算的重复性:

利用计算缓存进行优化

我们先用一个非常有效的方式来进行优化,可以让 Python 程序计算斐波那契数列立刻达到 C 语言的水平。

在 stackoverflow 上有专门讨论 python 实现 斐波那契数列的一个帖子。里面有很多实现方式,有的非常巧妙执行速度也非常快。https://stackoverflow.com/questions/494594/how-to-write-the-fibonacci-sequence

我称这个方法是计算缓存,因为递归时候有大量的都是重复计算之前计算过的步骤,我们把每一次的计算输入和输出都存储下来,形成一个缓存,这样一个(O(2^n)) 的复杂度就成了 (O(n)),比如当计算第 5 个数列中的数字时,第 3 个和 第 4 个都已经在缓存中,这样就变成了简单的加法,而不需要真正的递归计算了。并且并不失递归的优雅本质。

我们来看一下代码:

def cache_fib(n, _cache={}):

    if n in _cache:
        return _cache[n]
    elif n > 1:
        return _cache.setdefault(n, cache_fib(n-1) + cache_fib(n-2))
    return n

if __name__ == "__main__":
    x = 47
    start = time.time()
    res = cache_fib(x)
    elapsed = time.time() - start
    print("Python Computed fib(%s)=%s in %0.8f seconds" % (x, res, elapsed))

这样修改后的执行速度就是极速了,为此我把计算时间的代码精确到了小数点后八位,否则显示的就是 0 。

Python Computed fib(47)=2971215073 in 0.00016499 seconds

虽然我们用缓存的方式打败了所有其他语言有点胜之不武,但是在真正的业务系统开发过程中,这样做无可厚非,且应该大力推广。

“Fibonacci Numbers in Python” https://mortada.net/fibonacci-numbers-in-python.html 这篇文章也专门讨论了在 Python 中如何实现斐波那契数列,并且展示了如何使用 pandas 和 matplotlib 技术来可视化的分析执行效率。

计算缓存

缓存技术不是新技术,只是其概念在实际使用中再发生着变化。我们可以学习一下标准的缓存的定义。

Cache一词来源于1967年的一篇电子工程期刊论文。其作者将法语词“cache”赋予“safekeeping storage”的涵义,用于计算机工程领域。CPU的缓存曾经是用在超级计算机上的一种高级技术,不过现今计算机上使用的的AMD或Intel微处理器都在芯片内部集成了大小不等的数据缓存和指令缓存,通称为L1缓存(L1 Cache即Level 1 On-die Cache,第一级片上高速缓冲存储器);而比L1更大容量的L2缓存曾经被放在CPU外部(主板或者CPU接口卡上),但是现在已经成为CPU内部的标准组件;更昂贵的CPU会配备比L2缓存还要大的L3缓存(level 3 On-die Cache第三级高速缓冲存储器)。

主存容量远大于CPU缓存,磁盘容量远大于主存,因此无论是哪一层次的缓存都面临一个同样的问题:当容量有限的缓存的空闲空间全部用完后,又有新的内容需要添加进缓存时,如何挑选并舍弃原有的部分内容,从而腾出空间放入这些新的内容。解决这个问题的算法有几种,如最久未使用算法(LFU)、先进先出算法(FIFO)、最近最少使用算法(LRU)、非最近使用算法(NMRU)等,这些算法在不同层次的缓存上执行时拥有不同的效率和代价,需根据具体场合选择最合适的一种。

https://zh.wikipedia.org/wiki/缓存

在现代开发系统中,由于数据吞吐量太大了,并且重复访问的情况也非常多,为了有效的节约算力、提升响应速度、减少对系统的依赖,缓存技术大大发展。比如 Redis 就是广泛使用的一种缓存技术。刚才我们看到在递归计算中使用了缓存,对于性能可以有千万倍的提升。

下面介绍一下 Python 中的一些缓存技术。

lru_cache

Python 语言就是一个瑞士军刀,绝大多数需要的功能都已经整装待发。lru_cache 就是 Python 3.2 开始在 functools 中增加一个函数,通过装饰器的方式来缓存一个函数的执行结果。https://docs.python.org/3/library/functools.html#functools.lru_cache

在上面的文档中,我们可以看到 Python 官方同样是用斐波那契数列作为例子,可见这个斐波那契数列的递归其实是多么的不深入人心啊。

import time
from functools import lru_cache


@lru_cache(maxsize=None)
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
    x = 47
    start = time.time()
    res = fib(x)
    elapsed = time.time() - start
    print("Python Computed fib(%s)=%s in %0.8f seconds" % (x, res, elapsed))
    print(fib.cache_info())

我们在最后一行增加了显示缓存击中的情况。

Python Computed fib(47)=2971215073 in 0.00003409 seconds

CacheInfo(hits=44, misses=47, maxsize=None, currsize=47)

执行速度上可以看到比刚才我们自己实现的缓存算法还要快。

DiskCache

DiskCache 是一个纯 Python 的缓存包,http://www.grantjenks.com/docs/diskcache/

DiskCache 可以有效的只用上 G 空间用于缓存,通过利用坚如磐石的数据库和内存映射文件,缓存性能可以匹配并超越行业标准解决方案。(放在这里解决斐波那契数列问题有点杀鸡用牛刀了!)

DiskCache 的主要功能如下:

  • 纯 Python 构造
  • 完整的文档
  • 100% 单元测试覆盖
  • 数小时的压力测试
  • Django 兼容的 API
  • 线程安全和进程安全
  • 支持多种缓存算法 (包括LRU 和 LFU)
  • Keys 支持标签、元数据等
  • 基于 Python 2.7 开发,在 CPython 2.7, 3.4, 3.5, 3.6 和 PyPy 上测试
  • 支持 Linux, Mac OS X 和 Windows
  • 通过 Travis CI 和 AppVeyor CI 的集成测试

DiskCache 的功能更像是 RedisMemCached,并且性能优异,我们可以看看下面的性能对照表。

DiskCache 功能非常多,我们用文档中的一个例子修改一下,来继续刚才的斐波那契数列的 demo,前面的计算缓存是将相关缓存代码写在了函数的逻辑中,通过 DiskCache 的 FanoutCache 来沟通一个函数的装饰器,同样且更加通用的达到计算缓存的效果。

from diskcache import FanoutCache
import time

cache = FanoutCache('/tmp/diskcache/fanoutcache')


@cache.memoize(typed=True, expire=1, tag='fib')
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
    x = 47
    start = time.time()
    res = fib(x)
    elapsed = time.time() - start
    print("Python Computed fib(%s)=%s in %0.8f seconds" % (x, res, elapsed))

执行效果如下:

Python Computed fib(47)=2971215073 in 0.02766585 seconds

装饰器中的 expire 参数是多少毫秒后失效,使用 DiskCache 的话,在装饰器发挥作用前定义了磁盘缓存文件的位置。如果将参数 expire 调整到比较大的数值或者 None 的话,会发现再次执行的话,速度大大提升。

Python Computed fib(47)=2971215073 in 0.00032520 seconds

DiskCache 功能强大,值得用专门的章节来完整的介绍。

cache.py

如果想既有 lru_cache 这样简单,又暂时不想用 DiskCache 这样的大家伙,但是其文件可以实例化还是不错的一种解决方案,我们还可以尝试用一下 cache.py,出处在这里 https://github.com/bwasti/cache.py

只要 import cache 之后,就可以直接使用了。

import cache
import time


@cache.cache(timeout=20, fname="my_cache.pkl")
def fib(n):
    if n <= 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
    x = 47
    start = time.time()
    res = fib(x)
    elapsed = time.time() - start
    print("Python Computed fib(%s)=%s in %0.8f seconds" % (x, res, elapsed))

执行结果如下:

Python Computed fib(47)=2971215073 in 0.02948809 seconds

列出四种缓存方式的执行速度:

内置缓存 0.00016499 seconds

Python 自带 lru_cache 0.00003409 seconds

DiskCache 0.00032520 seconds

cache.py 0.02948809 seconds

现在在这类计算缓存的场景下,Python 自带的 lru_cache 速度最快,而 DiskCache 包性能均衡,考虑到其强大的功能,值得一试,cache.py 的性能一般,但是代码非常简洁,可以学习。

关于角色模型

最近一年一直比较关心并行计算,一开始因为 python 的线程锁实在麻烦,然后又因为一直受到 k8s 的弹性伸缩影响,加上最近又看了不少函数计算的材料和相关的 POC。我之前曾经设想过,如果将程序都用函数计算的方式,同时通过 DSL 来连接这些函数,这样既可以应对访问压力,又可以处理复杂逻辑,加上弹性,费用也不会太高,达到最佳 ROI。

看到阿里云最新发布的类似 numpy 的 python 包用于并行计算来处理本来需要大内存的机器学习运算,其中提到了通过角色模型。发现这个角色模型或许可以满足前面设想的一些逻辑。

wiki 告诉我们:在计算机科学中,角色模型(英语:Actor model)是一种并发运算上的模型。“角色”是一种程序上的抽象概念,被视为并发运算的基本单元:当一个角色接收到一则消息,它可以做出一些决策、创建更多的角色、发送更多的消息、决定要如何回答接下来的消息。角色模型在1973年于Carl Hewitt、Peter Bishop及Richard Steiger的论文中提出。

支持角色模型的语言非常多,

角色模型推崇的哲学是“一切皆是角色”,这与面向对象编程的“一切皆是对象”类似,但是面向对象编程通常是顺序执行的,而角色模型是并行执行的。角色是一个运算实体,回应接受到的消息,同时并行的:

发送有限数量的消息给其他角色;
创建有限数量的新角色;
指定接受到下一个消息时的行为。
以上操作不含有顺序执行的假设,因此可以并行进行。

发送者与已经发送的消息解耦,是角色模型的根本优势。这允许进行异步通信,同时满足消息传递的控制结构。

消息接收者是通过地址区分的,有时也被称作“邮件地址”。因此角色只能和它拥有地址的角色通信。它可以通过接受到的信息获取地址,或者获取它创建的角色的地址。

角色模型的特征是,角色内部或之间进行并行计算,角色可以动态创建,角色地址包含在消息中,交互只有通过直接的异步消息通信,不限制消息到达的顺序。

几乎所有主流语言都支持角色模型,在 Python 里面保持更新的库是:

Pykka:https://github.com/jodal/pykka
Pulsar:http://quantmind.github.io/pulsar/

之后会继续研究角色模型的使用和实用性。

每天编程:继续谈谈 python pathlib

本文内容翻译自 python3_with_pleasure 

pathlib 现在是 python 3 的标准库,避免你来写一大堆 os.path.join。

from pathlib import Path

dataset = 'wiki_images'
datasets_root = Path('/path/to/datasets/')

train_path = datasets_root / dataset / 'train'
test_path = datasets_root / dataset / 'test'

for image_path in train_path.iterdir():
    with image_path.open() as f: # note, open is a method of Path object
        # do something with an image

我们可以看到一个在机器学习中使用到的场景,训练数据的路径和测试用路径,通过很简单的 / 符号就连接起来了。代码安全、简洁、可读性好。

pathlib.path 有一大堆的方法和属性,我们可以减少很多花费在搜索引擎上的时间。

p.exists()
p.is_dir()
p.parts
p.with_name('sibling.png') # only change the name, but keep the folder
p.with_suffix('.jpg') # only change the extension, but keep the folder and the name
p.chmod(mode)
p.rmdir()

每天编程:非常好用的 python 路径文件处理库 pathlib

的确,我之前也是习惯用 os.path.join ,倒是也没有觉得有什么不好,直到我看到了 pathlib,应该是 python 3.x 时代引入的新的专门更加优雅的处理比较麻烦的路径拼接、文件是否存在、路径是否存在等操作系统文件路径相关的Python 官方函数。

比如你可以用 path1 / path2 / filename 这样的简单直观的方法来完成路径和文件名的拼接,通过 PurePosixPath 和 PureWindowsPath 的类显式的兼容多操作系统。

原来的 os 库发展太久了,如同 python 的日期时间库一样,在长时间的进化中包和其函数的结构越来越混乱了。从下面的 os 和 pathlib 的函数比较可见一斑:

os and os.pathpathlib
os.path.abspath()Path.resolve()
os.chmod()Path.chmod()
os.mkdir()Path.mkdir()
os.rename()Path.rename()
os.replace()Path.replace()
os.rmdir()Path.rmdir()
os.remove()os.unlink()Path.unlink()
os.getcwd()Path.cwd()
os.path.exists()Path.exists()
os.path.expanduser()Path.expanduser() and Path.home()
os.path.isdir()Path.is_dir()
os.path.isfile()Path.is_file()
os.path.islink()Path.is_symlink()
os.stat()Path.stat()Path.owner(),Path.group()
os.path.isabs()PurePath.is_absolute()
os.path.join()PurePath.joinpath()
os.path.basename()PurePath.name
os.path.dirname()PurePath.parent
os.path.samefile()Path.samefile()
os.path.splitext()PurePath.suffix

Pathlib 非常完美的体现了 Python 的优雅,更多信息可以参考官方文档。打算将 fishbase 中的一些封装的文件处理函数逐步更换为对 Pathlib 的封装,Python 2.7 实在没有必要支持了。

每天编程:用 Python 的类方法构造通用函数

今天花了十分钟将之前 fishbase 的身份证和银行卡的7个函数修改为类方法,这样调用基本没啥改变,调用方法优雅一些,并且用类管理之后,暴露接口等都比较好控制,类相比函数的好处就不用再说了。(很遗憾,20多年前我在用 Turbo Pascal 以及后来 Delphi 的很长一段时间内,都没有理解这一点,一直是面向过程编程,大约一直到 2005年才突然顿悟,应该用面向对象的方法,包括和自己的共享软件中的故事,以后可以专门聊聊。)

我在代码里保留了一小段 Classmethod 的例子:

class CardBin(object):

    @classmethod
    def a(cls):
        return 'hello'

    @classmethod
    def b(cls):
        return CardBin.a() + ' world'


print(CardBin.a())
print(CardBin.b())

这个例子定义了一个类方法 a,还有一个使用了类方法 a 的类方法 b,以及外部使用这两个类方法的例子。

类方法的定义和与静态方法的好处就不多说了,至少使用的时候不用实例化,这一点对我们来说在这个场景就很好。

每天编程:怎么判断身份证号码是否正确

继续分享生成测试数据库时候的一些心得,在生成假数据时,如何判断身份证号码是否正确,和银行卡一样,身份证最后一位是校验码,不过计算方法不太一样。算法这里不详细叙述,网上很多。下面的代码可以实现这一功能,输入身份证的前面17位,返回校验码。

def get_checkcode(id_number_str):
    """
    计算身份证号码的校验位;

    :param:
        * id_number_str: (string) 身份证号的前17位,比如 3201241987010100
    :returns:
        * 返回类型 (tuple)
        * flag: (bool) 如果身份证号格式正确,返回 True;格式错误,返回 False
        * checkcode: 计算身份证前17位的校验码

    举例如下::

        from fishbase.fish_data import *

        print('--- fish_data idcard_get_checkcode demo ---')

        # id number
        id1 = '32012419870101001'
        print(id1, idcard_get_checkcode(id1)[1])

        # id number
        id2 = '13052219840731647'
        print(id2, idcard_get_checkcode(id2)[1])

        print('---')

    输出结果::

        --- fish_data idcard_get_checkcode demo ---
        32012419870101001 5
        13052219840731647 1
        ---

    """

    # 判断长度,如果不是 17 位,直接返回失败
    if len(id_number_str) != 17:
        return False, -1

    id_regex = '[1-9][0-9]{14}([0-9]{2}[0-9X])?'

    if not re.match(id_regex, id_number_str):
        return False, -1

    items = [int(item) for item in id_number_str]

    # 加权因子表
    factors = (7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2)

    # 计算17位数字各位数字与对应的加权因子的乘积
    copulas = sum([a * b for a, b in zip(factors, items)])

    # 校验码表
    check_codes = ('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2')

    checkcode = check_codes[copulas % 11].upper()

    return True, checkcode

这段代码已经可以基本实现网上大部分的身份证校验器的功能了,在实际生成身份证假数据的时候,问题还要复杂一些,因为我们不能随便那一串数字,去生成一个校验码,那个意义不太大。转载如下:

公民身份号码是特征组合码,由前十七位数字本体码和最后一位数字校验码组成。排列顺序从左至右依次为六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。

地址码: 表示编码对象常住户口所在县(市、旗、区)的行政区划代码。对于新生儿,该地址码为户口登记地行政区划代码。需要没说明的是,随着行政区划的调整,同一个地方进行户口登记的可能存在地址码不一致的情况。行政区划代码按GB/T2260的规定执行。

出生日期码:表示编码对象出生的年、月、日,年、月、日代码之间不用分隔符,格式为YYYYMMDD,如19880328。按GB/T 7408的规定执行。原15位身份证号码中出生日期码还有对百岁老人特定的标识,其中999、998、997、996分配给百岁老人。

顺序码: 表示在同一地址码所标识的区域范围内,对同年、同月、同日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配给女性。

校验码: 根据本体码,通过采用ISO 7064:1983,MOD 11-2校验码系统计算出校验码。算法可参考下文。前面有提到数字校验码,我们知道校验码也有X的,实质上为罗马字符X,相当于10.


fishbase 扩展包 for python:
源代码:https://github.com/chinapnr/fishbase
文档:https://fishbase.readthedocs.io/en/latest/
pypi :https://pypi.org/project/fishbase/ 


每天编程:怎么校验银行卡是否有效

最近在公司有一个 fakedata 的项目,用来生成各类假数据,在各种测试环境,其中就有银行卡卡号的生成,中国这方面是有自己的标准的,详细的介绍可以参考这篇文章,写的很清楚。

简单来说,就是国内的银行卡卡号,主要都是银联标准,然后前面的4-8位称之为卡 bin,这个卡 bin 是可以区分是具体哪个银行,以及是否是借记卡还是贷记卡(信用卡),关键是最后一位校验位。

从校验卡号的角度来说,可以在应用的尽量前端进行判断,如果卡号校验错误,就不用去进行四要素判断,更不能进行实际的账户交易了。校验码对了,再根据卡 bin 判断是什么银行和什么性质的银行卡(这个下次再说)。

def get_bankcard_checkcode(card_number_str):
    """
    计算银行卡校验位;

    :param:
        * card_number_str: (string) 要查询的银行卡号
    :returns:
        checkcode: (string) 银行卡的校验位

    举例如下::

        from fishbase.fish_data import *

        print('--- fish_data get_bankcard_checkcode demo ---')

        # 不能放真的卡信息,有风险
        print(get_bankcard_checkcode('439188000699010'))

        print('---')

    输出结果::

        --- fish_data get_bankcard_checkcode demo ---
        9
        ---

    """
    total = 0
    even = True

    for item in card_number_str[-1::-1]:
        item = int(item)
        if even:
            item <<= 1
        if item > 9:
            item -= 9
        total += item
        even = not even

    checkcode = (10 - (total % 10)) % 10

    return str(checkcode)

实际算法部分不算复杂,参考了一些网上看到的资料。需要的朋友可以借鉴。


fishbase 扩展包 for python:
源代码:https://github.com/chinapnr/fishbase
文档:https://fishbase.readthedocs.io/en/latest/
pypi :https://pypi.org/project/fishbase/