回《驳 》
知乎用户@Manjusaka 在阅读了我的文章《Python正则表达式,请不要再用re.compile了!!!》以后,写了一篇驳文《驳 <Python正则表达式,请不要再用re.compile了!!!>》
今天,我在这里回应一下这篇驳文。首先标题里面,我用的是回
,意为回复,而不是继续驳斥@Manjusaka的文章。因为没有什么好驳斥的,他的观点没有什么问题。
首先说明,我自己在公司的代码里面,也会使用re.compile
。但是,我现在仍然坚持我的观点,让看这篇文章的人,不要用re.compile
。
你真的在意这点性能?
在公司里面,我使用re.compile
的场景是这样的:
每两小时从10亿条字符串中,筛选出所有不符合特定正则表达式的字符串。
这个程序可以简化为如下结构:
1 | import re |
在这个场景下面,对于10亿个字符串,3个正则表达式,需要循环30亿次。虽然读取正则表达式缓存的时间很短,假设只有1毫秒,那么也会浪费833小时。为了实现2小时内处理10亿条数据,我做了很多优化,其中之一就是提前re.compile
:
1 | import re |
在这样的场景下,这样的数据量级下面,你是用re.compile
,当然可以。
然而,你日常接触到的工作,都是这个量级吗?知乎上流行一句话:
抛开剂量谈毒性,都是耍流氓。
同样的,在数据处理上也适用:
抛开量级谈性能差异,都是耍流氓
处理几百条数据,还需要担心读取缓存字典的这点小小的性能开销?
我在另一篇文章为什么Python 3.6以后字典有序并且效率更高?中提到,从Python 3.6开始,字典不会再提前申请更多空间了,同时也变得有序了,作为代价就是从字典读取值的过程多了一步。多出来的这一步实际上也会有性能开销,因为它需要先查询indices
,然后再查询entries
。为什么Python愿意放弃性能而要让字典有序?因为新的实现方式,在整体迭代、空间利用率上面都更高。
维护自文档性
回到正则表达式的例子来,Python区别于其他语言的一个非常重要的点是什么?是它的自文档性。
网上有这样一个段子:
问:如何把伪代码改写为Python代码?
答:把.txt改成.py即可。
Python的自文档性非常好,即便完全不懂编程的人,看到Python的代码,也能猜的出代码想实现什么功能。
请大家对比下面两种写法:
1 | re.findall('密码: (.*?)$', sentence) |
和
1 | regex = re.compile('密码: (.*?)$') |
如果让一个完全不会编程的人来看,他看到第一段代码,会猜测:“findall是查找全部,这段代码可能是要从sentence找什么东西”。
而如果让他看第二段代码,他肯定会先问一句:“compile?编译?什么是编译?编写翻译吗?”
而对于刚刚学编程的人来说,如果他看的Python正则表达式入门的文档里面用了re.compile
,他也会很疑惑,为什么要compile
?编译成了什么东西?为什么不能直接查询?于是新人可能会过早去研究底层的东西。
但如果他看的文章直接是re.findall
,那么语义非常明确:正则表达式.查询所有
,一目了然,轻轻松松就能理解并学会。
以官方文档的实例入门
当我们学习一门新的语言的时候,第一应该参考的就是它的官方文档。在正则表达式官方文档https://docs.python.org/3/library/re.html#finding-all-adverbs的例子中,无论是search
还是findall
都是使用re.xxx
的形式。如下图所示:
所以网上那些首先使用pattern = re.compile
,再pattern.xxx
的人,要不就是直接从其他语言把先compile
再查询的思维定势带到了Python中,要不就是做正则表达式调优做太久了,思维僵化了,一抬手就是re.compile
。
面向接口编程还是面向人类编程?
在我文章的评论里面,有人说,应该面向接口编程,而不是面向实现编程。
对这些人,我想跟你们讲:你们对面向接口编程,理解得太狭隘了!
我们来看看,在Python著名的http库requests
出来之前,使用urllib2
发起一个请求是怎么写的:
1 | import urllib2 |
有了requests
以后,实现同样的功能,我们是这样写的:
1 | import requests |
大家自己品位一下,req = urllib2.Request(gh_url)
如果类比为pattern = re.compile('xxxx')
,handler = urllib2.urlopen(req)
类比为pattern.findall(sentence)
那么,requests.get(xxx)
就是re.findall
。
为什么我们现在愿意使用requests
而不愿意使用urllib2
?
因为requests
是for human,而urllib
是for interface
.
不是问题的问题
在评论里面,竟然有人质疑我使用re.findall
,正则表达式不好维护?
@Manjusaka竟然举出了下面这样的例子:
为什么使用re.findall
,就一定要把正则表达式复制粘贴很多遍?
我单独定义一个文件不行吗:
1 | # regex_str.py |
然后我要使用正则表达式的地方直接导入进来:
1 | import re |
请问哪里不好维护了?根本不会出现@Manjusaka说的那种情况。
总结
我的观点如下:
- re.compile很重要,也有用。但是大多数时候你不需要使用它。
- 对于初学者,请直接使用
re.findall
re.search
,不要使用re.compile
。 - 对于有经验的工程师,在开发项目的时候,请首先使用
re.findall
re.search
等等上层函数,直到你需要考虑优化正则表达式查询性能的时候,再考虑先re.compile
。因为很多时候,你的代码性能,还不至于需要靠几行re.compile
来提高。 - 有人问正则表达式默认缓存512条,这个数字没有写在文档里面,如果哪天改了怎么办?我的回答是:看看你写过的代码,涉及到的正则表达式有几次超过了100条?
- 正则表达式基于DFA,在它的原理上,compile这一步确实是必需的。但这并不意味着,在写代码的时候,我们一定要自己手动写compile. 毕竟封装、抽象才是高级语言的一大特征,直接。在其他编程语言里面,没有把compile和查询封装成一个整体接口,但是在Python里面这样做了。那么我们就应该用这个更上层的接口。而不是手动compile再查询。
- 为什么Java程序员常常加班,而Python程序员常常提前完成任务?正是因为这种Language Specific的特性提高了生产效率,屏蔽了前期不需要太早关心的实现细节。如果抱着写代码要语言无关,要通用而故意放弃了一些语言特性,那为什么不直接写1010?那才是真正的语言无关,所有语言都是建立于二进制的1010上的。
多说一句
以下内容与本次讨论的re.compile无关。
@Manjusaka给出了一个compile需要3秒钟的大型正则表达式,并以此作为例子说明re.compile的合理性。
首先这种情况下,确实需要提前re.compile。
但我所想表达的是,在这种情况下,就不应该使用正则表达式。既然要做Redis的语法校验,那么就应该使用有限状态机。这种使用很多的f表达式拼出来的正则表达式,才是真正的难以维护,难以阅读。
否则为什么里面需要用一个csv文件来存放命令呢?为什么不直接写在正则表达式里面呢?使用CSV文件每行一个命令尚且可以理解,但是SLOT
/SLOTS
/NODE
/NEWKWY
这些正则表达式,可就说不过去了。或条件连接的每一段都要加上这些东西,如果直接写进去,这个正则表达式你们自己都看不下去了,所以才会需要使用拼接的方式生成。
我在读这段代码的时候,首先看到正则表达式里面的t[xxx]
,会先去找t
是什么东西,发现t是一个字典,字典是在commands_csv_loader.py
中生成的,然后去到这个文件里面,发现它读的是一个存放Redis命令的CSV文件。然后去项目根目录读取这个csv文件的内容,知道了它的结构,于是推测出t的结构。然后再回到正则表达式里面,继续看这个超大的正则表达式。整个过程会非常费时间和脑子。
但是,我又不能直接打印REDIS_COMMANDS这个变量,因为它多且乱,不同命令长短不一,拼出来以后再打印出来根本没法看。
这个正则表达式只有两位维护者知道什么意思,如果别人想贡献新的Redis命令,那么理解这个超大正则表达式都需要花很久的时间。
如果换成有限状态机,并且t使用Python的data class来表示,而不是使用字典,那么就会简洁很多。有限状态机的一个特点是,只需要关注当前状态、转移条件和目标状态,可能一开始写起来有点麻烦,但是以后维护和新增,都是直接定位目标,直接修改,不用担心会影响不想干的其他地方。
算上维护时间,正则表达式真是一个非常糟糕的方式。