Files
coding-mentor-agent/kb/python-course-kb-practical-python/wiki/summaries/03_Producers_consumers.md
T

6.9 KiB
Raw Blame History

doc_type, full_text
doc_type full_text
short sources/03_Producers_consumers.md

03_Producers_consumers 总结

本文讲解如何利用 Python 生成器 组织生产者-消费者问题,并把多个处理步骤串联成惰性执行的 concepts/数据流管道。核心思想是:yield 负责生产值,for 循环负责消费值;中间处理函数既消费上游数据,又通过 yield 向下游继续生产数据。

核心概念

生产者与消费者

生成器天然适合表达 concepts/生产者消费者模式:

# Producer
def follow(f):
    while True:
        yield line

# Consumer
for line in follow(f):
    ...

其中:

  • 生产者通过 yield 产生数据。
  • 消费者通过 for 循环逐项取得数据。
  • 数据不会一次性全部生成,而是按需、增量流动。

这使生成器特别适合处理日志跟踪、实时数据流、大文件读取等场景。

生成器管道

本文将处理流程抽象为类似 Unix 管道的结构:

producer -> processing -> processing -> consumer

管道通常包含三类组件:

1. 生产者

生产者一般是生成器,也可以是列表、元组等可迭代对象。

def producer():
    yield item

它负责向管道输入初始数据。

2. 中间处理阶段

中间阶段既是消费者,也是生产者:

def processing(s):
    for item in s:
        yield newitem

它可以:

  • 转换数据;
  • 过滤数据;
  • 重组数据结构;
  • 将原始文本解析为更有意义的对象。

这是 惰性求值 的重要应用:每个阶段只在下游请求数据时才处理一个元素。

3. 最终消费者

消费者通常是一个 for 循环:

def consumer(s):
    for item in s:
        ...

它接收最终处理结果,并执行打印、写入、展示或其他副作用操作。

管道的组装方式

一个典型管道可以这样搭建:

a = producer()
b = processing(a)
c = consumer(b)

数据会从 producer() 增量流入 processing(),最后由 consumer() 消费。各阶段之间通过迭代协议连接,而不是显式调用彼此的内部逻辑。

练习 6.8:简单过滤管道

文中首先构造了一个简单的过滤生成器:

def filematch(lines, substr):
    for line in lines:
        if substr in line:
            yield line

它不负责打开文件,只处理传入的行序列。这体现了良好的管道组件设计:每个函数只完成单一职责。

示例用法:

from follow import follow

lines = follow('Data/stocklog.csv')
ibm = filematch(lines, 'IBM')
for line in ibm:
    print(line)

这里形成了如下数据流:

follow(logfile) -> filematch(lines, 'IBM') -> print

练习 6.9:与 csv.reader 组合

生成器管道不仅可以连接自定义生成器,也可以连接标准库中接受可迭代对象的函数。

from follow import follow
import csv

lines = follow('Data/stocklog.csv')
rows = csv.reader(lines)
for row in rows:
    print(row)

follow() 产生文本行,csv.reader() 消费这些行并产生拆分后的列表。这个例子说明,只要对象遵守 迭代协议,就能自然接入管道。

练习 6.10:构建更多管道组件

随后文档构建了更完整的股票行情解析管道。

解析 CSV 行

def parse_stock_data(lines):
    rows = csv.reader(lines)
    return rows

选择特定列

def select_columns(rows, indices):
    for row in rows:
        yield [row[index] for index in indices]

该阶段将完整 CSV 行缩减为需要的字段,例如股票名、价格、涨跌额:

rows = select_columns(rows, [0, 1, 4])

转换数据类型

def convert_types(rows, types):
    for row in rows:
        yield [func(val) for func, val in zip(types, row)]

这一步把字符串转换为合适类型,例如:

[str, float, float]

构造字典

def make_dicts(rows, headers):
    for row in rows:
        yield dict(zip(headers, row))

最终每条股票数据被转换为结构化字典:

{'name': 'BA', 'price': 98.35, 'change': 0.16}

封装完整解析流程

def parse_stock_data(lines):
    rows = csv.reader(lines)
    rows = select_columns(rows, [0, 1, 4])
    rows = convert_types(rows, [str, float, float])
    rows = make_dicts(rows, ['name', 'price', 'change'])
    return rows

这个函数把多个管道阶段封装为一个更高层接口,是 函数组合 在数据处理中的应用。

练习 6.11:过滤数据

文档继续加入一个过滤阶段:

def filter_symbols(rows, names):
    for row in rows:
        if row['name'] in names:
            yield row

它根据投资组合中的股票名称过滤实时行情:

import report

portfolio = report.read_portfolio('Data/portfolio.csv')
rows = parse_stock_data(follow('Data/stocklog.csv'))
rows = filter_symbols(rows, portfolio)
for row in rows:
    print(row)

这里展示了生成器管道的一项重要能力:可以在不中断整体流式处理的前提下插入过滤逻辑。

练习 6.12:组合成实时股票行情器

最后要求实现:

def ticker(portfile, logfile, fmt):
    ...

该函数应把以下步骤整合起来:

  1. 读取投资组合文件;
  2. 使用 follow() 追踪股票日志;
  3. 解析 CSV 数据;
  4. 选择并转换字段;
  5. 构造字典;
  6. 根据投资组合过滤股票;
  7. 按指定格式输出,例如 txt 或 csv。

示例输出包括文本表格:

      Name      Price     Change
---------- ---------- ----------
        GE      37.14      -0.18
      MSFT      29.96      -0.09

以及 CSV 格式:

Name,Price,Change
IBM,102.79,-0.28
CAT,78.04,-0.48

这说明管道不仅可用于数据转换,也能作为应用程序架构的一部分。

关键思想

  • yield 是生产者,for 循环是消费者。
  • 生成器可以被串联成增量执行的数据处理管道。
  • 管道中的中间阶段既消费上游数据,又生产下游数据。
  • 每个阶段应保持简单、单一职责,便于组合和复用。
  • 标准库中接受可迭代对象的工具,如 csv.reader(),可以自然融入生成器管道。
  • 通过封装多个阶段,可以构建更高级的数据处理接口。
  • 该模式适合实时日志处理、数据清洗、数据过滤、格式转换等场景。

相关概念