问题 为什么迭代器在Python文档中被认为是函数式的?


我试图理解迭代器。我注意到 Python文档 认为迭代器是一个功能风格的构造。我真的不明白。

迭代器里面有一个状态是不是真的。所以当你打电话 it.__next__(),你改变了迭代器的状态。据我所知,对象的变异状态不被认为是功能性的,因为函数式编程强调对象/闭包的不变性和组合能力。

实际上,问题出现了,因为我想编写一个Scheme过程/函数,它接受令牌并返回迭代器。

(define tokens->iterator
  (lambda ls
    (lambda ()
      (if (null? ls)
          '*eoi*
          (let ((tok (car ls)))
            (set! ls (cdr ls))
            tok)))))

注意我必须使用 set! 变异 ls这就是我提出这个问题的方法。

要使用它,

(define it (tokens->iterator 1 '+ 2))

为了测试它,

scheme@(guile-user)> (it)
$2 = 1
scheme@(guile-user)> (it)
$3 = +
scheme@(guile-user)> (it)
$4 = 2
scheme@(guile-user)> (it)
$5 = *eoi*
scheme@(guile-user)> (it)
$6 = *eoi*

只是为了好玩,我还将其翻译成Python:

def tokens_to_iterator(*tup):
    ls = list(tup)
    def iterator():
        if not ls:
            return "*eoi*"
        else:
            tok = ls.pop(0)
            return tok
    return iterator

类似地,pop()方法通过改变列表来移除并返回第一个元素。

要使用它,

it = tokens_to_iterator(1, "+", 2)

为了测试它,

>>> it()
1
>>> it()
'+'
>>> it()
2
>>> it()
'*eoi*'
>>> it()
'*eoi*'

任何人都可以澄清一下吗?顺便说一句,我正在使用Python 3和Guile Scheme,以防任何人有兴趣尝试这些例子。


8197
2018-03-17 15:20


起源

“功能性”和“可变状态”并不是一回事。 - zwol
因为这种状态隐藏在内部,从外面无法进入;它是 封装。它会自动更改 下一个 调用,并保留对象的有效性。从外部观察者的角度来看,它表现得像一些人 法律是的 一贯,这不能从外部任意改变。这个状态只是一个实现细节。 - Will Ness
@zwol还有“无副作用”。它是否与“可变状态免费”相同?为什么“功能性”和“可变状态”不一样?并非所有“功能性”语言都强调“可变状态免费”吗?我错过了吗? - Alex Vong
@Will Ness不是OOP也有数据封装吗?例如,state封装在对象内。您所能做的就是调用对象的方法。究竟什么使迭代器起作用? - Alex Vong
有关相关示例,请参阅 http://3e8.org/pub/scheme/doc/lisp-pointers/v1i4/p23-clinger.pdf 以及作者对使用发电机问题的评论。对于两个消费者,任何一个观察到的序列都是混乱的。所以这 是 脆弱,取决于使用协议。该论文中的另一件事,即流,实际上增加了 存储,存储生成器产生的结果,因此两个消费者都观察到相同(共享)的序列(因为它们从存储中拉出,而不是从发生器本身拉出)。那也是什么 itertools.tee() 是的,IIRC。或类似的东西。 - Will Ness


答案:


你有一个很好的观点。迭代器肯定不是“纯函数式”,这个术语通常用于描述根本不使用突变的习语。然而,更广泛的术语“功能性”更宽松地定义为表示使用相对较少的突变的程序,这些程序利用高阶和第一类函数,或许最广泛的是,“使用不具有的奇怪抽象看起来像C.“

坦率地说,我认为我会这么做  调用迭代器功能。那就是:我同意你的看法。


7
2018-03-17 15:52



“使用看起来不像C的怪异抽象”,像monad ...... - Alex Vong


功能性 样式 是处理整个数据列表,而不是可以随心所欲地改变的值集合。例如,如果您有一个数字列表,并且您想要更改第三个元素,那么非功能性方法是直接更改它:

>>> lst = ["a", "b", "c", "d", "e"]
>>> lst[3] = "Z"
>>> lst
["a", "b", "c", "Z", "e"]

功能方法是编写一个函数,该函数接受原始序列并返回一个包含所做更改的新列表,使原始序列保持不变。

>>> lst = ["a", "b", "c", "d", "e"]
>>> new_lst = [x if i != 3 else "Z" for (i, x) in enumerate(lst)]
>>> lst
["a", "b", "c", "d", "e"]
>>> new_lst
["a", "b", "c", "Z", "e"]

你的两个迭代器都没有纯粹的功能,因为它们确实保持了可变状态,虽然它们被视为一个黑盒子,你可以在功能上使用它们,因为 用户 迭代器不能直接影响该状态。

纯函数迭代器将是一个将列表作为输入的函数  当前状态,并返回一个值  要传递给函数的下一个调用的新状态。

>>> state = 0
>>> def it(lst, state):
...   if state is None:
...       return None
...   return lst[state], state + 1
...
>>> lst = ["a", "b", "c", "d", "e"]
>>> value, new_state = it(lst, state)
>>> value
'a'
>>> state, new_state
(0, 1)
>>> it(lst, new_state)
('b', 2)
>>> state, new_state
(0, 1)

3
2018-03-17 16:11



固定。由于它没有,因此很难在Python中演示纯粹的功能性技术 let 绑定以临时命名函数结果,这与变量赋值不完全相同。 (最大的区别是你可以绑定一个名字 一旦;你以后不能改变绑定。) - chepner
我明白了,所以显式传递状态,使你的迭代器纯粹是功能性的,因为它是相同的 lst 同样的 state,迭代器将返回相同的元素。我可以进一步声称Python的迭代器不是纯粹的功能因为 it.__next__() 将返回具有相同输入的不同元素(在这种情况下什么都不是),这是迭代器在内部维护状态的证据? - Alex Vong
正确。这就是为什么在将迭代器传递给之后的原因之一 itertools.tee,你只应该使用函数返回的迭代器之一,而不是原始的迭代器本身。 - chepner
(请注意未来的读者:有人评论我最初使用变量来在连续调用之间传递状态 it;我的第一个评论和随后的编辑是对此的回应。我希望该用户没有删除他的评论;这是一个有效的观点。) - chepner