我们定义一个名为
treecomp
的函数,它根据一棵根树
T
的结构,将一组函数列表
L
合成。该函数接受
L
和
T
作为分开的参数。请保留 HTML 标签。
F = treecomp(T, L)
与迄今提出的其他解决方案不同,它不会被不必要的簿记(如跟踪叶子或参数数量,这些最好由装饰器处理)所复杂。
treecomp
的简单构造
treecomp
的一个简单实现如下:它仅生成树组合的符号(字符串)表达式。然后,将其插入并评估生成的表达式就是一件简单的事情。
这个天真的想法可以使用相当基本的数据结构来实现:用于树和函数的列表,以及用于函数标记树的简单类。(命名元组也可以做到。但是,通过使用具有特殊比较方法的类,我们可以编写更加语义化自然的代码。)
数据结构
作为“平面”列表,根树的最经济编码方式是作为“节点地址”的列表。在对@JeD的评论中,我暗示可以通过“绘制”树来完成此操作:
T = [(0,),
(0, 0),
(0, 0, 0),
(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 0, 2),
(0, 1),
(0, 1, 0),
(0, 1, 0, 0),
(0, 1, 1),
(0, 1, 1, 0), (0, 1, 1, 1)]
在这里,
(0,)
是对应于
a0
的节点,
(0, 0)
是对应于
b0
的节点,
(0, 1)
是对应于
b1
的节点,依此类推,就像书中章节的编号一样。最长(或“最高”)的元组是叶子节点。
函数列表
L
可以按照
T
中节点的顺序给出一个匹配列表。
L = [a0, b0, c0, e0, e1, e2, b1, d0, f0, d1, g0, g1]
由于树T的节点是由L中的函数标记的,因此为此需要一个数据结构。我们定义了一个类来记录节点的地址和标记它的函数的字面名称;其方法实现相对于树的偏序比较(其中根是最小元素):
class SymbNode:
'''Class that records a node's address and symbol.'''
def __init__(self, addr, symb):
self.addr = addr
self.symb = symb
def __len__(self):
return len(self.addr)
def _compare(self, other, segment):
return self.addr == other.addr[:segment]
def __le__(self, other):
return self._compare(other, segment=len(self))
def begets(self, other):
return self._compare(other, segment=-1)
实现
treecomp
的简单两步机制如下所示。通过规范化SymbNodes列表的顺序,我们可以通过简单地“剥离”树的每一层来构建符号表达式。
from functools import partial
from operator import attrgetter
def treecomp(tree, funcs):
'''Returns the composition of a tree of functions.'''
symbtree = makesymbtree(tree, funcs)
symbexp = makesymbexp(symbtree)
return partial(evalsymbexp, symbexp=symbexp)
FUNC_CALL = '{func}({{}})'
def makesymbtree(tree, funcs):
'''Returns the symbolic expression of a tree composition.'''
symbols = [FUNC_CALL.format(func=func.__name__) for func in funcs]
symbtree = sorted((SymbNode(*x) for x in zip(tree, symbols)),
key=attrgetter('addr'))
symbtree.sort(key=len)
return symbtree
def makesymbexp(symbtree):
root = symbtree[0]
if len(symbtree) == 1:
return root.symb
symbargs = [makesymbexp(subsymbtree(symbtree, root=node))
for node in symbtree if root.begets(node)]
return root.symb.format(','.join(symbargs))
def subsymbtree(symbtree, root):
subsymbtree = [node for node in symbtree if root <= node]
return subsymbtree
ARGS = 'args[{idx}]'
def evalsymbexp(symbexp, *args):
'''Returns the evaluation of a symbolic expression on arguments.'''
argnames = [ARGS.format(idx=str(n)) for n, _ in enumerate(args)]
return eval(symbexp.format(*argnames))
验证
由于 treecomp
的分隔性,我们只需要验证函数 makesymbexp
生成正确的符号表达式,以及函数 evalsymbexp
正确评估符号表达式。
函数evalsymbexp
(基本上只有一行)应该采用字符串模板并插入参数名称'args [0]'
,'args [1]'
等,然后评估结果。它显然做到了这一点。
至于 makesymbexp
,在没有正式证明(我们避免这样做)的情况下,我们可以通过检查其在一些测试数据上的输出来获得其正确性。例如,考虑以下函数:
def D(x): return 2*x
def M(x): return -x
def S(*xs): return sum(xs)
a0 = S
b0, b1 = D, S
c0, d0, d1 = S, D, S
e0, e1, e2, f0, g0, g1 = D, M, D, M, D, M
使用上述的
T
和
L
,我们可以检查是否得到正确的符号表达式:
makesymbexp(makesymbtree(T, L))
确实会返回字符串
'S(D(S(D({}),M({}),D({}))),S(D(M({})),S(D({}),M({}))))'
为了检查treecomp
对evalsymbexp
的委托,作为一个部分函数,我验证了以下值:
F = treecomp(T, L)
F(x0, x1, x2, x3, x4, x5)
同意价值观
a0(b0(c0(e0(x0), e1(x1), e2(x2))), b1(d0(f0(x3)), d1(g0(x4), g1(x5))))
从-100到100之间的整数中,随机抽取1000个样本x0
,…,x5
。
L
和T
都可以表示为词典列表:L = [a0, b0, b1, c0, ... , g1]
和T = [(0,), (0,0), (0,1), (0,0,0), ... , (0,1,1,1)]
。(类似于书中的章节编号。)然后,带有函数标签的树是列表TL = list(zip(T, L))
。因此,在编写treecomp
时,一种方法是展开TL
中的(隐式)树结构并递归地组合函数。但我不确定如果我选择将T
和L
作为列表是否已经出现了问题!尽管这看起来很自然。 - egnha