【问题标题】:Converting an imperative algorithm that "grows" a table into pure functions将“增长”表的命令式算法转换为纯函数
【发布时间】:2012-09-11 05:22:02
【问题描述】:

我的程序是用 Python 3 编写的,它有很多地方从(非常大的)类似表格的数字数据结构开始,并按照某种算法向其中添加列。 (算法在每个地方都不一样。)

我正在尝试将其转换为纯函数式方法,因为我遇到了命令式方法的问题(难以重用,难以记忆中间步骤,难以实现“惰性”计算,由于依赖状态而容易出错,等等)。

Table 类实现为字典字典:外部字典包含行,由row_id 索引;内部包含一行中的值,由column_title 索引。表格的方法很简单:

# return the value at the specified row_id, column_title
get_value(self, row_id, column_title)

# return the inner dictionary representing row given by row_id
get_row(self, row_id) 

# add a column new_column_title, defined by func
# func signature must be: take a row and return a value
add_column(self, new_column_title, func)

到目前为止,我只是在原始表中添加了列,每个函数都将整个表作为参数。当我转向纯函数时,我必须使所有参数不可变。因此,初始表变得不可变。任何其他列都将创建为独立列,并仅传递给需要它们的那些函数。一个典型的函数会获取初始表和一些已经创建的列,并返回一个新列。

我遇到的问题是如何实现独立列(Column)?

我可以为它们每一个都制作一本字典,但它似乎非常昂贵。事实上,如果我需要对每个逻辑行中的 10 个字段执行操作,我将需要进行 10 次字典查找。最重要的是,每一列都将包含键和值,使其大小增加一倍。

我可以使Column 成为一个简单的列表,并在其中存储对从row_id 到数组索引的映射的引用。好处是这个映射可以在对应于同一个初始表的所有列之间共享,并且一旦查找一次,它就适用于所有列。但这会产生任何其他问题吗?

如果我这样做,我可以更进一步,将映射实际存储在初始表本身中吗?我可以将来自Column 对象的引用放回创建它们的初始表吗?这似乎与我想象的功能性工作方式大不相同,但我看不出它会导致什么问题,因为一切都是不可变的。

一般来说,函数式方法是否不赞成在返回值中保留对其中一个参数的引用?看起来它不会破坏任何东西(比如优化或惰性评估),因为无论如何这个参数都是已知的。但也许我错过了什么。

【问题讨论】:

  • 你考虑过使用 Numpy 数组吗?这听起来就像 Numpy 旨在处理的事情:访问特定的列或行并将它们作为参数传递很容易,而且速度非常快,尤其是对于纯数字操作。
  • numpy.array 会很好用; pandas.DataFrame 更好。但不幸的是,由于我概述的原因,我真的很想转向纯函数。这需要不变性,所以我不能只是通过附加新计算的列来扩展原始表。
  • 您仍然可以编写函数来接受和返回任何维度的 Numpy 数组(单列、整个表......),而不会偏离函数式风格。 IMO,ndarray 类型的可变性既不存在也不存在;您只需要确保您的任何功能都没有副作用。如果你不想利用可变性,那就这样吧。
  • 哦,那当然。 ndarray 是实现ColumnTable 类的合理选择。

标签: python functional-programming software-design imperative-programming


【解决方案1】:

我会这样做:

  1. frozenset 派生您的表类。
  2. 每一行都应该是元组的子类。

现在您不能修改表 -> 不变性,太棒了!下一步 可以将每个函数视为您应用于 生成一个新的表:

f T -> T'

应该理解为在表 T 上应用函数 f 来生成 一个新表 T'。您也可以尝试客观化实际处理 表数据并将其视为您应用或添加到 表。

add(T, A) -> T'

这里的好处是加法可以是减法,而不是给你 建模撤消的简单方法。当你进入这种心态时,你的代码 变得很容易推理,因为你没有状态可以 搞砸了。

以下是如何实现和处理表格的示例 在 Python 中以纯函数方式构建结构。恕我直言,Python 不是 学习 FP 的最佳语言,因为它很容易 命令式地编程。我认为 Haskell、F# 或 Erlang 是更好的选择。

class Table(frozenset):
    def __new__(cls, names, rows):
        return frozenset.__new__(cls, rows)

    def __init__(self, names, rows):
        frozenset.__init__(self, rows)
        self.names = names

def add_column(rows, func):
    return [row + (func(row, idx),) for (idx, row) in enumerate(rows)]

def table_process(t, (name, func)):
    return Table(
        t.names + (name,),
        add_column(t, lambda row, idx: func(row))
        )

def table_filter(t, (name, func)):
    names = t.names
    idx = names.index(name)
    return Table(
        names,
        [row for row in t if func(row[idx])]
        )

def table_rank(t, name):
    names = t.names
    idx = names.index(name)
    rows = sorted(t, key = lambda row: row[idx])
    return Table(
        names + ('rank',),
        add_column(rows, lambda row, idx: idx)
        )

def table_print(t):
    format_row = lambda r: ' '.join('%15s' % c for c in r)
    print format_row(t.names)
    print '\n'.join(format_row(row) for row in t)

if __name__ == '__main__':
    from random import randint
    cols = ('c1', 'c2', 'c3')
    T = Table(
        cols,
        [tuple(randint(0, 9) for x in cols) for x in range(10)]
        )
    table_print(T)

    # Columns to add to the table, this is a perfect fit for a
    # reduce. I'd honestly use a boring for loop instead, but reduce
    # is a perfect example for how in FP data and code "becomes one."
    # In fact, this whole program could have been written as just one
    # big reduce.
    actions = [
        ('max', max),
        ('min', min),
        ('sum', sum),
        ('avg', lambda r: sum(r) / float(len(r)))
        ]
    T = reduce(table_process, actions, T)
    table_print(T)

    # Ranking is different because it requires an ordering, which a
    # table does not have.
    T2 = table_rank(T, 'sum')
    table_print(T2)

    # Simple where filter: select * from T2 where c2 < 5.
    T3 = table_filter(T2, ('c2', lambda c: c < 5))
    table_print(T3)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-10-11
    • 2011-09-30
    • 2021-01-13
    • 1970-01-01
    • 1970-01-01
    • 2012-07-02
    • 2016-01-08
    • 2012-10-20
    相关资源
    最近更新 更多