【问题标题】:Python example from Computerphile about yield and yield fromComputerphile 中关于产量和产量的 Python 示例
【发布时间】:2021-02-23 03:25:21
【问题描述】:

主题:Python 中的懒惰 - Computerphile
网址:https://www.youtube.com/watch?v=5jwV3zxXc8E

在这个例子中,它尝试从 Python 中从 2 到 Infinity 的 Infinity 系列生成素数

特征的产量和产量

代码:

def nats(n):
    yield n
    yield from nats(n+1)

def sieve(s):
    n = next(s)
    yield n 
    yield from sieve(i for i in s if i%n!=0)

p = sieve(nats(2))
next(p)
next(p)

在函数 nats 中,它从 n 开始生成无穷大数,每次调用 next() 时都产生 n

series = nats(1)
next(series) # output 1
next(series) # output 2
....
next(series) # output n    



问题
在函数 sieve 中,它调用生成器对象并从 s 产生 n ;在此示例中,s 是 nats(2)
下一行是最棘手的部分

被子里面

sieve(i for i in s if i%n!=0)

它通过对象 s 循环 当 if 语句触发时返回 i

sieve(i)

我的问题是

  1. 究竟是什么?整数或生成器对象
  2. 假设 n = 2 for 循环序列是什么样的
    i % 2 我不知道 i 应该是什么
  3. 当它触发 if i%n!=0 时,它会返回 k 迭代器 i 或仅返回 kth单个数字/对象
  4. 毕竟上面最后它变成了sieve(i)sieve(nats(2))有什么区别
  5. 有人可以逐步了解 for 循环的工作原理吗

【问题讨论】:

  • 我不确定我是否理解所询问的内容,但没有像您想象的那样被调用 sieve(int)。使用i for i in s if i%n!=0 创建的生成器对象调用sieve。考虑更简单的代码:(i for i in range(0)),如果您在 repl 上运行它,它将返回 <generator object <genexpr> at 0x7f82fd061970>。这就是函数的内容。
  • 我对代码很困惑,尤其是 for 循环
  • 看来是这样。 “循环”是一个生成器表达式——它不像它在现场进行任何迭代。除了创建生成器对象之外,它不做任何事情。直到您在函数中点击 yield 后,您才开始从中提取整数。如果你在它上面调用next 也是一样的——这就是懒惰的全部意义——前面什么都没有发生,然后你从对象中挑选项目。

标签: python


【解决方案1】:
  1. sieve 总是用生成器对象调用。如前所述,nats(n)(它确实需要一个 int)是一个生成器,因为它包含一个 yield。这解释了p = sieve(nats(2))。让我们看看当您拨打sieve(i for i in s if i%n!=0) 时会发生什么。中间的那个位实际上是一个发电机本身!它不是用i 的任何特定值调用sieve,而是用生成i 的方式。

所以当你像这样调用sieve 时,第一行是n = next(s)。这要求s 产生的下一个值。这就是视频试图解释的部分内容是这些生成器根据需要执行操作。 (i for i in s if i%n!=0) 不会立即找到i 并将其传递给sieve,如果提供sieve 一种在需要时找到i 的方法。这就是next(s) 正在做的事情,它要求实际找到一个特定的i。在这种情况下,它将在原始 s 中查找下一个值(请记住,s 现在的含义有所不同)其中i%n != 0

  1. 假设n 等于2。这并不意味着s 是2,而是你从s 得到的第一个值是2。所以当它再次调用sieve 时,下一个值将是三个,这会导致 3%2 != 0 为真,因此它是从生成器中产生的,依此类推。

  2. 所以这可以追溯到(i for i in s if i%n!=0) 是如何生成生成器的。它不会以正确的方式返回任何东西,它不会给sieve 一个数字。只有在它调用sieve 并且新调用请求下一个值之后,它才会获取下一个值。所以它确实会产生一个数字,但只有当你打电话给next(s) 来询问它时。

  3. 我希望您现在可以看到,它从来没有调用过sieve(i),它只会使用生成器调用sievenats(2) 是一个生成器,(i for i in s if i%n!=0) 也是。

我认为这也是为您解释的第五条,但如果您仍然感到困惑,那么我(或其他人)可以准确地展示正在发生的事情,尽管仅用文字很难解释。

【讨论】:

    【解决方案2】:

    我终于意识到发生了什么 首先

    yield from sieve(i for i in s if i%n!=0)
    

    只是一个表达式,就像正则表达式一样,它只提供模式
    实际上表达式看起来像:

    1th time u visit the code   
    s1 = i for i in nats(2) if i % 2! = 0
    
    2nd time u visit
    s2 = i for i in s1 if i % 3! = 0
    
    3rd time u visit
    s3 = i for i in s2 if i % 5! = 0
    
    ....
    
    nth time u visit
    sn = i for i in (sn-1) if i % n_prime ! = 0
    

    你每次访问n = next(s),那么nats(2)就会增加1 并开始使用模式

    例如,假设我们在n = next(s)@ 行中有s3 模式
    1.首先它使用6 % 2 运行 s1,它什么也不返回,因为它不能完全填充 if 语句
    2.它再次使用7 % 2 运行s1,这次它填满了if 语句

    然后将其传递给s2,这一次它也填满了 if 语句
    然后将它传递给s3 它也完全填充了 if 语句
    因为 没有更多的表达式了
    它以yield n 输出结果,即7

    如有错误希望有人指正

    【讨论】:

      【解决方案3】:

      首先,这段代码过于聪明,是why not to use recursion in Python的一个很好的例子:

      >>> g = nats(10)
      >>> [next(g) for _ in range(1000)]
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in <listcomp>
      File "<stdin>", line 3, in nats
      File "<stdin>", line 3, in nats
      File "<stdin>", line 3, in nats
      [Previous line repeated 994 more times]
      RecursionError: maximum recursion depth exceeded
      

      如果不破坏调用堆栈,我们甚至无法生成 1000 个自然数或素数。

      深入研究代码,让我们从yield fromnats 开始。 yield from 使递归调用能够将yield 结果传递给原始调用代码返回的生成器。 nats 生成从n 到无穷大的无限自然数序列。

      实际上,nats 在 Python 中已经以 itertools.count 的形式存在。这不会破坏堆栈:

      >>> from itertools import count
      >>> g = count(10)
      >>> len([next(g) for _ in range(10000000)])
      10000000
      

      如果你必须自己写nats,你会更直接、更安全地使用循环(itertools.count 的实现类似):

      def nats(start=0):
          while True:
              yield start
              start += 1
      

      我们可以根据nats 看到生成器提供状态;直到您使用next() 请求结果才会返回结果,并且在每个yield 之后暂停执行。这对于无限序列很有用,因为我们可以在需要的时候获取我们想要的东西,而无需使用额外的空间来存储所有先前数字的列表或从头开始。

      虽然我在谈论它,但nats 并不是最伟大的名字;目前尚不清楚它在没有上下文的情况下意味着什么,并且该函数在负数等非自然数上运行良好。


      sievenats 做同样的事情,递归地逐个质数地前进。每个递归调用都会创建一个新的生成器,该生成器根据前一个生成器s 的输出执行筛选(s 应该称为last_sieve)、(i for i in s if i%n != 0)。此生成器会跳过任何数字,该数字是上一个递归调用中前一个生成器生成的第一个素数的倍数,n

      关键的认识是生成器不只是消失:它们停留在一个调用框架中过滤一个特定的素数,并继续被未来的生成器在更深的框架中调用。

      这有点像一个斗式旅。第一个生成器将所有数字的流发送到第二个生成器,第二个生成器过滤掉数字% 2,第三个生成器进一步过滤% 3,四个生成器过滤% 5上的流......每一帧,生成器链变长 1,数字必须经过越来越多的过滤器才能被视为素数。

      这是算法的迭代版本,它不会破坏堆栈,并有一些调试打印显示生成器的工作原理。您可以看到每一步都拒绝了哪些数字(括号中的数字是每个生成器的唯一单调递增标识符):

      from itertools import count
      
      def make_debuggable_gen(gen_expr, identifier):
          while True:
              val = next(gen_expr)
              print(f"[{identifier}] emitting '{val}'")
              yield val
              # note: no need to except StopIteration since our generators are infinite
      
      def make_prime_gen(last_gen, prime, identifier):
          return make_debuggable_gen((n for n in last_gen if n % prime), identifier)
      
      def sieve():
          identifier = 0
          prime_gen = make_prime_gen(count(2), -float("inf"), identifier)
      
          while True:
              prime = next(prime_gen)
              yield prime
              identifier += 1
              prime_gen = make_prime_gen(prime_gen, prime, identifier)
      
      if __name__ == "__main__":
          s = sieve()
          
          for _ in range(6):
              print(next(s))
      

      示例运行:

      [0] emitting '2'
      2
      [0] emitting '3'
      [1] emitting '3'
      3
      [0] emitting '4'
      [0] emitting '5'
      [1] emitting '5'
      [2] emitting '5'
      5
      [0] emitting '6'
      [0] emitting '7'
      [1] emitting '7'
      [2] emitting '7'
      [3] emitting '7'
      7
      [0] emitting '8'
      [0] emitting '9'
      [1] emitting '9'
      [0] emitting '10'
      [0] emitting '11'
      [1] emitting '11'
      [2] emitting '11'
      [3] emitting '11'
      [4] emitting '11'
      11
      [0] emitting '12'
      [0] emitting '13'
      [1] emitting '13'
      [2] emitting '13'
      [3] emitting '13'
      [4] emitting '13'
      [5] emitting '13'
      13
      

      希望这能回答您的问题,但要明确:

      1. i 是前一个生成器 s(我们称之为“last_sieve”)从前一个调用帧发出的整数。
      2. 希望上面的调试输出能回答——第二个生成器(id 1)有n = 2,因为那是生成器 id 0 发出的第一个素数。生成器 id 1 传递的值序列将是 3、5、7 ...它拒绝任何偶数 (% 2 == 0),当它达到 3 时,它会创建下一个 id 为 2 的生成器,用于过滤掉所有数字 % 3
      3. 条件i % n != 0 根据数字流是否可被此特定调用帧上的此特定生成器关心的一个素数n 整除来过滤数字流。素数n 表示链中前一个生成器找到的第一个素数(它可能应该称为primelast_prime)。
      4. 初始调用sieve(nats(2))i-th 调用之间的区别在于i-th 调用使用来自i-1-th 调用的生成器作为种子,该生成器对其进行了过滤某个素数。另一方面,第一个调用帧没有过滤,只有 nats 单调加 1。
      5. for 循环只是一个普通的generator expression,它基本上是一个有状态的、惰性的列表理解。它所做的只是从s 中无限拉取数字,并且不会发出任何未通过过滤器的数字,在我们的例子中,过滤器是用于测试可除性的模数。

      最后,这是上面代码没有调试的清理版本:

      from itertools import count
      
      def make_prime_gen(last_gen, prime):
          return (n for n in last_gen if n % prime)
      
      def sieve():
          prime_gen = count(2)
      
          while True:
              prime = next(prime_gen)
              yield prime
              prime_gen = make_prime_gen(prime_gen, prime)
      
      if __name__ == "__main__":
          s = sieve()
          
          for _ in range(6):
              print(next(s))
      

      请注意,函数make_prime_gen 充当prime 的闭包,其方式与原始代码让每个生成器在其自己的调用框架中跟踪n 的方式相似。我们不必在这里使用函数,但它是一种方便的习惯用法,可以方便地跟踪每个生成器的所有素数,而无需保留列表。

      即使没有不可原谅的递归,这个函数的空间复杂性也是一个严重的缺点,似乎几乎挫败了生成器背后的想法。为每个素数创建一个全新的生成器是一个严重的危险信号。在传统的筛子中,我们没有一个简单的数组或一组先前的素数,而是有一堆生成器对象和调用帧。

      从效率的角度来看,第一个生成器不仅需要遍历每个数字,而且还需要通过不断增加的生成器链将其传递到可以发出的点。这类似于 naive 算法的嵌套循环,但 naive 算法可以利用 Wikipedia 中描述的主循环中的各种烘焙跳过,更不用说更少的调用开销和可能更好的缓存局部性。

      【讨论】:

        猜你喜欢
        • 2015-11-14
        • 1970-01-01
        • 2020-06-10
        • 2016-06-08
        • 2018-02-04
        • 2013-09-14
        • 1970-01-01
        • 2011-09-29
        • 2016-08-25
        相关资源
        最近更新 更多