提问人:ruohola 提问时间:11/5/2023 最后编辑:ruohola 更新时间:11/22/2023 访问量:419
如何通过纯粹使用Python的标准库来组合函数?
How to compose functions through purely using Python's standard library?
问:
Python 的标准库非常庞大,我的直觉告诉我,它一定有一种方法可以做到这一点,但我就是想不通。这纯粹是出于好奇心和学习目的:
我有两个简单的函数:
def increment(x):
return x + 1
def double(x):
return x * 2
我想将它们组合成一个新功能.我当然可以简单地这样做:double_and_increment
double_and_increment = lambda x: increment(double(x))
但我也可以用一种更复杂但可能更“符合人体工程学的可扩展性”的方式来做到这一点:
import functools
double_and_increment = functools.partial(functools.reduce, lambda acc, f: f(acc), [double, increment])
以上两种方法都可以正常工作:
>>> double_and_increment(1)
3
现在的问题是,标准库中是否有工具可以在没有任何用户定义的 lambda、常规函数或类的情况下实现组合。
第一种直觉是将 functools.reduce
调用中的定义替换为 operator.call
,但不幸的是,这会以相反的顺序获取参数:lambda acc, f: f(acc)
>>> (lambda acc, f: f(acc))(1, str) # What we want to replace.
>>> '1'
>>> import operator
>>> operator.call(str, 1) # Incorrect argument order.
>>> '1'
我有一种预感,使用仍然是完成组合的方法,但就我而言,我一辈子都想不出摆脱用户定义的 lambda 的方法。functools.reduce
一些开箱即用的方法让我接近:
import functools, operator
# Curried form, can't figure out how to uncurry.
functools.partial(operator.methodcaller, '__call__')(1)(str)
# The arguments needs to be in the middle of the expression, which does not work.
operator.call(*reversed(operator.attrgetter('args')(functools.partial(functools.partial, operator.call)(1, str))))
已经浏览了所有现有问题,但它们完全不同,并且依赖于使用用户定义的函数和/或 lambda。
答:
好吧,既然你说
我想“滥用”该语言,只使用标准库中的现有定义
从 Python 3.12 开始,测试套件恰好包含您想要的小工具:
import functools
import operator
from test.test_zipfile._path._functools import compose
increment = functools.partial(operator.add, 1)
double = functools.partial(operator.mul, 2)
increment_and_double = compose(increment, double)
print(increment_and_double(10))
(我通过本地 CPython 结账时的战略发现了这一点。ag compose
评论
functools
def compose(fa, fb): return lambda x: fa(fb(x))
虽然@AKX CPython 代码树中找到完美实现 OP 所需的函数组合功能的函数很酷,但它实际上并不属于问题规则所要求的标准库,原因如下:test.test_zipfile._path._functools.compose
- 它属于 Python 语言的 CPython 实现的测试套件中的帮助程序模块。
- 测试套件不是该语言标准库的一部分;它只是验证语言及其标准库的特定实现的代码。
- 测试套件(更不用说测试套件中的任何帮助程序功能)可以随时删除,而无需任何正常的高级弃用警告正当程序。
- Python 的其他实现不需要包含任何 CPython 的测试套件以符合 Python 的规范。
因此,如果没有 CPython 3.12 测试套件中不属于标准库的辅助函数,我相信 OP 在评估中确实是正确的,即 Python 的标准库中没有开箱即用的工具可以实现函数组合。
但是,这并不意味着我们不能通过修改现有工具来实现它,因为 OP 的规则只是使用“标准库中的工具,这些工具允许在没有任何用户定义的 lambda、常规函数或类的情况下实现组合”。
由于 OP 几乎已经得到了它:
double_and_increment = partial(reduce, lambda acc, f: f(acc), [double, increment])
和:
>>> (lambda acc, f: f(acc))(1, str) # What we want to replace.
>>> '1'
>>> import operator
>>> operator.call(str, 1) # Incorrect argument order.
>>> '1'
这里真正的问题是,我们如何修改标准库中的现有函数,使其变为:
def rcall(value, obj):
return obj(value)
为此,让我们看一下上述函数的字节码,以及定义参数的代码对象的相关属性:
>>> import dis
>>> def call(value, obj):
... return obj(value)
...
>>> dis.dis(call)
1 0 RESUME 0
2 2 PUSH_NULL
4 LOAD_FAST 1 (obj)
6 LOAD_FAST 0 (value)
8 PRECALL 1
12 CALL 1
22 RETURN_VALUE
>>> c = call.__code__
>>> c.co_varnames
('value', 'obj')
>>> c.co_argcount
2
>>> c.co_nlocals
2
>>>
这并不奇怪。一个简单的函数体,将第二个参数 () 和第一个参数 () 加载到堆栈上,然后使用堆栈中的可调用对象和参数进行调用,最后将堆栈顶部的值返回给调用方。obj
value
现在,让我们在标准库中找到一个类似的简单函数,该函数接受一两个参数并用它/它们进行调用,因此可以更轻松地将其修改为我们想要的函数。事实证明,operator.abs
就是这样一个函数,它接受一个参数并对内置函数进行包装调用:_abs
def abs(a):
"Same as abs(a)."
return _abs(a)
我们想拆解它进行比较,但不幸的是,如果我们尝试访问 ,你会得到一个错误:operator.abs.__code__
>>> import operator
>>> operator.abs.__code__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'builtin_function_or_method' object has no attribute '__code__'. Did you mean: '__call__'?
>>>
这是因为 CPython 的模块实现包括一个模块,该模块用 C 语言实现的函数覆盖了所有纯 Python 函数,并在 operator.py
中有一个块:operator
_operator
operator.py
try
try:
from _operator import *
except ImportError:
pass
用 C 语言实现的函数没有对象,因此无法修改。我们需要的是纯 Python 版本的 ,然后再被 覆盖。但是我们如何避免覆盖呢?好吧,我们可以先自己导入模块,然后从中删除属性,以便将修改后的模块缓存在其中,以便在导入时,它得到的是我们的修改版本,而不是其中:__code__
operator.call
_operator.call
_operator
call
sys.modules
operator.py
_operator
call
>>> try: # other Python implementations may not have _operator.py
... import _operator
... del _operator.call
... except ImportError:
... pass
...
>>> import operator
>>> operator.call.__code__
<code object call at 0x000001F68F4FADB0, file "C:\python311\Lib\operator.py", line 226>
伟大!现在我们终于可以看一下 代码对象的字节码和相关属性了:operator.abs
>>> dis.dis(operator.abs)
71 0 RESUME 0
73 2 LOAD_GLOBAL 1 (NULL + _abs)
14 LOAD_FAST 0 (a)
16 PRECALL 1
20 CALL 1
30 RETURN_VALUE
71 0 RESUME 0
>>> c = operator.abs.__code__
>>> c.co_varnames
('a',)
>>> c.co_argcount
1
>>> c.co_nlocals
1
>>>
可以看出,我们需要修改才能变成我们想要的函数对象,只需将指令替换为(以指示 CALL
的常规函数调用)和(加载第二个参数,可调用对象)以及 ,并添加第二个参数。operator.abs
LOAD_GLOBAL
PUSH_NULL
LOAD_FAST 1
co_varnames
co_argcount
co_nlocals
obj
要从现有的代码对象中获取修改后的代码对象,我们可以调用它的 replace
方法:operator.abs
try:
import _operator
del _operator.abs
except ImportError:
pass
from operator import abs as rcall
from opcode import opmap
from functools import partial, reduce
code = bytearray(rcall.__code__.co_code)
code[code.find(opmap['LOAD_GLOBAL']):code.find(opmap['LOAD_FAST'])] = \
opmap['PUSH_NULL'], 0, opmap['LOAD_FAST'], 1
rcall.__code__ = rcall.__code__.replace(
co_code=bytes(code),
co_varnames=('value', 'obj'),
co_argcount=2,
co_nlocals=2
)
print(rcall(1, str))
这将正确输出:
1
因此,通过将修改后的 in 插入到 OP 的关闭尝试中,实现 OP 想要的复合函数就变得微不足道了:operator.call
def increment(x):
return x + 1
def double(x):
return x * 2
double_and_increment = partial(reduce, rcall, [double, increment])
print(double_and_increment(1))
这将输出:
3
演示: 这里
评论
test
PRECALL
KeyError: 'PRECALL'
PRECALL
正如我的另一个答案中提到的,我不同意 @AKX 发现的测试套件应被视为 OP 规则的标准库的一部分。
事实证明,在研究要修改我的另一个答案的现有函数时,我发现模块中有这个辅助函数_int_to_enum
,它完美地实现了具有单个参数的可调用对象,但参数颠倒了,正是 OP 想要的,并且从 Python 3.5 开始可用:signal
operator.call
def _int_to_enum(value, enum_klass):
"""Convert a numeric value to an IntEnum member.
If it's not a known member, return the numeric value itself.
"""
try:
return enum_klass(value)
except ValueError:
return value
因此,我们可以简单地重新利用/滥用它:
from signal import _int_to_enum as rcall
from functools import reduce, partial
def increment(x):
return x + 1
def double(x):
return x * 2
double_and_increment = partial(reduce, rcall, [double, increment])
print(double_and_increment(1))
这将输出:
3
演示: 这里
评论
_int_to_enum
compose = lambda *fs: reduce(lambda f, g: lambda x: f(g(x)), fs)
double_and_incrmeent = compose(increment, double)
评论
compose2
lambda