在 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
函数来解决这个问题。
这样的函数应该像用 pattern
和 flags
调用的原始函数一样,但没有 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'>
尽管如此,我发现上述实现存在两个可维护性问题:
- 我正在使用那个
if
语句,它迫使我为我想要支持的每个 re
方法修改函数;
- 我需要明确指定参数,而不是仅仅解压“**kwargs”
我的aims/doubts:
- 有没有办法让上面的功能更加动态和可维护?
functools.partial()
和 operator.methodcaller()
是完成这项工作的正确工具吗?
- 如果可以,可以合并吗?
谢谢!
为什么不直接调用函数,只使用 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
不是这里的正确工具。
我在 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
函数来解决这个问题。
这样的函数应该像用 pattern
和 flags
调用的原始函数一样,但没有 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'>
尽管如此,我发现上述实现存在两个可维护性问题:
- 我正在使用那个
if
语句,它迫使我为我想要支持的每个re
方法修改函数; - 我需要明确指定参数,而不是仅仅解压“**kwargs”
我的aims/doubts:
- 有没有办法让上面的功能更加动态和可维护?
functools.partial()
和operator.methodcaller()
是完成这项工作的正确工具吗?- 如果可以,可以合并吗?
谢谢!
为什么不直接调用函数,只使用 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
不是这里的正确工具。