【问题标题】:Python: writing files in functional programming stylePython:以函数式编程风格编写文件
【发布时间】:2016-01-11 11:47:47
【问题描述】:

我们应该如何在 Python 中编写文件同时保持功能纯正?通常我会做这样的事情

from typing import Iterable
from io import IOBase


def transform_input(input_lines: Iterable[str]) -> Iterable[str]: ...


def print_pack(input_lines: Iterable[str], output: IOBase) -> None:
    for line in input_lines:
        print(line, file=output)


def main(*args, **kwargs):
    # Somehow we get a bunch iterables with strings and a list of output streams
    packs_of_input = ... # Iterable[Iterable[str]]
    output_streams = ... # Iterable[IOBase]
    packs_to_print = map(transform_input, packs_of_input)
    for pack, output_stream in zip(packs_to_print, output_streams):
        print_pack(pack, output_stream)

我们可以用类似这样的东西替换for-loop

list(map(lambda pack_stream: print_pack(*pack_stream), zip(packs_to_print, output_streams))

但它只会使打印看起来像是在功能上完成的。问题是print_pack 不是一个纯函数,它的所有努力都会产生副作用并且它什么也没有返回。 我们应该如何编写文件并保持功能纯(或几乎纯)?

【问题讨论】:

  • 您为什么要这样做? for 循环是最易读的,而且是最“正确”的方式。
  • @tobias_k 我不认为我会在任何严肃的项目中使用它,但我想知道这些事情是如何完成的。了解一个完全不同的范例总是一件好事,即使您不打算使用它。
  • 我将定义一个包含for 循环的forEach 方法,并使用它。 def forEach(iterable, func): for i in iterable: func(i) 然后forEach(input_lines, lambda x: print(x, file=output))。 (无论如何,函数式编程只是将 for 循环移到其他地方)
  • @CongMa 是的,看起来很有希望。

标签: python python-3.x io functional-programming


【解决方案1】:

本质上,在 Python 中,您需要在 somewhere 有一个不纯的函数,因此在这个应用程序中没有办法拥有 100% 的纯函数。最后你需要做一些IO,IO是不纯的。

但是,您可以尝试将应用程序中的特定抽象层表示为纯函数,并将产生实际副作用的部分隔离在另一个模块中。您可以以一种特别的方式很容易地做到这一点——例如,通过在您的主代码中累积您想要编写为纯不可变数据结构的文件的内容。然后可以减少副作用代码的大小,因为它所要做的就是将字符串转储到文件中。

我们可以向 Haskell 寻求一种更严格的方法,以使用纯函数和数据结构来纯粹表示副作用操作的全部功能——使用 Monad 抽象。本质上,Monad 是可以绑定回调的东西,以创建一个基于纯函数的有效计算链。对于 IO monad,一旦你从 main 函数返回一个 IO 值,Haskell 运行时就会负责实际执行副作用——所以你编写的所有代码在技术上都是纯函数,运行时负责 IO .

Effect 库(免责声明:我写的)基本上在 Python 中实现了某种风格的 Monad(或非常接近于 monad 的东西)。这使您可以将任意 IO(和其他副作用)表示为纯对象和函数,并将这些效果的实际性能放在一边。所以你的应用程序代码可以是 100% 纯的,只要你有一种相对简单的副作用函数库。

因此,例如,要实现一个将行列表写入具有效果的文件的函数,您需要执行以下操作:

@do
def write_lines_to_file(lines, filename):
    file_handle = yield open_file(filename)
    for line in lines:
        yield write_data(file_handle, line)
    # alternatively:
    # from effect.fold import sequence; from functools import partial
    # yield sequence(map(partial(write_data, file_handle), lines))
    yield close_file(file_handle)

Effect 库提供了这个特殊的do 装饰器,让您可以使用看起来很命令式的语法来描述纯粹的有效操作。上面的函数相当于这个:

def write_lines_to_file(lines, filename):
    file_handle_eff = open_file(filename).on(
        lambda file_handle:
            sequence(map(partial(write_data, file_handle), lines)).on(
                lambda _: close_file(file_handle)))

它们都假设存在三个函数:open_file、write_data 和 close_file。假定这些函数返回表示执行这些操作的意图的 Effect 对象。最后,Effect 本质上是一种意图(对所请求的操作的某种透明描述),以及在该操作的结果完成时运行的一个或多个回调。有趣的区别是 write_lines_to_file 实际上不会将行写入文件;它只是返回intent的一些表示以将一些行写入文件。

要实际执行此效果,您需要使用sync_perform 函数,如sync_perform(dispatcher, write_lines_to_file(lines, filename))。这是一个不纯函数,它实际上运行执行器,以获得有效计算的纯表​​示使用的所有效果。

我可以详细介绍如何实现 open_file、write_data 和 close_file,以及“dispatcher”参数的详细信息,但实际上https://effect.readthedocs.org/ 的文档可能是正确的参考此时。

我还在 Strange Loop 上发表了关于 Effect 及其实现的演讲,您可以在 YouTube 上观看:https://www.youtube.com/watch?v=D37dc9EoFus

值得注意的是,Effect 是一种非常严格的方法,可以让代码保持纯粹的功能。通过采用“功能核心/命令式外壳”方法并尽力将大部分代码编写为纯函数并最小化有效代码,您可以在实现可维护代码方面取得很大进展。但是,如果您对更严格的方法感兴趣,我认为 Effect 是好的。我的团队在生产环境中使用它,它提供了很多帮助,尤其是它的测试 API。

【讨论】:

    猜你喜欢
    • 2022-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-20
    • 2023-03-24
    • 2017-10-31
    相关资源
    最近更新 更多