我们定义了一个函数treecomp,它通过将L 和T 作为单独的参数,返回根据有根树T 结构化的函数列表L 的组合:
F = treecomp(T, L)
与迄今为止提出的其他解决方案不同,它不会因为不必要的簿记而变得复杂,例如跟踪叶子或参数的数量(此外,装饰器可以更好地处理)。
treecomp 的简单构造
treecomp 的直接实现如下:它仅生成树组合的符号(字符串)表达式。然后,对参数的求值只是将它们插入并求值结果表达式的问题。
这个幼稚的想法可以使用相当基本的数据结构来实现:树和函数的列表,以及函数标记树的简单类。 (命名元组也可以。但是,通过使用具有特殊比较方法的类,我们可以编写更多语义自然代码。)
数据结构
将有根树编码为 flat 列表的最经济的编码是“节点地址”列表。在对@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): # how "high" a node is above the root
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 的简单两步机制在下面实现。通过标准化 SymbNode 列表的顺序,我们可以通过在向上移动时简单地“剥离”树的每一层来构建符号表达式。
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: # symbtree is a leaf node
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 之间的整数中抽取的 x0、...、x5 的 1000 个随机样本上。