如何用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
相关产品推荐
相关产品推荐

