备注,这种动态设置 module 里的方法不推荐

# 前言

整理工具字符类的时候,想借助正则表达式来实现一部分的文字判断抽取等操作。

比如实现:

  • 判断文字是否为 UUID
  • 判断文字是否包含 UUID
  • 抽取文字是中第一个 UUID
  • 抽取文字是中所有 UUID

# 一个暴力的实现方法

如果正则表达式比较少,就只一个 UUID,我们就不需要思考什么,我们分别编写四个函数:

  • is_uuid(_str)
  • has_uuid(_str)
  • extract_first_uuid(_str)
  • extract_all_uuid(_str)

没错,过早优化是万恶之源

但很显然,手动方法显得很弱智,当我需要编写判断 QQ 号的时候,我又必须编写四个函数:

  • is_qq_num(_str)
  • has_qq_num(_str)
  • extract_first_qq_num(_str)
  • extract_all_qq_num(_str)

然而:

  • 如果,我还需要判断手机号、日期、时间等等,这手动复制粘贴的过程就比较痛苦了。
  • 如果,我去要添加一个方法,给 QQ 号码,uuid 等打码 那就必须要给所有的 uuid, 手机,邮箱都添加一个 dama_xxx(_str) 方法

有没有好一点的解决方法呢?

# 两个方法

第一种,比如把函数修改为:

  • is(_str,QQ_NUM_PATTEN)
  • has(_str,QQ_NUM_PATTEN)
  • extract_first(_str,QQ_NUM_PATTEN)
  • extract_all(_str,QQ_NUM_PATTEN)

第二种,Python 中动态添加工具方法,我个人比较喜欢这种:

# 一个优雅的错误实现方式
for regex, regex_pattern in REGEXES.items():
    def has_regex_func(_str):
        return has_pattern(_str, regex_pattern)

    def is_regex_func(_str):
        return match_pattern(_str, regex_pattern)

    def extract_first_regex_func(_str):
        return find_first_matched_pattern(_str, regex_pattern)

    def extract_all_regex_func(_str):
        return find_all_matched_pattern(_str, regex_pattern)

    setattr(sys.modules[__name__], 'has_{regex_suffix}'.format(regex_suffix=regex), has_regex_func)
    setattr(sys.modules[__name__], 'is_{regex_suffix}'.format(regex_suffix=regex), is_regex_func)
    setattr(sys.modules[__name__], 'extract_first_{regex_suffix}'.format(regex_suffix=regex), extract_first_regex_func)
    setattr(sys.modules[__name__], 'extract_all_{regex_suffix}'.format(regex_suffix=regex), extract_all_regex_func)

于是我添加了测试方法:

一个不对稍微有些复杂的逻辑的程序进行测试的程序员不是一个称职的老司机。

@pytest.mark.parametrize('test_input,expected', [
    ("321323199509234453", False),
    ("000528-332222", False),
    ("521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4", True),
])
def test_is_uuid(test_input, expected):
    assert is_uuid(test_input) == expected

@pytest.mark.parametrize('test_input,expected', [
    ("321323199509234453", False),
    ("000528-332222", False),
    ("521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4", True),
])
def test_has_uuid(test_input, expected):
    assert has_uuid(test_input) == expected

@pytest.mark.parametrize('test_input,expected', [
    ("321323199509234453", None),
    ("000528-332222", None),
    ("521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4", "521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4"),
])
def test_extract_first_uuid(test_input, expected):
    assert extract_first_uuid(test_input) == expected

@pytest.mark.parametrize('test_input,expected', [
    ("321323199509234453", None),
    ("000528-332222", None),
    (
            "521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4",
            ['521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4', '521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4',
             '521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4', '521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4']),
    (
            "521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4   521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4   521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4   aslakdj 521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4",
            ['521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4', '521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4',
             '521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4', '521e7bb0-d8d5-4f49-a5c2-fee1aaf9e8c4']),
])
def test_extract_all_uuid(test_input, expected):
    assert extract_all_uuid(test_input) == expected

测试未通过:

怎么查看代码本身都没有什么逻辑问题,那么问题出在哪里?

对程序植入一些 print 代码来 Debug 一下:

for regex, regex_pattern in REGEXES.items():
    def has_regex_func(_str, regex_pattern=regex_pattern):
        # 当函数被调用之后,打印 regex_pattern 查看对应的字符串
        print(regex_pattern)
        return has_pattern(_str, regex_pattern)

    def is_regex_func(_str, regex_pattern=regex_pattern):
        return match_pattern(_str, regex_pattern)

    def extract_first_regex_func(_str, regex_pattern=regex_pattern):
        return find_first_matched_pattern(_str, regex_pattern)

    def extract_all_regex_func(_str, regex_pattern=regex_pattern):
        return find_all_matched_pattern(_str, regex_pattern)

    # 查看是否为同一个函数
    print(id(has_regex_func))

    setattr(sys.modules[__name__], 'has_{regex_suffix}'.format(regex_suffix=regex), has_regex_func)
    setattr(sys.modules[__name__], 'is_{regex_suffix}'.format(regex_suffix=regex), is_regex_func)
    setattr(sys.modules[__name__], 'extract_first_{regex_suffix}'.format(regex_suffix=regex), extract_first_regex_func)
    setattr(sys.modules[__name__], 'extract_all_{regex_suffix}'.format(regex_suffix=regex), extract_all_regex_func)

于是发现问题,所有打印出来的 regex_pattern 都是一致的。也就是,不管是 has_uuid 还是 has_qq_num 还是其他,最后 regex_pattern 都是我在字典中实现的