在 Python 中创建动态部分可调用对象

Creating a dynamic partial callable object in Python

我在 YAML 配置文件中定义了一个正则表达式。

为了方便起见,我将在这里使用字典:

rule_1 = {
    'kind': 'regex',
    'method': 'match',
    'args': None,
    'kwargs': {
        'pattern': "[a-z_]+",
        'flags': re.X,
        'string': 's_test.log',
    }
}

我希望能够在函数中解析该规则。

如果我们假设这些值不变,那么我可以做这样的事情。

正在导入模块:

import re
from operator import methodcaller
from functools import partial

我下面的第一个函数能够适应所使用的正则表达式方法的变化:

def rule_parser_re_1(*, kind, method, args=None, kwargs=None):
    if args is None: args = []
    if kwargs is None: kwargs = {}
    mc = methodcaller(method, **kwargs)
    return mc(re)

它按预期工作:

>>> rule_parser_re_1(**rule_1)
<re.Match object; span=(0, 6), match='s_test'>

现在,假设在定义配置字典时我没有要解析的字符串。

例如假设它是文件中的特定行,只能在运行时访问。

myfile = """
first line
second line
third line
"""

io_myfile = io.StringIO(myfile)

content = io_myfile.readlines()

我的第二条规则,其中“line_number”(即 int)替换“字符串”(即 str)。

rule_2 = {
    'kind': 'regex',
    'method': 'match',
    'args': None,
    'kwargs': {
        'pattern': "[a-z_]+",
        'flags': re.X,
        'line_number': 2,
    }
}

我的理解是我应该可以通过定义部分 rule_parser_re 函数来解决这个问题。 这样的函数应该像用 patternflags 调用的原始函数一样,但没有 string.

我想出了以下功能:

def rule_parser_re_2(*, kind, method, args=None, kwargs=None):
    if args is None: args = []
    if kwargs is None: kwargs = {}

    if kind == 'regex' and method == 'match':
        pa = partial(re.match, pattern=kwargs['pattern'], flags=kwargs['flags'])
        return pa

这似乎也能正常工作:

>>> r2 = rule_parser_re_2(**rule_2)
>>> r2(string=content[2])
<re.Match object; span=(0, 6), match='second'>

尽管如此,我发现上述实现存在两个可维护性问题:

  1. 我正在使用那个 if 语句,它迫使我为我想要支持的每个 re 方法修改函数;
  2. 我需要明确指定参数,而不是仅仅解压“**kwargs”

我的aims/doubts:

谢谢!

为什么不直接调用函数,只使用 kwargs,而是使用配置来驱动大部分 kwargs/args 内容,而不是尝试部分调用程序或方法调用程序?我为此使用了一个闭包,准备好的“记住”了配置。

请注意,我的最终调用并不关心 string 是 re.match 的关键字。我发现您的示例与正则表达式特定内容有相当多的耦合,其中一些内容如 re.X 无法在不进行进一步操作的情况下存储在 YAML 中。

同样,partial/methodcaller调用函数的方式不应该关心值来自文件中的哪个行号,耦合太多。如果必须,请在配置中添加其他内容, 而不是 kwargs 下的 ,用于处理运行时参数获取。

所以我做了一些改动。我相信,但你可能不同意,当调用解析规则时,调用函数不必知道参数是如何调用的。嗯,也就是说,除非你的规则只是风格上的正则表达式,在这种情况下你不需要在配置中使用 kind

这是替代方法的快速、不完美的草图。详细信息将取决于您希望如何使用它。

我还对 *args 处理进行了投注,但如果需要,它可能会以相同的方式执行。

import importlib

rule_1 = {
    'kind': 're',
    'method': 'match',
    'args': None,
    "positional_mapper" : ["string"],
    'kwargs': {
        'pattern': "[a-z_]+",
        # I don't know how this would be stored in a YAML
        # 'flags': re.X,
        'string': 's_test.log',
    }
}

rule_2 = {
    'kind': 're',
    'method': 'match',
    'args': None,
    "positional_mapper" : ["string"],
    'kwargs': {
        'pattern': "[a-z_]+",
    }
}


def prep(config):

    mod = app_urls = importlib.import_module(config["kind"])
    f = getattr(mod, config["method"])

    pre_args = config.get("args") or []
    pre_kwargs = config.get("kwargs") or {}
    positional_mapper = config["positional_mapper"]

    def prepped(*args, **kwargs):

        kwargs2 = pre_kwargs.copy()

        for value, argname in zip(args, positional_mapper):
            kwargs2[argname] = value
        kwargs2.update(**kwargs)

        return f(**kwargs2)

    return prepped


parsed_rule1 = prep(rule_1)

print ("#1", parsed_rule1("second line"))
print ("#2", parsed_rule1())

parsed_rule2 = prep(rule_2)
print ("#3", parsed_rule2("second line"))
print ("#3.5", parsed_rule2(string="second line"))
print ("#4", parsed_rule2())

正如预期的那样,调用 #4 扼流圈,因为它缺少要放入 string 的参数。

#1 <re.Match object; span=(0, 6), match='second'>
#2 <re.Match object; span=(0, 6), match='s_test'>
#3 <re.Match object; span=(0, 6), match='second'>
#3.5 <re.Match object; span=(0, 6), match='second'>
Traceback (most recent call last):
  File "test_299_dyn.py:57", in <module>
    print ("#4", parsed_rule2())
  File "test_299_dyn.py:44", in prepped
    return f(**kwargs2)
TypeError: match() missing 1 required positional argument: 'string'

您不必创建偏函数。您可以先编译模式,然后调用所需的方法:

rule_2 = {
    'kind': 'regex',
    'method': 'match',
    'args': None,
    'kwargs': {
        'pattern': "[a-z_]+",
        'flags': re.X,
        # 'line_number': 2, commented out this line
    }
}

content = ['', 'first line', 'second line', 'third line']

pattern = re.compile(**rule_2['kwargs'])
method = getattr(pattern, rule_2['method'])
>>> method(content[2])
<re.Match object; span=(0, 6), match='second'>

如果你想保留行号,你可以这样做:

rule_2 = {
    'kind': 'regex',
    'method': 'match',
    'args': None,
    'kwargs': {
        'pattern': "[a-z_]+",
        'flags': re.X,
        'line_number': 2,
    }
}

content = ['', 'first line', 'second line', 'third line']
def rule_parser_re(*, kind, method, args=None, kwargs=None):
    copied_kwargs = kwargs.copy()
    line_number = copied_kwargs.pop('line_number')
    pattern = re.compile(**copied_kwargs)
    method = getattr(pattern, method)
    return method, line_number
    
parser, line_number = rule_parser_re(**rule_2)
>>> parser(content[line_number])
<re.Match object; span=(0, 6), match='second'>

由于您的第二个模式与 re.match 的签名不匹配(),您需要编写自己的函数。它可以使用带有命名参数的包装函数来 adapt 接口(尽管如果您关心 args,这涉及为您发明的 line_number 参数固定一个位置)。它还可以使用 getattr,这相当于 operator.methodcaller:

的某些琐碎用法
def rule2(kind,method,args,kwargs):
  return _rule2(getattr(re,method),*args or (),**kwargs or {})
def _rule2(f,pattern,line_number,flags):
  return lambda content: f(pattern,content[line_number],flags)

请注意,content 是保留的参数,因为只有行号会使文件内容未知;因为它不是直接底层函数的参数,所以partial不是这里的正确工具。