首发于Python 码坊
驳 《Python正则表达式,请不要再用re.compile了!!!》

驳 《Python正则表达式,请不要再用re.compile了!!!》

刚刚看到 @青南 的这篇文章

青南:Python正则表达式,请不要再用re.compile了!!!zhuanlan.zhihu.com图标

最近正好和播客的搭档讨论了下正则,看到文章中的一些观点,我自己很不赞同。学前端娱乐圈,来写篇文章驳斥下吧

首先作者开宗明义,观点先行

如果大家在网上搜索Python 正则表达式,你将会看到大量的垃圾文章会这样写代码:
import re

pattern = re.compile('正则表达式')
text = '一段字符串'
result = pattern.findall(text)
这些文章的作者,可能是被其他语言的坏习惯影响了,也可能是被其他垃圾文章误导了,不假思索拿来就用。

然后作者的观点在于,对于常见的诸如 findallsearch 之类的 module-level function 是和 compile 一样,调用了私有的 _compile 方法,而在 _compile 方法中对于一定数量的 pattern 的编译结果做了缓存。那么是不是这样呢?

我们先来看看文档,我去检查了这一部分的文档。在 Python 3.3 的文档中,官方描述如下

The compiled versions of the most recent patterns passed to re.match(), re.search() or re.compile() are cached, so programs that use only a few regular expressions at a time needn’t worry about compiling regular expressions.

参见 docs.python.org/3.3/lib

而 Python 3.4 到最近的 Python 3.7 描述如下

The compiled versions of the most recent patterns passed tore.compile()and the module-level matching functions are cached, so programs that use only a few regular expressions at a time needn’t worry about compiling regular expressions.

参见,docs.python.org/3/libra

我们能从文档中发现,对于具体版本的 cache 行为,是不一致的,而我去查了一下历年的 PEP,发现目前无具体的提案,对 re module 中的缓存行为做了明确约束。而这样的一个东西,从某种意义上来讲,叫做 undefined behavior,(写C++的同学可能对于这个词更熟悉)

我再举一个 Python 中的例子

大家都知道,Python 3.6 的时候,因为优化 dict 的性能,导致了 dict 一个特性的变化,即从原本的无序(插入顺序与存储顺序无关)变成了有序,在 Python 3.7 作为一个特性被在文档中说明(无PEP),但是,为了通用性的考虑,我们在使用有序字典的时候,还是会优先考虑 collections.OrderedDict

好了,假设缓存行为所有版本一致,我们该怎么选择?

我的答案是看场景,

如果是一些临时的场景,自己的一些脚本,我觉得 re.xxx 暴力出奇迹。但是如果要写一些正式的项目,那么就未必了。我来分点描述一下

  1. 为了可维护性

比如假定一个场景,我们要对 zk 一个 URL 进行匹配,提取出有效的信息,有这样一个正则 \/(?P<app_name>.*?)\/(?P<cluster_name>.*?)\/(?P<config_key>.*)

我们有很多个地方都会用这样的正则进行参数校验,数据提取。

那么我们两个版本的代码如下

import re


def validate(data: str) -> bool:
    match_object = re.match(
        "\/(?P<app_name>.*?)\/(?P<cluster_name>.*?)\/(?P<config_key>.*)", data)
    if match_object:
        return True
    return False


def get_config_key(data: str) -> str:
    match_object = re.match(
        "\/(?P<app_name>.*?)\/(?P<cluster_name>.*?)\/(?P<config_key>.*)", data)
    if match_object:
        return match_object.groupdict()["config_key"]
    return ""
 

使用 compile 的版本如下

import re

CONFIG_PATTERN = re.compile(
    "\/(?P<app_name>.*?)\/(?P<cluster_name>.*?)\/(?P<config_key>.*)")


def validate(data: str) -> bool:
    match_object = CONFIG_PATTERN.match(data)
    if match_object:
        return True
    return False


def get_config_key(data: str) -> str:
    match_object = CONFIG_PATTERN.match(data)
    if match_object:
        return match_object.groupdict()["config_key"]
    return ""

这里我肤浅的认为,论可维护性,使用 compile 编译正则,并赋值给一个 const 这样的代码,还是要比满屏乱飞的 re.xxx 要好很多吧?

2. 性能问题

作者原文的评论区,提到了这样一个概念

Q:都用正则表达式了,一群人竟然在讨论性能开销?真正出了瓶颈绝对不会是没有compile,而是search跟findall[捂脸]
A:终于遇到了一个明白人,其他人除了装逼什么都不会。

这里我肤浅的认为在一些情况下, compile 还是会存在性能问题的。并不是作者说的装逼

比如,那天和搭档 @赖信涛 讨论过一个正则

REDIS_COMMANDS = fr"""
(\s*  (?P<command_slots>({t['command_slots']}))        \s+ {SLOTS}                                    \s*)|
(\s*  (?P<command_node>({t['command_node']}))          \s+ {NODE}                                     \s*)|
(\s*  (?P<command_slot>({t['command_slot']}))          \s+ {SLOT}                                     \s*)|
(\s*  (?P<command_failoverchoice>({t['command_failoverchoice']}))
                                                       \s+ {FAILOVERCHOICE}                           \s*)|
(\s*  (?P<command_resetchoice>({t['command_resetchoice']}))
                                                       \s+ {RESETCHOICE}                              \s*)|
(\s*  (?P<command_slot_count>({t['command_slot_count']}))
                                                       \s+ {SLOT}    \s+ {COUNT}        \s*)|
(\s*  (?P<command>({t['command']}))                                                                   \s*)|
(\s*  (?P<command_key>({t['command_key']}))            \s+ {KEY}                                      \s*)|
(\s*  (?P<command_ip_port>({t['command_ip_port']}))    \s+ {IP}      \s+ {PORT}                       \s*)|
(\s*  (?P<command_epoch>({t['command_epoch']}))        \s+ {EPOCH}                                    \s*)|
(\s*  (?P<command_slot_slotsubcmd_nodex>({t['command_slot_slotsubcmd_nodex']}))
                                                       \s+ {SLOT}    \s+ {SLOTSUBCMD}   (\s+ {NODE})? \s*)|
(\s*  (?P<command_slot_slotsubcmd_nodex>({t['command_slot_slotsubcmd_nodex']}))
                                                       \s+ {SLOT}    \s+ {SLOTSUBCMDBARE}             \s*)|
(\s*  (?P<command_password>({t['command_password']}))  \s+ {PASSWORD}                                 \s*)|
(\s*  (?P<command_message>({t['command_message']}))    \s+ {MESSAGE}                                  \s*)|
(\s*  (?P<command_messagex>({t['command_messagex']}))  (\s+{MESSAGE})?                                \s*)|
(\s*  (?P<command_index>({t['command_index']}))        \s+ {INDEX}                                    \s*)|
(\s*  (?P<command_index_index>({t['command_index_index']}))
                                                       \s+ {INDEX}   \s+ {INDEX}                      \s*)|
(\s*  (?P<command_key>({t['command_key']}))            \s+ {KEY}                                      \s*)|
(\s*  (?P<command_keys>({t['command_keys']}))          \s+ {KEYS}                                     \s*)|
(\s*  (?P<command_key_second>({t['command_key_second']})) 
                                                       \s+ {KEY}     \s+ {SECOND}                     \s*)|
(\s*  (?P<command_key_timestamp>({t['command_key_timestamp']})) 
                                                       \s+ {KEY}     \s+ {TIMESTAMP}                  \s*)|
(\s*  (?P<command_pattern>({t['command_pattern']}))    \s+ {PATTERN}                                  \s*)|
(\s*  (?P<command_key_index>({t['command_key_index']})) 
                                                       \s+ {KEY}     \s+ {INDEX}                      \s*)|
(\s*  (?P<command_key_millisecond>({t['command_key_millisecond']})) 
                                                       \s+ {KEY}     \s+ {MILLISECOND}                \s*)|
(\s*  (?P<command_key_timestampms>({t['command_key_timestampms']}))
                                                       \s+ {KEY}     \s+ {TIMESTAMPMS}                \s*)|
(\s*  (?P<command_key_newkey>({t['command_key_newkey']}))
                                                       \s+ {KEY}     \s+ {NEWKEY}                     \s*)|
"""

来源 github.com/laixintao/ir

这个正则是用来校验用户输入的 Redis 的命令语法是否正确的,当时讨论的焦点是,这个正则单独 compile 需要 3s 的时间,我们最后讨论的一个解决方案是,提前 compile,将 compile 的结果 pickle 后随包发布。如果这里不提前 compile ,那么将会遇到用户第一次输入命令时,终端将会无响应 3s。 我肤浅的认为,这可能会导致用户体验不佳

3. 直接使用 module-level 也会有性能问题

如同 @Danpier@樊冰心 在评论区中提到的一样,官方文档很明确的说明了这一点

but usingre.compile()and saving the resulting regular expression object for reuse is more efficient when the expression will be used several times in a single program.

参见 docs.python.org/3/libra

所以,很多东西并不能一概而论,要考虑具体的使用方式和场景。

其实我自己是很不喜欢写这种驳斥文章,因为我认为每个人的观点有所不同是一件很正常的事。但是我所秉持的一个观点态度是不论青红皂白,给别人的观点盖上一个垃圾的帽子这是一个很不可取的行为,比如

比如

综上所述,请你不要再手动调用re.compile了,这是从其他语言(对的,我说的就是Java)带过来的陋习。

这也是,我这篇文章真正想驳斥的东西

以上。

编辑于 2019-08-15

文章被以下专栏收录