8

下面的代码总结了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

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

4

4 回答 4

8

Agenerator不存储值,您需要将生成器视为具有 context 的函数GENERATE,它会在每次被要求这样做时保存状态和值,因此,它会给您一个值,然后“丢弃”它,保持计算的上下文并等到你要求更多;并且会一直这样做,直到生成器上下文用完为止

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

在您提供的此示例中,使用的“唯一”内存是num, 是存储计算的位置,firstn生成器将num其保存在其中,context直到完成while loop

于 2017-06-18T10:25:08.873 回答
2

我认为你的第一个和第二个函数/方法在幕后所做的一个真实的例子会很有帮助,你会更好地理解发生了什么。

让我们在处理每个函数/方法时打印隐藏的 Python 内容locals()

locals():更新并返回表示当前本地符号表的字典。自由变量在函数块中调用时由 locals() 返回,而不是在类块中。

>>> 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]

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

但是,在您的第一个示例中,您的函数/方法需要存储并记住每一步以及用于停止 while 循环然后返回最终结果的值......与生成器相比,这使得该过程非常长。

于 2017-06-18T11:05:44.003 回答
1

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

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

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

于 2017-06-18T10:20:00.337 回答
0

如果sum-function 是用 Python 编写的,它可能类似于:

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

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

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

于 2017-06-18T10:31:42.560 回答