当前位置:   article > 正文

如何在Python中使用生成器(Generate)和yield语句_generate_npy=false 函数用法

generate_npy=false 函数用法


原文链接:
https://realpython.com/introduction-to-python-generators/
数据链接:
https://github.com/realpython/materials/tree/master/generators

您是否因为使用了一个庞大的数据集,导致计算机的内存溢出?或许,您有一个复杂的函数,每次调用时都需要保持内部的状态,但是该函数太小,不足以证明创建自己的类是合理的。在这些情况下,生成器(Generate)和yield语句会提供帮助。
在本文结束时,您会知道:

  • 什么是生成器以及如何使用生成器

  • 如何创建生成器函数和表达式

  • Python的yield语句是如何工作的

  • 如何在一个生成器函数中使用多个Python yield 语句

  • 如何使用高级生成器方法

  • 如何用多个生成器构建数据管道

    如果您是一个python的初学者或进阶者,并且您对学习如何用一个更加Pythonic的方式来处理大数据集感兴趣的话,这篇教程将会对您很有用。

    本教程中使用的数据集的获取方式
    https://github.com/realpython/materials/tree/master/generators

使用生成器

PEP 255中引入的生成器函数是一种特殊的函数,可返回惰性迭代器。 这些对象可以像列表一样循环遍历。 但是,与列表不同,惰性迭代器不会将其内容存储在内存中

现在,您对生成器的功能有了一个大概的了解,您可能想知道它们在运行中是什么样子。 让我们看两个例子。 首先,您将从一个全局的角度来看生成器是如何工作的。 然后,您将更具体地观察每个示例。

例1:读取大文件

生成器的一个常见用例是处理数据流或大文件,例如CSV文件。 这些文本文件通过使用逗号将数据分为几列。 这种格式是共享数据的常用方法。 现在,如果要计算CSV文件中的行数怎么办? 下面的代码块显示了一种计数这些行的方法:

csv_gen = csv_reader("some_csv.txt")
row_count = 0

for row in csv_gen:
    row_count += 1

print(f"Row count is {row_count}")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Note:这里的字符串打印方式,可参见另一篇博客 字符串格式化的几种方法

观察上面的例子,您可能会把csv_gen设计为一个列表,csv_reader()打开一个文件,并把文件内容加载到csv_gen中。然后,程序迭代这个列表,并对每一行自动增加row_count

这是一种合理的解释,但是如果文件非常大的话,这种设计还可行吗?如果文件比现有的内存还大会发生什么呢?为了回答这个问题,我们假设csv_reader()只是打开文件,并将它读入列表:

def csv_reader(file_name):
    file = open(file_name)
    result = file.read().split("\n")
    return result
  • 1
  • 2
  • 3
  • 4

该函数打开一个给定的文件,并使用file.read().split()将每一行作为单独的元素添加到列表中。 如果您要在进一步的行计数代码块中使用此版本的csv_reader(),则将获得以下输出:

Traceback (most recent call last):
  File "ex1_naive.py", line 22, in <module>
    main()
  File "ex1_naive.py", line 13, in main
    csv_gen = csv_reader("file.txt")
  File "ex1_naive.py", line 6, in csv_reader
    result = file.read().split("\n")
MemoryError
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这种情况下,open()返回一个生成器对象,您可以逐行懒惰地对其进行迭代。 但是,file.read().split()会将所有内容立即加载到内存中,从而导致MemoryError

在此之前,您可能会注意到计算机的运行速度变慢。 您甚至可能需要使用KeyboardInterrupt杀死程序。 那么,如何处理这些庞大的数据文件? 看一看csv_reader()的新定义:

def csv_reader(file_name):
    for row in open(file_name, "r"):
        yield row
  • 1
  • 2
  • 3

通过使用这一版的csv_reader(),你可以打开文件,对其进行迭代,并且用 yield每一行。上面的代码的输出如下所示,并且没有内存错误:

Row count is 64186394
  • 1

到底发生了什么呢?实际上,您已经将csv_reader()变成了生成器函数。 这一版本的代码打开一个文件,遍历每一行,并yield每一行,而不是return每一行。

您还可以定义生成器表达式(也称为生成器解析式),其语法与列表解析式非常相似。 这样,您可以使用生成器而无需调用函数

csv_gen = (row for row in open(file_name))
  • 1

这是创建csv_gen的更简洁的方法。 您很快就会了解有关Python yield语句的更多信息。 现在,请记住以下主要区别:

  • 使用yield将产生一个生成器对象。
  • 使用return将仅产生文件的第一行(解释:上面的代码中,return的是 file.read().split("\n"))

例2:生成一个无穷序列

让我们换一个场景,看看无限序列的生成。在Python中,为了得到一个有限序列,你可以使用range(),并且将其转换成列表:

>>> a = range(5)
>>> list(a)
[0, 1, 2, 3, 4, 5]
  • 1
  • 2
  • 3

要生成一个无限序列,就必须使用生成器了,因为你的计算机内存是有限的:

def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1
  • 1
  • 2
  • 3
  • 4
  • 5

这个代码块简短而有趣。 首先,您初始化变量num并开始无限循环。 然后,您立即 yield num,以便可以捕获初始状态。 这模仿了range()的作用。

yield之后,将num加1。如果在for循环中尝试此操作,则会发现它确实确实是无限的:

>>> for i in infinite_sequence():
...     print(i, end=" ")
...
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39 40 41 42
[...]
6157818 6157819 6157820 6157821 6157822 6157823 6157824 6157825 6157826 6157827
6157828 6157829 6157830 6157831 6157832 6157833 6157834 6157835 6157836 6157837
6157838 6157839 6157840 6157841 6157842
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

程序会持续执行,直到您手动将其停止。

Note: 可以使用Ctrl+c实现 KeyboardInterrupt

除了使用for循环,您还能直接在生成器对象上调用next(),这对于在控制台中测试生成器特别有用:

>>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里,您有一个名为gen的生成器,可以通过重复调用next(),手动对其进行迭代。这可以很好地起到检查作用,以确保您的生成器正在产生您期望的输出。

例3:检测回文数

无限序列有多种用途,对它们的一种实际用途是构建回文检测器。 回文检测器将查找所有属于回文的字母序列或数字序列。 回文序列是指,向前或向后读取都是相同的单词或数字,例如121。首先,定义数字回文检测器:

def is_palindrome(num):
    if num // 10 == 0:  # 如果num是10的倍数,那么必然不是回文数,如100、20
        return False
    temp = num  # 将num复制给temp
    reversed_num = 0  # 保存反转后的数字

    while temp != 0:  # 当temp不为0时,进行反转数字的计算
        reversed_num = (reversed_num * 10) + (temp % 10)  
        temp = temp // 10
    '''
    例如,num = 121
    temp = num  # 所以 temp = 121
    循环的第一步:
    temp != 0 
    reversed_num = (0 * 10) + (1)  # reversed_num = 1
    temp = 12
    循环的第二步:
    temp != 0 
    reversed_num = (1 * 10) + (2)  # reversed_num = 12
    temp = 1
    循环的第三步:
    temp != 0 
    reversed_num = (12 * 10) + (1)  # reversed_num = 121
    temp = 0
    所以,整个过程相当于是,从后往前构建一个数,达到反转的效果
    '''          
    if num == reversed_num: # 说明是回文数
        return num
    else:  # 否则,不是回文数
        return False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

如果您对理解此代码中的数学有些困难,请不必担心。只需要知道,该函数输入一个数字,将其反转,然后检查反转后的数字是否与原数字相同,如果相同的话,就返回原数字,如果不同,就返回False(表示不是回文数)。

现在,您可以使用无限序列生成器来获取所有回文数字的列表:

>>> for i in infinite_sequence():
...     pal = is_palindrome(i)
...     if pal:
...         print(pal)
...
11
22
33
[...]
99799
99899
99999
100001
101101
102201
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 5, in is_palindrome
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

运行上述代码,打印到控制台的,只会是反转后的结果和原数字相同的值。

Note:实际上,您可以不必编写自己的无限序列生成器。itertools模块提供了一个非常高效的无限序列生成器,即itertools.count()

现在,您已经看到了无限序列生成器的一些简单使用方法,那么让我们更深入地理解生成器的工作方式。

理解生成器

到目前为止,您已经了解了创建生成器的两种主要方法:通过使用生成器函数和生成器表达式。 您甚至可能对生成器的工作方式有直观的了解。 让我们花一点时间使这些知识更加明确。

# 生成器函数
def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1
# 生成器表达式
csv_gen = (row for row in open(file_name))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

生成器函数的外观和行为与常规函数类似,但具有一种定义特征。 生成器函数使用Python yield关键字而不是return。 回顾您之前编写的生成器函数(如上所示),这看起来像一个典型的函数定义,除了Python yield语句及其后的代码。 yield意味着将值发送回调用者的位置,但是与return不同,您不会退出该函数而是记住该函数的状态。 这样,当在生成器对象(在for循环内显式或隐式)上调用next()时,先前yield的变量num递增,然后再次被yield。 由于生成器函数看起来与其他函数相似,并且行为与它们也非常相似,因此可以假定生成器表达式与Python中可用的其他表达式非常相似。

用生成器表达式来构建生成器

类似于列表表达式,生成器表达式允许您仅用几行代码就可以快速创建生成器对象。在使用列表表达式的情况下,它们也很有用,而且有一个额外的好处:您能够创建生成器表达式而无需在迭代前构建整个对象并将其保存在内存中。换句话说,使用生成器表达式仅会占用一点点内存。以对数字求平方为例:

>>> nums_squared_lc = [num**2 for num in range(5)]  # lc:list comprehension
>>> nums_squared_gc = (num**2 for num in range(5))  # gc:generator comprehension
  • 1
  • 2

nums_squared_lcnums_squared_gc看上去基本相同,但是有一个关键的区别。 你能发现吗? 看一下检查以下每个对象时会发生什么:

>>> nums_squared_lc
[0, 1, 4, 9, 16]
>>> nums_squared_gc
<generator object <genexpr> at 0x107fbbc78>
  • 1
  • 2
  • 3
  • 4

区别:第一个对象使用方括号构建列表,第二个对象使用圆括号构建生成器表达式。 输出证明您已创建了生成器对象,并且该对象与列表不同。

分析生成器性能

您之前了解到,生成器是优化内存的好方法。 尽管无限序列生成器是这种优化的一个极端示例,但让我们放大刚刚看到的数字平方示例,并检查生成的对象的大小。 您可以通过调用sys.getsizeof()来做到这一点:

>>> import sys
>>> nums_squared_lc = [i * 2 for i in range(10000)]
>>> sys.getsizeof(nums_squared_lc)
87624
>>> nums_squared_gc = (i ** 2 for i in range(10000))
>>> print(sys.getsizeof(nums_squared_gc))
120
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这种情况下,您从列表表达式中获得的列表为87,624字节,而生成器表达式只有120字节。这意味着列表比生成器对象大700倍!

不过,要记住一件事。 如果列表占用的内存小于运行计算机的可用内存,则列表表达式的计算速度可能要比等效的生成器表达式快。 为了探讨这一点,让我们总结一下以上两种理解的结果。 您可以使用cProfile.run()生成读数:

>>> import cProfile
>>> cProfile.run('sum([i * 2 for i in range(10000)])')
         5 function calls in 0.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <string>:1(<listcomp>)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.001    0.001 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> cProfile.run('sum((i * 2 for i in range(10000)))')
         10005 function calls in 0.003 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    10001    0.002    0.000    0.002    0.000 <string>:1(<genexpr>)
        1    0.000    0.000    0.003    0.003 <string>:1(<module>)
        1    0.000    0.000    0.003    0.003 {built-in method builtins.exec}
        1    0.001    0.001    0.003    0.003 {built-in method builtins.sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

在这里,您可以看到,对列表表达式中所有值求和的时间是生成器求和的三分之一。 如果速度是一个问题,而内存不是问题,那么列表理解可能是更好的工具。

请记住,列表表达式返回完整列表,而生成器表达式则返回生成器。 无论生成器是通过函数还是表达式生成的,生成器的工作原理都相同。 使用表达式只是允许您在一行中定义简单的生成器,并假设在每次内部迭代结束时使用yield关键字。

当然,Python yield语句是生成器所有功能赖以存在的关键,因此让我们深入了解yield在Python中的工作方式。

理解Python的yield

总体而言,yield是一个相当简单的语句。其主要工作是以类似于return语句的方式控制生成器函数的流程。但是,如前所述,Python yield语句有一些技巧。

当您调用生成器函数或使用生成器表达式时,您将返回一个称为生成器的特殊迭代器。您可以将此生成器分配给变量以使用它。当您在生成器上调用诸如next()之类的特殊方法时,该函数内的代码将被执行直至屈服。

命中Python yield语句时,程序将中止函数执行,并将产生的值返回给调用方。 (相反,return完全停止执行功能。)当某个功能被挂起时,该功能的状态将被保存。这包括生成器本地的所有变量绑定,指令指针,内部堆栈以及任何异常处理。

这样,无论何时调用生成器的方法之一,都可以恢复函数执行。那么,所有函数状态都会在yield后立即恢复。您可以通过使用多个Python yield语句来观察这种行为:

>>> def multi_yield():
...     yield_str = "This will print the first string"
...     yield yield_str
...     yield_str = "This will print the second string"
...     yield yield_str
...
>>> multi_obj = multi_yield()
>>> print(next(multi_obj))
This will print the first string
>>> print(next(multi_obj))
This will print the second string
>>> print(next(multi_obj))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

仔细看看对next()的最后一次调用。 您可以看到执行已被终止,返回的是Traceback错误。 这是因为与所有迭代器一样,生成器也可能被遍历结束。 除非生成器是无限的,否则只能迭代一次。 一旦遍历了所有值,迭代将停止并且退出for循环。 如果您使用next(),那么您将获得一个明确的StopIteration异常。

Note:注意:StopIteration是一个异常,通过引发它来表示迭代器已结束。 例如,for循环围绕StopIteration构建。 您甚至可以使用while循环来实现自己的for循环:

>>> letters = ["a", "b", "c", "y"]
>>> it = iter(letters)
>>> while True:
...     try:
...         letter = next(it)
...     except StopIteration:
...         break
...     print(letter)
...
a
b
c
y
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

您可以通过阅读官网文档exceptions,了解到更多冠寓异常的知识。

yield可用于多种方式来控制生成器的执行流程。 在您的创造力允许的范围内,可以利用多个Python yield语句。

使用高级的生成器方法

您已经看到了生成器的最普通的用法和结构,但是还有更多技巧需要介绍。 除了yield之外,生成器对象还可以使用以下方法:

  • .send()
  • .throw()
  • .close()

如何使用 .send()

在下一节中,您将构建一个使用这三种方法的程序。 该程序将像以前一样打印回文数字,但是要进行一些调整: 一旦遇到回文,新程序将添加一个数字并从那里开始搜索下一个。 您还可以使用.throw()处理异常,并使用.close()在给定的位数后停止生成器。 首先,让我们回想一下您的回文检测器的代码:

def is_palindrome(num):
    # Skip single-digit inputs
    if num // 10 == 0:
        return False
    temp = num
    reversed_num = 0

    while temp != 0:
        reversed_num = (reversed_num * 10) + (temp % 10)
        temp = temp // 10

    if num == reversed_num:
        return True  # 这里!与之前的代码不同
    else:
        return False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这与您之前看到的代码相同,只是现在程序严格返回TrueFalse。 您还需要修改原始的无限序列生成器,如下所示:

# 新的无限序列生成器
def infinite_palindromes():
    num = 0
    while True:
        if is_palindrome(num):
            i = (yield num)
            if i is not None:
                num = i
        num += 1

# 原始的无限序列生成器        
for i in infinite_sequence():
        pal = is_palindrome(i)
        if pal:
            print(pal)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里有很多变化!您会看到的第一个是第5行,其中i =(yield num)。尽管您之前已经了解到yield是一个语句,但这还不是全部。

从Python 2.5(引入了您现在正在学习的方法的同一发行版)开始,yield是一个表达式,而不是一个语句。当然,您仍然可以将其用作语句。但是现在,您也可以使用它,如您在上面的代码块中所看到的那样,在此i获取所产生的值。这使您可以处理yield得到的值。更重要的是,它允许您将值用.send()发送回生成器。当执行完yield之后,i将采用发送的值。

也就是,i = (yield num) 这条语句在执行时,先执行yield num,然后将yield num的结果赋值给i

您还将检查i是否不为None,如果在generator对象上调用next(),则可能会得到None。 (当您使用for循环进行迭代时,也会发生这种情况。)如果i有一个值,则可以使用新值更新num。但是,无论i是否持有一个值,您都将递增num并再次开始循环。

现在,看一下主要函数代码,它将最小的数字和另一个数字发送回生成器。例如,如果回文为121,则它将为.send()1000:

pal_gen = infinite_palindromes()
for i in pal_gen:
    digits = len(str(i))
    pal_gen.send(10 ** (digits))
  • 1
  • 2
  • 3
  • 4

使用此代码,您可以创建生成器对象并对其进行迭代。一旦找到回文,程序只会产生一个值。它使用len()确定该回文中的位数。然后,它将10**digits发送到生成器。这将执行带回到生成器逻辑中,并将i赋值为10**digits。由于i现在有一个值,因此程序将更新num,递增并再次检查回文。

一旦代码找到并yield另一个回文,您将通过for循环进行迭代。这与使用next()进行迭代相同。生成器也以i = (yield num)在第5行开始运行。但是,现在i是None,因为您没有明确发送值。

您在此处创建的是协程或生成器函数,可以在其中传递数据。这些对于构建数据管道很有用,但是您很快就会看到,它们对于构建数据管道不是必需的。 (如果您想深入研究协程,那么您可以参考A Curious Course on Coroutines and Concurrency。)

现在,您已经了解了.send(),让我们看一下.throw()

如何使用 .throw()

.throw()允许您使用生成器引发异常。 在下面的示例中,在第6行引发异常。一旦数字达到5,此代码将引发ValueError

 1 pal_gen = infinite_palindromes()
 2 for i in pal_gen:
 3     print(i)
 4     digits = len(str(i))
 5     if digits == 5:
 6         pal_gen.throw(ValueError("We don't like large palindromes"))
 7     pal_gen.send(10 ** (digits))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

.throw()在您可能需要捕获异常的任何区域都非常有用。 在此示例中,您使用.throw()来控制何时停止遍历生成器。 您可以使用.close()更优雅地完成此操作。

如何使用 .close()

顾名思义,.close()允许您停止生成器。 当控制无限序列发生器时,这尤其方便。 让我们通过将.throw()更改为.close()来停止迭代来更新上面的代码:

 1 pal_gen = infinite_palindromes()
 2 for i in pal_gen:
 3     print(i)
 4     digits = len(str(i))
 5     if digits == 5:
 6         pal_gen.close()  # 这里!与上面的代码不同
 7     pal_gen.send(10 ** (digits))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

您在第6行使用了.close(),而不是.throw()。使用.close()的好处是它会引发StopIterationStopIteration是一种用于通知有限迭代器结束的异常:

11
111
1111
10101
Traceback (most recent call last):
  File "advanced_gen.py", line 46, in <module>
    main()
  File "advanced_gen.py", line 42, in main
    pal_gen.send(10 ** (digits))
StopIteration
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在,您已经了解了生成器的特殊方法,下面我们来讨论使用生成器构建数据管道。

用生成器创建数据管道

数据管道使您可以将代码串联在一起,以处理大型数据集或数据流,而无需占用计算机的内存。 假设您有一个大型CSV文件:

permalink,company,numEmps,category,city,state,fundedDate,raisedAmt,raisedCurrency,round
digg,Digg,60,web,San Francisco,CA,1-Dec-06,8500000,USD,b
digg,Digg,60,web,San Francisco,CA,1-Oct-05,2800000,USD,a
facebook,Facebook,450,web,Palo Alto,CA,1-Sep-04,500000,USD,angel
facebook,Facebook,450,web,Palo Alto,CA,1-May-05,12700000,USD,a
photobucket,Photobucket,60,web,Palo Alto,CA,1-Mar-05,3000000,USD,a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

此示例来自TechCrunch美国大陆公司集,该集描述了总部位于美国的各种初创公司的融资轮次和美元金额。数据集的链接在本文开头。

现在该使用Python进行一些处理了! 为了演示如何使用生成器构建管道,您将分析此文件以获取数据集中所有A轮融资的总数和平均值。

让我们考虑一个策略:

  1. 读取文件的每一行。
  2. 将每一行拆分为值列表。
  3. 提取列名称。
  4. 使用列名和列表创建字典。
  5. 过滤掉您不感兴趣的回合。
  6. 计算您感兴趣的回合的总和平均值。

通常,您可以使用pandas之类的软件包来执行此操作,但也可以仅使用几个生成器来实现此功能。 首先,使用生成器表达式从文件中读取每一行:

file_name = "techcrunch.csv"
lines = (line for line in open(file_name))
  • 1
  • 2

然后,您将使用另一个生成器表达式与上一个生成器表达式配合使用,以将每行拆分为一个列表:

list_line = (s.rstrip().split(",") for s in lines)
  • 1

在这里,您创建了生成器list_line,该生成器遍历了第一个生成器lines。 这是设计生成器管道时使用的常见模式。 接下来,您将从techcrunch.csv中拉出列名称。 由于列名往往位于CSV文件的第一行,因此您可以通过简短的next()调用来获取它:

cols = next(list_line)  # 列名称
  • 1

对该next()的调用使迭代器在list_line生成器上前进了一次。 放在一起,您的代码应如下所示:

file_name = "techcrunch.csv"
lines = (line for line in open(file_name))
list_line = (s.rstrip().split(",") for s in lines)
cols = next(list_line)
  • 1
  • 2
  • 3
  • 4

总结一下,您首先创建一个生成器表达式linesyield文件中的每一行。 接下来,您在另一个名为list_line的生成器表达式的定义内迭代lineslist_line将每一行变成值列表。 然后,您只需使用next()进行一次list_line的迭代,即可从CSV文件中获取列名称的列表。

Note:注意尾随换行符! 此代码利用list_line生成器表达式中的.rstrip()来确保没有尾随换行符,该尾随换行符可以出现在CSV文件中。

为了帮助您过滤数据并对数据执行操作,您将创建字典,其中的键是CSV中的列名:

company_dicts = (dict(zip(cols, data)) for data in list_line)
  • 1

该生成器表达式遍历list_line生成的列表。 然后,它使用zip()dict()创建如上所述的字典。 现在,您将使用第四个生成器来过滤所需的资金回合,并同时提起raisedAmt

funding = (
    int(company_dict["raisedAmt"])
    for company_dict in company_dicts
    if company_dict["round"] == "A"
)
  • 1
  • 2
  • 3
  • 4
  • 5

在此代码段中,生成器表达式遍历company_dicts的结果,并为所有回合键为Acompany_dict接受raiseAmt

请记住,您并不是在生成器表达式中一次迭代所有这些。 实际上,直到您真正使用for循环或可迭代对象上的函数(例如sum(),您才可以迭代任何对象。 实际上,现在调用sum()遍历生成器:

total_series_a = sum(funding)
  • 1

将所有内容放在一起,您将产生以下脚本:

 1 file_name = "techcrunch.csv"
 2 lines = (line for line in open(file_name))
 3 list_line = (s.rstrip()split(",") for s in lines)
 4 cols = next(list_line)
 5 company_dicts = (dict(zip(cols, data)) for data in list_line)
 6 funding = (
 7     int(company_dict["raisedAmt"])
 8     for company_dict in company_dicts
 9     if company_dict["round"] == "A"
 10 )
 11 total_series_a = sum(funding)
 12 print(f"Total series A fundraising: ${total_series_a}")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

该脚本将您构建的每个生成器汇集在一起,它们都充当一个大数据管道。 以下是逐行细分:

  • 第2行读取文件的每一行。
  • 第3行将每一行拆分为多个值,并将这些值放入列表中。
  • 第4行使用next()将列名存储在列表中。
  • 第5行创建字典并将其与zip()调用结合起来:
    • 是第4行的列名cols。
    • 是在第3行中创建的列表形式的行。
  • 第6行获得每个公司的A轮融资金额。 它还过滤掉任何其他筹集的金额。
  • 第11行通过调用sum()来开始迭代过程,以获取在CSV中找到的A系列资金的总额。
    当您在techcrunch.csv上运行此代码时,您会发现A轮融资共筹集了$4,376,015,000美元。

Note:本教程中开发的处理CSV文件的方法对于理解如何使用生成器和Python yield语句非常重要。 但是,当您使用Python处理CSV文件时,应改用Python标准库中包含的csv模块。 该模块具有优化的方法,可有效处理CSV文件。

要进行更深入的研究,请尝试算出每家公司在A轮融资中获得的平均金额。 这有点棘手,所以这里有一些提示:

  • 生成器在完全迭代之后会耗尽。
  • 您仍然需要sum()函数。

祝好运!

结论

在本教程中,您已经了解了生成器函数和生成器表达式。

您现在知道:

  • 如何使用和编写生成器函数和生成器表达式
  • Python yield语句如何启用生成器
  • 如何在生成器函数中使用多个Python yield语句
  • 如何使用.send()将数据发送到生成器
  • 如何使用.throw()引发生成器异常
  • 如何使用.close()停止生成器的迭代
  • 如何构建生成器管道以有效处理大型CSV文件
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/509094
推荐阅读
相关标签
  

闽ICP备14008679号