【问题标题】:Composition of a hierarchy of functions功能层次结构的组成
【发布时间】:2016-03-25 15:19:27
【问题描述】:

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

这是我所说的“功能树的组合”的具体示例。取一棵有根树,其节点由函数标记,如下所示:

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

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,以及一个与T 的节点相对应的函数列表L,是否有一种规范方法来编写一个函数F 的参数T 和 L 返回 L 中根据树构造的函数的组合 T?

这样,组合的“连线”——树T——与其内部“组件”——列表L分离。一个“规范”的解决方案应该特别包括T 和L 的表示,它们自然地适应了这个问题。

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

def treecomp(tree, list_of_funcs):
    ...
    return function

F = treecomp(T, L)

附录

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

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

预感

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

【问题讨论】:

  • 听起来像一个奇怪的调用图。乐趣! :)
  • @EugeneHa 我使用的是函数字典而不是列表。可以吗?
  • @JeD: dict 会很合理,但列表在某种程度上更方便。在示例中,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 作为列表,我是否已经被搞砸了!——虽然看起来很自然。
  • @JeD:我没有被坑;参看。我的解决方案,但是仍然很笨拙。 :(
  • @JeD:我的解决方案中的笨拙已得到纠正。

标签: python functional-programming hierarchy function-composition


【解决方案1】:

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

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

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

【讨论】:

  • 感谢您的意见。事实证明,没有必要对节点计数进行任何记账。
【解决方案2】:

我们定义了一个函数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 个随机样本上。

【讨论】:

    【解决方案3】:

    既然你想解耦函数和树,你可以这样做:

    #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】:

      这是一个我编写的简单示例:

      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)
      

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

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

      一些实现说明:

      我使用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_count,或您可以将leaf_count 转换为计数的函数(@property)每次调用的叶子(对于大树来说可能会变得昂贵)。

      现在进行一些测试...我能想到的最简单的情况是,如果叶节点都与恒等函数相关联,那么非叶节点是一个添加输入值的函数。在这种情况下,结果应该始终是输入值的总和:

      def my_sum(*args):
          return sum(args)
      
      def identity(value):
          return value
      
      e0, e1, e2, f0, g0, g1 = [Node([], identity) for _ in xrange(6)]
      c0 = Node([e0, e1, e2], my_sum)
      d0 = Node([f0], my_sum)
      d1 = Node([g0, g1], my_sum)
      b0 = Node([c0], my_sum)
      b1 = Node([d0, d1], my_sum)
      a0 = Node([b0, b1], my_sum)
      
      arg_tests = [
          (1, 1, 1, 1, 1, 1),
          (1, 2, 3, 4, 5, 6)
      ]
      for args in arg_tests:
          assert a0(*args) == sum(args)
      print('Pass!')
      

      【讨论】:

      • 非常清晰易读的实现说明。事实证明,如果您将树以标准化形式放置,则“技巧”是不必要的。 (如果用户给出了错误数量的 args,就让执行失败——请求宽恕比请求许可更好。;)看起来你设置树标签的方式基本上相当于手工编写组合。
      【解决方案5】:

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

      1. 节点
      2. 叶
      3. 树

      对于使用树结构,递归方法通常更容易。

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

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

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

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

      (n.b. 我曾经使用“元组中的元组”来实现 Huffmann 编码算法——它也适用于树结构)。

      【讨论】:

        猜你喜欢
        • 2016-04-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-12-16
        • 1970-01-01
        • 1970-01-01
        • 2014-03-01
        相关资源
        最近更新 更多