【问题标题】:Partial functions: how should I normalize the output type of two partials?偏函数:我应该如何标准化两个偏函数的输出类型?
【发布时间】:2019-11-23 05:45:02
【问题描述】:

我遇到了这个问题...我有一些代码使用functools.partial,这样我就可以将无参数函数传递给后续函数。我所有的例子都来自random并返回整数,例如:

def main():
    random.seed()
    adist = functools.partial(random.uniform, 20, 2000) # no parameter function that returns int
    tdist = functools.partial(random.randrange, 4096) # no parameter function returning int

这些随后在构造函数中使用,如下所示:

for n in range(4):
    random.seed()
    env = simpy.Environment()  # Create the SimPy environment
    line = runSim(env, adist, sdist, tdist) # dists expected to be ints

并且每次执行 runSim 时,我们都会根据partial 定义中描述的分布得到不同的随机值。我接下来要做的只是定义另一个分布偏函数,但这个函数使用random.choices(),它返回一个列表而不是一个整数——如果我们严格这样做,它会使该版本与需要数字类型的其余代码不兼容)

sdist = functools.partial(random.choices, population=[64, 536, 1300, 1500], weights=[0.4, 0.02, 0.02, 0.2])

我似乎想不通的是如何构建或修改部分函数,​​使其也返回一个整数。即使它返回一个列表,我也不能直接为可调用对象下标。我认为可能有一些方法可以组合部分(嵌套部分?部分部分?)但我无法弄清楚。

【问题讨论】:

  • 这就是像 这样的函数组合运算符可以派上用场的地方:sdist = itemgetter(0) ∘ partial(random.choices, ...)。您可以使用lambda 表达式更详细地编写此内容:sdist = lambda : itemgetter(0)(random.choices(...))

标签: python functional-programming partials


【解决方案1】:

您想要的是 function composition,在 Python 中实现此目的的一种方法如下:

import functools
import random

from operator import itemgetter

random.seed(42)


def compose(f, g):
    def composition():
        return f(g())

    return composition


sdist = functools.partial(random.choices, population=[64, 536, 1300, 1500], weights=[0.4, 0.02, 0.02, 0.2])

new_sdist = compose(itemgetter(0), sdist)

print(new_sdist())

输出

536

【讨论】:

    【解决方案2】:

    不用partial,只需编写一个不带参数并以所需形式返回所需值的包装函数:

    def sdist():
        return random.choices(population=[64, 536, 1300, 1500], weights=[0.4, 0.02, 0.02, 0.2])[0]
    

    如果你想在一行中保持精简,只需使用 lambda 关键字:

    sdist = lambda: random.choices(population=[64, 536, 1300, 1500], weights=[0.4, 0.02, 0.02, 0.2])[0]
    

    从你的文本和 cmets 中,它看出你正在探索部分,并决心继续它:部分是可读的,并且在一定程度上保证覆盖所有极端情况,并且可能会在函数的元数据上暴露一些东西- 被调用,但它与简单地将原始函数包装在另一个调用中没有太大区别 - 就像可以这样做:

    def partial(func, *args, **kw):
        def partial_callable(*more_args, **more_kw):
            final_args = args + more_args
            final_kw = kw.copy()
            final_kw.update(more_kw)
            return func(*final_args, **final_kw)
        return partial_callable
    

    换句话说:partial 是通过将您的可调用对象包装在另一个具有您想要的签名的可调用对象中可以做的事情的一个子集。而在 partial 中受限的部分正是,虽然它允许您绑定输入参数并完全自定义想要的参数,但它返回包装函数的原始值。

    如果你想让你的代码保持相同的外观,那么你可以定义一个“超级部分”,作为第二个位置参数,另一个可调用函数将转换主函数的返回值。 (我不建议将它作为命名参数,因为它可能与可调用中的命名参数冲突):

    def partial_normalize(func, normalizer, /, *args, **kw):
        def partial_callable(*more_args, **more_kw):
            final_args = args + more_args
            final_kw = kw.copy()
            final_kw.update(more_kw)
            result = func(*final_args, **final_kw)
            return normalizer(result) if normalizer else result
        return partial_callable
    

    现在你可以这样做了:

    from operator import itemgetter
    sdist = partial_normalize(random.choices, itemgetter(0), population=[64, 536, 1300, 1500], weights=[0.4, 0.02, 0.02, 0.2])
    

    operator.itemgetter(N) 返回一个可调用对象,该可调用对象将检索传递给它的参数的第 N 项(相当于 mylist[N] 语法)

    (参数列表中的/ 是Python 3.8 的新语法,表示前两个参数必须是位置参数,并释放名称funcnormalizer 以用作包装函数的关键字参数. 对于 3.7 及以下的 Python 版本,请忽略它)

    【讨论】:

    • 我曾在其他地方使用过您的第一个示例,但试图理解部分用法而不是“仅仅”解决问题。如果这是最pythonic的方法之一是使用包装器,那将是有道理的......似乎部分基础设施在这里更有意义。
    • 我写了答案的第二部分,我希望能更好地解释partial 的作用,并澄清我为什么说你可以将可调用对象包装在 lambda 中(如果你可以使用更高级的部分)想要保持通用性)
    猜你喜欢
    • 2018-10-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-14
    • 2021-10-16
    • 1970-01-01
    相关资源
    最近更新 更多