函数层次结构

时间:2016-03-25 15:19:27

标签: python functional-programming hierarchy function-composition

是否有一种规范的方式来表达一个由根函数树组成的函数?

这是一个具体的例子,我的意思是“功能树的组成”。获取一个根节树,其节点由函数标记,如下所示:

enter image description here

节点上的每个函数都是其子节点上函数的组合。与树相关联的函数本身就是组合

F = a0(b0(c0(e0, e1, e2)), b1(d0(f0), d1(g0, g1)))

更明确地说,F是6个参数的函数,由叶子上的函数计算:

F(x0, ... , x5) == a0(b0(c0(e0(x0), e1(x1), e2(x2))),
                      b1(d0(f0(x3)), d1(g0(x4), g1(x5))))

一般问题

  • 给定一个根树T,以及与L节点对应的函数列表T,是否有规范方式来编写函数参数FT的{​​{1}}返回L根据树L构建的函数的组合?

通过这种方式,组合的“布线” - 树T - 与其内部“组件” - 列表T分开。 “规范”解决方案应特别包括LT的表示,这些表示自然适应此问题。

我怀疑这个问题在函数式编程语言中有一个简单的解决方案,但理想情况下我希望有一个动态类型命令式语言的解决方案,如 Python ,类似

L

附录

与此同时,我想出了自己的解决方案(发布在下面)。

虽然我对其经济和概念简单性感到满意,但我仍然对其他本质上不同的方法感兴趣,特别是那些利用Python中缺乏或支持不足的另一种语言的优势的方法。

预感

使用适当的数据结构 - 基本上不会重现所需的输出! - 功能编程习惯用法应该能够实现非常短的解决方案。

5 个答案:

答案 0 :(得分:2)

是的,所以这对我来说听起来很有趣。所以我试了一下,这就是结果。

class Node(object):
    def __init__(self, parents, fn):
        self.parents = parents
        self.fn = fn

    def get_number_of_args(self):
        if not self.parents:
            return 1
        if not hasattr(self, '_cached_no_args'):
            self._cached_no_args = sum(
                parent.get_number_of_args() for parent in self.parents
            )
        return self._cached_no_args

    def compose(self):
        if not self.parents:
            def composition(*args):
                return self.fn(*args)
            return composition

        fns = []
        fns_args = []
        for parent in self.parents:
            fns.append(parent.compose())
            fns_args.append(parent.get_number_of_args())
        number_of_args = sum(fns_args)
        length = len(self.parents)

        def composition(*args):
            if len(args) != number_of_args:
                raise TypeError
            sub_args = []
            last_no_args = 0
            reached_no_args = 0
            for i in range(length):
                fn = fns[i]
                no_args = fns_args[i]
                reached_no_args += no_args
                args_cut = args[last_no_args: reached_no_args]
                sub_call = fn(*args_cut)
                sub_args.append(sub_call)
                last_no_args = no_args
            return self.fn(*sub_args)

        return composition

您没有指定实现树结构的方式,因此我将节点和函数合并为一个结构(您可以自己进行映射)。现在用法:

>>> def fn(x):
...     return x
>>> def fn2(x):
...    return 1
>>> def add(x, y):
...    return x + y
>>> n1 = Node([], fn)
>>> n2 = Node([], fn2)
>>> n3 = Node([n1, n2], add)
>>> fn = n3.compose()
>>> print(fn(5, 7))
6

正如所料。随意测试它(我实际上没有在更深的树上尝试过它),如果你发现任何问题,请告诉我。

答案 1 :(得分:2)

我们定义一个函数treecomp,它通过取L和{{1}来返回根据根树T构造的函数列表L的组合。作为单独的参数:

T

与目前提出的其他解决方案不同,它不会因不必要的簿记而变得复杂,例如跟踪叶子或参数的数量(除了装饰者更好地处理)。

F = treecomp(T, L)

的简单构造

treecomp的简单实现如下:它只生成树组合的符号(字符串)表达式。对参数的评估只是将它们插入并评估结果表达式的问题。

这个天真的想法可以使用相当基本的数据结构来实现:树和函数的列表,以及功能标记树的简单类。 (Namedtuples也可以。但是,通过使用具有特殊比较方法的类,我们可以编写更多语义自然代码。)

数据结构

作为 flat 列表的根树的最经济的编码是"节点地址的列表。"在对@JeD的评论中,我暗示这可以通过" drawing"树:

treecomp

此处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)对应的节点等等,就像书中各节的编号一样。最长的(或#34;最高的#34;)元组是叶子。

然后可以将函数列表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) 的简单两步机制在下面实现。通过规范化SymbNode列表的顺序,我们可以简单地通过"剥离"来构建符号表达式。当我们向上移动树的每一层时。

treecomp

验证

由于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)

在1000个随机样本中a0(b0(c0(e0(x0), e1(x1), e2(x2))), b1(d0(f0(x3)), d1(g0(x4), g1(x5)))) ,...,x0从-100到100之间的整数绘制。

答案 2 :(得分:1)

这是我做过的一个简单例子:

from collections import deque

class Node(object):
    def __init__(self, children, func):
        self.children = children
        if children:
            self.leaf_count = sum(c.leaf_count for c in children)
        else:
            self.leaf_count = 1  # It is a leaf node.

        self.func = func

    def __call__(self, *args):
        if not self.children:
            assert len(args) == 1, 'leaf can only accept 1 argument.'
            return self.func(*args)  # Base case.

        d_args = deque(args)
        func_results = []
        for child in self.children:
            f_args = [d_args.popleft() for _ in xrange(child.leaf_count)]
            func_results.append(child(*f_args))
        assert not d_args, 'Called with the wrong number of arguments'
        return self.func(*func_results)

基本上,&#34;技巧&#34;是跟踪每个节点有多少叶节点,因为叶节点的数量是它预期接受的参数数量。

  • 如果一个节点是一个叶子,那么只需用单个输入参数调用它的委托函数。
  • 如果节点不是叶子,则根据子子树中子叶节点的数量调用每个提供参数的子节点。

一些实施说明:

我使用collections.deque拉出正确的数字args传递给孩子。这是为了提高效率,因为deque允许我们在O(1)时间内获取这些参数。否则,我们会留下类似的东西:

for child in self.children:
    f_args = args[:child.leaf_count]
    args = args[child.leaf_count:]
    func_results.append(child(*args))

但是每次传球都是O(N)时间。对于小树,它可能并不重要。对于大树来说,它可能很重要: - )。

我还为leaf_count使用了一个静态成员,这意味着你需要从leaf到root构建你的树。当然,您可以根据问题限制使用不同的策略。例如您可以构建树,然后在构建树之后将leaf_count填充为单个遍,然后再开始评估函数,可以将leaf_count转换为函数({{1} })计算每次调用时的叶子(大树可能会花费很多)。

现在有些测试...我能想到的最简单的情况是叶节点是否都与身份函数相关联,然后非叶节点是一个添加输入值的函数。在这种情况下,结果应始终为输入值的总和:

@property

答案 3 :(得分:1)

由于你想要解耦函数和树,你可以这样做:

#root=RootNode, funcs=Map from Node to function, values=list of inputs
#nodes need isLeaf and children field
def Func(root,funcs,values):
    #check if leaf
    if root.isLeaf:
        #removes value from list
        val=values.pop(0)
        #returns function of root
        #(can be identity if you just want to input values)
        return funcs[root](val)

    #else we do a recursive iteration:
    else:
        nextVals=[]
        #for each child
        for child in root.children:
            #call this function->DFS for roots, removes values that are used
            nextVals.append(Func(child,funcs,values))
        #unpack list and call function
        return funcs[root](*nextVals)

这是一个例子:

class Node:
    children=[]
    isLeaf=False

    def __init__(self,isLeaf):
        self.isLeaf=isLeaf

    def add(self,n):
        self.children.append(n)




def Func(root,funcs,values):
    #check if leaf
    if root.isLeaf:
        #removes value from list
        val=values.pop(0)
        #returns function of root
        #(can be identity if you just want to input values)
        return funcs[root](val)

    #else we do a recursive iteration:
    else:
        nextVals=[]
        #for each child
        for child in root.children:
            #call this function->DFS for roots, removes values that are used
            nextVals.append(Func(child,funcs,values))
        #unpack list and call function
        return funcs[root](*nextVals)


def sum3(a,b,c):
    return a+b+c


import math

funcMap={}
funcMap[root]=sum3

root=Node(False)
layer1=[Node(True) for i in range(3)]
for i in range(3):
    root.add(layer1[i])
    funcMap[layer1[i]]=math.sin




print Func(root,funcMap,[1,2,3])
print math.sin(1)+math.sin(2)+math.sin(3)

返回相同的值 (使用python 2.7)

答案 4 :(得分:0)

这将是面向对象编程(OOP)的理想选择。例如,您可以使用这三个类

  1. 节点
  2. 对于使用树结构,递归方法通常更容易。

    或者,您也可以通过在元组中嵌入元组来直接构建递归结构。例如

    n1 = ( 'L', 'e0' )
    n2 = ( 'L', 'e1' )
    n3 = ( 'L', 'e2' )
    n4 = ( 'N', 'c0', n1, n2, n3 )
    n5 = ( 'N', 'b0', n4 )
    

    这不是您的完整树,但可以轻松扩展。只需使用print (n5)即可查看结果。

    这不是唯一的方法,可能会有变化。对于每个元组,第一个项目是一个字母,指定它是否是叶子&#34; L&#34;或节点&#34; N&#34; - 这将使递归函数更容易。第二项是名称(取自您的图纸)。对于节点,其他项是子节点。

    (n.b。我曾经在元组中使用&#34;元组&#34;来实现Huffmann编码算法 - 它也适用于树结构)。