由单行功能组成的数据处理管道的单元测试


10

在阅读Mary Rose Cook的《函数式编程实用入门》时,她以反模式为例。

def format_bands(bands):
    for band in bands:
        band['country'] = 'Canada'
        band['name'] = band['name'].replace('.', '')
        band['name'] = band['name'].title()

以来

  • 该功能不只是一件事
  • 名称不是描述性的
  • 它有副作用

作为建议的解决方案,她建议通过管道传递匿名函数

pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
                      call(lambda x: x.replace('.', ''), 'name'),
                      call(str.title, 'name')])

但是,在我看来,这具有无法测试的缺点。至少format_bands可以进行单元测试以检查它是否符合预期,但是如何测试管道?还是说匿名函数如此不言自明以至于不需要对其进行测试?

我为此的实际应用程序是尝试使我的pandas代码更具功能性。我经常会在“ munging”函数中使用某种管道”

def munge_data(df)
     df['name'] = df['name'].str.lower()
     df = df.drop_duplicates()
     return df

或以管道样式重写:

def munge_data(df)
    munged = (df.assign(lambda x: x['name'].str.lower()
                .drop_duplicates())
    return munged

对于这种情况下的最佳做法有何建议?


4
这些单独的lambda函数太小,无法进行单元测试。测试最终结果。换句话说,匿名函数不可单元测试,因此,如果您打算对单元进行单独的单元测试,请不要将其编写为匿名函数。
罗伯特·哈维

Answers:


1

我认为您可能错过了本书更正示例中更重要的部分。对代码的更根本的更改是从对列表中的所有值进行操作的方法到对一个元素进行操作的方法。

已经存在类似的函数iter(在本例中名为pipeline_foreach),它们对列表中的所有元素执行给定的操作。无需使用for循环来复制它。另外,使用众所周知的列表操作可以使您的意图清晰明了。与map您一起改变价值。随着iter您对每个元素都产生副作用。有了for循环,您就可以了……嗯,直到您仔细研究一下,您才真正知道。

经过校正的示例代码仍然不是很实用,因为(据我所知)它已对列表中的值进行了变异而不返回它们,从而阻止了进一步的管道处理或函数组合。功能上首选的方法map将使用更新的country和创建一个新的频段列表name。然后,您可以将该输出通过管道传递到下一个函数,或者map与另一个已获得波段列表的函数组成。使用iter,就像是流水线的死胡同。

我认为最终结果代码具有一些小的功能,这些功能对于在这里进行麻烦的测试来说太琐碎了。毕竟,您不需要针对replace或编写单元测试title。现在也许您确实想将它们组合成自己的功能并进行单元测试,以确保在单个项目上实现所需的组合。我自己,我可能会刚换format_bandsformat_band奇异,扔下for循环,并呼吁pipeline_each(bands, format_band)。然后,您可以测试format_band以确保您没有忘记什么。

无论如何,在您的代码上。您的第二个代码示例似乎更加流水线。但这本身并不能提供函数式编程的好处。实际上,函数式编程是指仅通过输入和输出定义函数的兼容性,从而确保函数与其他函数的兼容性。如果函数内部存在隐藏的副作用,那么尽管其输入/输出与其他函数保持一致,但直到运行时才能知道它们是否兼容。但是,如果两个函数是无副作用的,并且匹配输出到输入,那么您可以流水线或组合它们而不必担心意外的结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.