'elements' 存储在生成器中的什么位置?

Where are the 'elements' being stored in a generator?

下面的代码总结了all_numbers中列表中的所有数字。这是有道理的,因为所有要汇总的数字都在列表中。

def firstn(n):
    '''Returns list number range from 0 to n '''
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
    return nums

# all numbers are held in a list which is memory intensive
all_numbers = firstn(100000000)
sum_of_first_n = sum(all_numbers)

# Uses 3.8Gb during processing and 1.9Gb to store variables
# 13.9 seconds to process
sum_of_first_n 

将上述函数转换为生成器函数时,我发现使用更少的内存得到了相同的结果(代码下方)。我不明白的是,如果 all_numbers 不包含上面列表中的所有数字,怎么能总结呢?

如果数字是按需生成的,那么人们会生成所有数字并将它们汇总在一起,那么这些数字存储在哪里,这如何转化为减少内存使用量?

def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

# all numbers are held in a generator
all_numbers = firstn(100000000)
sum_of_first_n = sum(all_numbers)

# Uses < 100Mb during processing and to store variables
# 9.4 seconds to process
sum_of_first_n

我知道如何创建生成器函数以及为什么要使用它们,但我不明白它们是如何工作的。

此示例可以帮助您了解计算项目的方式和时间:

def firstn(n):
    num = 0
    while num < n:
        yield num
        print('incrementing num')
        num += 1

gen = firstn(n=10)

a0 = next(gen)
print(a0)      # 0
a1 = next(gen) # incrementing num
print(a1)      # 1
a2 = next(gen) # incrementing num
print(a2)      # 2

该函数没有 return,但它保持其内部状态(堆栈框架)并从上次 yield 编辑的点继续。

for 循环只是重复调用 next

您的下一个值是按需计算的;当时并非所有可能的值都需要在内存中。

A generator 不存储值,您需要 将生成器视为具有上下文 的函数,它将保存状态 GENERATE 每次被要求这样做时的值,所以,它给你一个值,然后 "discard" 它, 保持计算的上下文 并等到你要求更多的;并且 将这样做,直到生成器上下文耗尽

def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

在您提供的这个示例中,使用的 "only" 内存是 num,是存储计算的地方,firstn 生成器将 num 保存在其 context 直到 while loop 完成。

如果sum-函数写在Python中,它可能类似于:

def sum(iterable, start=0):
    part_sum = start
    for element in iterable:
        part_sum += element
    return part_sum

(当然这个函数和真正的函数有很多不同sum,但是你的例子的工作方式非常相似。)

如果用生成器调用sum(all_numbers),变量element只存储当前元素,变量part_sum只存储当前元素之前所有数字的总和.这样就可以只使用两个变量来计算总和,这显然比存储所有 100000000 个数字的数组需要的 space 少得多。正如其他人所指出的那样,生成器本身只是存储它的当前状态并在使用 next 调用时从那里继续计算,因此只需要在您的示例中存储 nnum

我认为您的第一个和第二个 functions/methods 在幕后所做的真实示例会有所帮助,您会更好地理解发生了什么。

让我们在使用 locals() 处理每个 function/method 时打印隐藏的 Python :

locals(): Update and return a dictionary representing the current local symbol table. Free variables are returned by locals() when it is called in function blocks, but not in class blocks.

>>> def firstn(n):
     '''Returns list number range from 0 to n '''
     num, nums = 0, []
     while num < n:
         nums.append(num)
         num += 1
         print(locals())
     return nums
>>> firstn(10)

将打印:

{'nums': [0], 'n': 10, 'num': 1}
{'nums': [0, 1], 'n': 10, 'num': 2}
{'nums': [0, 1, 2], 'n': 10, 'num': 3}
{'nums': [0, 1, 2, 3], 'n': 10, 'num': 4}
{'nums': [0, 1, 2, 3, 4], 'n': 10, 'num': 5}
{'nums': [0, 1, 2, 3, 4, 5], 'n': 10, 'num': 6}
{'nums': [0, 1, 2, 3, 4, 5, 6], 'n': 10, 'num': 7}
{'nums': [0, 1, 2, 3, 4, 5, 6, 7], 'n': 10, 'num': 8}
{'nums': [0, 1, 2, 3, 4, 5, 6, 7, 8], 'n': 10, 'num': 9}
{'nums': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'n': 10, 'num': 10}
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

但是:

>>> def firstn(n):
     num = 0
     while num < n:
         yield num
         num += 1
         print(locals())

>>> list(firstn(10))

将打印:

{'n': 10, 'num': 1}
{'n': 10, 'num': 2}
{'n': 10, 'num': 3}
{'n': 10, 'num': 4}
{'n': 10, 'num': 5}
{'n': 10, 'num': 6}
{'n': 10, 'num': 7}
{'n': 10, 'num': 8}
{'n': 10, 'num': 9}
{'n': 10, 'num': 10}
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

因此,如您所见,第二个 function/method(您的生成器)不关心过去或下一个过程的结果。此函数仅记住最后一个值(中断 while 循环的条件)并按需生成结果。

但是,在您的第一个示例中,您的 function/method 需要存储并记住每一步以及用于停止 while 循环然后返回最终结果的值...这使得过程非常长相比到你的发电机。