Python:在map函数中使用yield

8

在 map 函数内部使用 yield 是可能的吗?

为了证明目的,我创建了一个简单的代码片段。

# Python 3  (Win10)
from concurrent.futures import ThreadPoolExecutor
import os
def read_sample(sample):
    with open(os.path.join('samples', sample)) as fff:
        for _ in range(10):
            yield str(fff.read())

def main():
    with ThreadPoolExecutor(10) as exc:
        files = os.listdir('samples')
        files = list(exc.map(read_sample, files))
        print(str(len(files)), end="\r")

if __name__=="__main__":
     main()

我在samples文件夹中有100个文件。根据代码片段100*10=1000应该被打印出来。然而,它只打印了100。当我检查时,它只打印生成器对象。

通过什么更改可以打印出1000?


map 中使用生成器(“yield 函数”)是可能的,但正如您所观察到的那样,这只会实例化该生成器。为什么 read_sample 不直接生成列表呢?您使用生成器的目的是什么?请注意,您可以使用 list(itertools.chain(*exc.map(read_sample, files))) 来获取结果,但它既不受线程也不受生成器的好处。 - MisterMiyagi
这个有帮助吗?https://dev59.com/W1cP5IYBdhLWcg3wN3qP? - Nikos M.
1
我不明白为什么你期望得到1000。如果原始的“files”是一个包含100个名称的列表,那么结果也是一个包含100个元素的列表,并且你打印了len(),这意味着列表中元素的数量,而不是所有元素的总大小(这可能是1000 - 就像 sum(len(x) for x in files))。 - furas
2
我猜你实际想要的是让 map 查看生成器内部,类似于不存在的 map_from 或其他什么东西? - norok2
感谢您的回复。这是我为POC创建的代码片段。在我的产品中,我有一个包含100个A对象的列表,每个对象都有一个正则表达式。每个A对象都需要生成10个B对象。因此,我正在产生10个B对象。由于它们都是文件操作并且使用map函数,我正在尝试将其变成多线程。如果这个POC成功了,我将在我的产品中应用相同的概念。然而,我相信在Python中无论业务逻辑如何,都应该有一种方法来实现这一点。 - Jay Joshi
显示剩余4条评论
1个回答

3
你可以使用 map() 和一个生成器一起使用,但它只会尝试映射生成器对象本身,而不会尝试进入生成器中。
一种可能的方法是让生成器按照你想要的方式循环,并让函数对这些对象进行操作。这样做的附加优势是更清晰地将循环与计算分离。因此,类似以下代码应该可行:
  • 方法 #1
# Python 3  (Win10)
from concurrent.futures import ThreadPoolExecutor
import os
def read_samples(samples):
    for sample in samples:
        with open(os.path.join('samples', sample)) as fff:
            for _ in range(10):
                yield fff

def main():
    with ThreadPoolExecutor(10) as exc:
        files = os.listdir('samples')
        files = list(exc.map(lambda x: str(x.read()), read_samples(files)))
        print(str(len(files)), end="\r")

if __name__=="__main__":
     main()

另一种方法是嵌套一个额外的 map 调用来消耗生成器:

  • 第二种方法
# Python 3  (Win10)
from concurrent.futures import ThreadPoolExecutor
import os
def read_samples(samples):
    for sample in samples:
        with open(os.path.join('samples', sample)) as fff:
            for _ in range(10):
                yield fff

def main():
    with ThreadPoolExecutor(10) as exc:
        files = os.listdir('samples')
        files = exc.map(list, exc.map(lambda x: str(x.read())), read_samples(files))
        files = [f for fs in files for f in fs]  # flattening the results
        print(str(len(files)), end="\r")

if __name__=="__main__":
     main()

一个更加简化的示例

为了提供更具有可重复性的示例,您可以编写一个更加简化的示例来展示代码的特性(不依赖于系统中留下的文件):

from concurrent.futures import ThreadPoolExecutor


def foo(n):
    for i in range(n):
        yield i


with ThreadPoolExecutor(10) as exc:
    x = list(exc.map(foo, range(k)))
    print(x)
# [<generator object foo at 0x7f1a853d4518>, <generator object foo at 0x7f1a852e9990>, <generator object foo at 0x7f1a852e9db0>, <generator object foo at 0x7f1a852e9a40>, <generator object foo at 0x7f1a852e9830>, <generator object foo at 0x7f1a852e98e0>, <generator object foo at 0x7f1a852e9fc0>, <generator object foo at 0x7f1a852e9e60>]
  • 方法 #1:
from concurrent.futures import ThreadPoolExecutor


def foos(ns):
    for n in range(ns):
        for i in range(n):
            yield i


with ThreadPoolExecutor(10) as exc:
    k = 8
    x = list(exc.map(lambda x: x ** 2, foos(k)))
    print(x)
# [0, 0, 1, 0, 1, 4, 0, 1, 4, 9, 0, 1, 4, 9, 16, 0, 1, 4, 9, 16, 25, 0, 1, 4, 9, 16, 25, 36]
  • 方法二
from concurrent.futures import ThreadPoolExecutor


def foo(n):
    for i in range(n):
        yield i ** 2


with ThreadPoolExecutor(10) as exc:
    k = 8
    x = exc.map(list, exc.map(foo, range(k)))
    print([z for y in x for z in y])
# [0, 0, 1, 0, 1, 4, 0, 1, 4, 9, 0, 1, 4, 9, 16, 0, 1, 4, 9, 16, 25, 0, 1, 4, 9, 16, 25, 36]

我在想,您是否也可以将其应用于.submit(),而不是.map()。同样的逻辑也适用于它吗? - Lee Sai Mun

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接