You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Python实现类似SDF中的compose组合子?

问题描述

自学MIT 6.5151课程时,需要实现《软件设计灵活性》(SDF)中的compose组合子,但遇到了Python多返回值的歧义问题:

  • 最初用list传递多返回值,但无法区分函数返回单个list和函数返回多个值用list封装的情况;
  • 改用tuple后,同样无法区分函数返回单个tuple和**函数返回多个值(自动打包为tuple)**的情况。

Scheme通过values和call-with-values原生解决了这个问题,但Python没有类似机制,如何让compose正确处理这两种场景?

初始实现(list版)

import inspect
def compose(f, g):
  g_arity=len(inspect.signature(g).parameters)
  def compose_composition(*arguments):
    try:
      if len(arguments) != g_arity:
        print("compose Arg number error")
      g_result=g(*arguments)
      if type(g_result) is list:
        print("g_result",g_result) 
        return f(*g_result)
      else:
        return f(*[g_result])
    except:
      pass
  return compose_composition

assert compose(lambda x:x+2, lambda x:x*x)(2)==2*2+2
print("compose test_2",compose(lambda x:x+2, lambda x:x*x)(2,2)) # 预期报错
assert compose(lambda x:["foo",x], lambda x:["bar",x])(2)==["foo",["bar",2]]

改进实现(tuple版)

def compose(f, g):
  g_arity=len(inspect.signature(g).parameters)
  def compose_composition(*arguments):
    try:
      if len(arguments) != g_arity:
        print("compose Arg number error")
      if type(g(*arguments)) is tuple:
        return f(*g(*arguments))
      else:
        return f(*(g(*arguments),))
    except:
      pass
  return compose_composition
compose(lambda x:x, lambda x,y:(x,y))(2,3) # 预期报错
assert compose(lambda x,y:x+y, lambda x,y:(x,y))(2,3)==5

Scheme参考实现

(define (compose f g)
  (define (the-composition . args)
    (call-with-values (lambda () (apply g args))
      f))
  (restrict-arity the-composition (get-arity g)))

解决方案

核心思路是显式标记多返回值,模拟Scheme中values的语义,彻底消除歧义。

方案1:自定义多值标记类(推荐)

通过一个专用类封装多返回值,让compose能明确区分“返回多个值”和“返回单个容器对象”。

代码实现

import inspect

# 自定义多值标记类,模拟Scheme的values
class Values:
    def __init__(self, *args):
        self.args = args

def compose(f, g):
    g_arity = len(inspect.signature(g).parameters)
    def composition(*arguments):
        # 参数个数校验
        if len(arguments) != g_arity:
            raise ValueError(f"compose: 预期{g_arity}个参数,实际传入{len(arguments)}个")
        g_result = g(*arguments)
        # 判断是否为显式多返回值
        if isinstance(g_result, Values):
            return f(*g_result.args)
        # 否则作为单个参数传递
        else:
            return f(g_result)
    return composition

# 测试用例
# 1. 单值函数组合
assert compose(lambda x: x+2, lambda x: x*x)(2) == 2*2 + 2

# 2. 返回单个tuple(不拆分)
assert compose(lambda x: x[0]+x[1], lambda x: (x, x))(2) == 4

# 3. 显式多返回值(拆分传递)
assert compose(lambda x,y: x+y, lambda x,y: Values(x, y))(2, 3) == 5

# 4. 返回单个list(不拆分)
assert compose(lambda x: ["foo", x], lambda x: ["bar", x])(2) == ["foo", ["bar", 2]]

# 5. 参数个数错误(触发异常)
try:
    compose(lambda x: x+2, lambda x: x*x)(2, 2)
except ValueError as e:
    print(f"正确捕获异常: {e}")

方案2:基于函数签名推断(有局限性)

如果不想引入自定义类,可以尝试通过f的参数个数推断是否拆分,但仅适用于参数个数固定且无歧义的场景:

import inspect

def compose(f, g):
    g_arity = len(inspect.signature(g).parameters)
    f_sig = inspect.signature(f)
    f_params = list(f_sig.parameters.values())
    # 判断f是否接受可变参数
    has_var_args = any(p.kind == p.VAR_POSITIONAL for p in f_params)
    
    def composition(*arguments):
        if len(arguments) != g_arity:
            raise ValueError(f"compose: 预期{g_arity}个参数,实际传入{len(arguments)}个")
        g_result = g(*arguments)
        if has_var_args:
            # 可变参数场景直接传递(仍有歧义)
            return f(*g_result) if isinstance(g_result, (list, tuple)) else f(g_result)
        else:
            f_arity = len(f_params)
            # 仅当tuple长度匹配f的参数个数时才拆分
            if isinstance(g_result, tuple) and len(g_result) == f_arity:
                return f(*g_result)
            else:
                return f(g_result)
    return composition

注意:此方案存在误判风险,比如函数返回的tuple长度恰好等于f的参数个数时,会被错误拆分。


方案对比
方案优点缺点
自定义Values类语义明确,无歧义,最可靠需要显式标记多返回值
函数签名推断无需额外类型存在歧义场景,适用性有限

内容的提问来源于stack exchange,提问作者An5Drama

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 04:10:04