Python 生成器函数和普通函数有什么区别?从执行机制到工程实战全面解析

在 Python 编程中,函数是最基本、也最重要的代码组织单元之一。

对于刚接触 Python 的开发者来说,普通函数通常很好理解:调用函数,执行代码,返回结果。但当代码中出现 yield 关键字时,事情就开始变得不一样了。

def normal_function():
    return 1

def generator_function():
    yield 1

这两个函数看起来只差一个关键字,但它们的执行方式、返回值类型、内存占用、适用场景以及内部状态管理机制,都存在本质区别。

理解生成器函数,不只是掌握一个 Python 语法点。它关系到如何处理大数据、如何构建流式任务、如何降低内存消耗,以及如何设计更优雅的数据处理程序。

本文将从基础语法、执行机制、内存模型、实际案例和最佳实践等多个角度,系统讲清楚:

生成器函数和普通函数的区别究竟是什么?


一、什么是普通函数?

普通函数是我们在 Python 开发中最常见的函数形式。

它通常使用 def 定义,通过 return 返回执行结果。

def add(a, b):
    result = a + b
    return result

value = add(10, 20)
print(value)

运行结果:

30

普通函数的执行过程可以概括为:

调用函数
   ↓
从第一行开始执行
   ↓
执行函数内部代码
   ↓
遇到 return 返回结果
   ↓
函数结束

普通函数一旦被调用,就会立即执行函数体中的代码。

当遇到 return 时,函数会返回结果并结束运行。如果没有显式编写 return,Python 会默认返回 None

def say_hello():
    print("Hello, Python!")

result = say_hello()

print(result)

运行结果:

Hello, Python!
None

从执行模型来看,普通函数属于“一次性执行”。

函数开始运行后,要么正常执行完毕,要么因为异常而终止。函数结束后,其内部局部状态通常也随之释放。


二、什么是生成器函数?

生成器函数同样使用 def 定义,但函数体中包含 yield 关键字。

def generate_numbers():
    yield 1
    yield 2
    yield 3

需要注意的是,调用生成器函数时,函数体并不会立即执行。

generator = generate_numbers()

print(generator)

输出结果类似:

<generator object generate_numbers at 0x...>

这里得到的不是数字 123,而是一个生成器对象。

只有在使用 next() 获取数据时,生成器函数才真正开始执行。

generator = generate_numbers()

print(next(generator))
print(next(generator))
print(next(generator))

输出:

1
2
3

再次调用:

print(next(generator))

程序会抛出异常:

StopIteration

因为生成器已经没有更多数据可以产生。

生成器函数的执行过程可以表示为:

调用生成器函数
      ↓
返回生成器对象
      ↓
函数体暂不执行
      ↓
调用 next()
      ↓
执行到 yield
      ↓
返回当前值并暂停
      ↓
再次调用 next()
      ↓
从上次暂停位置继续执行

生成器函数最大的特点不是“返回多个值”,而是:

它可以在执行过程中暂停,并在下一次请求数据时从原位置继续运行。


三、核心区别一:return 和 yield 的行为不同

普通函数通常通过 return 返回结果,而生成器函数通过 yield 产生结果。

1. return 会终止函数

def normal_demo():
    print("开始执行")
    return 100
    print("这一行不会执行")

调用:

result = normal_demo()
print(result)

输出:

开始执行
100

执行到 return 后,函数立即结束。

return 后面的代码不会继续执行。

2. yield 会暂停函数

def generator_demo():
    print("第一次执行")
    yield 100

    print("第二次执行")
    yield 200

    print("执行结束")

调用:

generator = generator_demo()

print(next(generator))
print(next(generator))

输出:

第一次执行
100
第二次执行
200

每次执行到 yield,函数都会暂停,并保存当前执行状态。

下一次调用 next() 时,函数不会从头执行,而是从上一个 yield 后面继续。

这也是生成器能够实现“惰性计算”的基础。


四、核心区别二:普通函数立即执行,生成器函数延迟执行

来看一个直观的例子。

普通函数

def normal_function():
    print("普通函数开始运行")
    return 10

result = normal_function()

print("函数调用完成")
print(result)

输出:

普通函数开始运行
函数调用完成
10

调用普通函数时,函数体立即执行。

生成器函数

def generator_function():
    print("生成器开始运行")
    yield 10

generator = generator_function()

print("生成器已经创建")
print(next(generator))

输出:

生成器已经创建
生成器开始运行
10

可以看到:

generator = generator_function()

这一行只是创建了生成器对象,并没有真正执行函数体。

直到:

next(generator)

函数内部代码才开始运行。

这种“需要数据时再执行”的方式被称为:

惰性求值

英文通常称为:

Lazy Evaluation

五、核心区别三:返回值类型不同

普通函数调用后,返回的是实际计算结果。

def get_numbers():
    return [1, 2, 3]

result = get_numbers()

print(type(result))

输出:

<class 'list'>

生成器函数调用后,返回的是生成器对象。

def generate_numbers():
    yield 1
    yield 2
    yield 3

result = generate_numbers()

print(type(result))

输出:

<class 'generator'>

生成器对象同时也是迭代器。

因此,它支持:

next(generator)

也支持:

for value in generator:
    print(value)

示例:

def generate_numbers():
    yield 1
    yield 2
    yield 3

for number in generate_numbers():
    print(number)

输出:

1
2
3

六、核心区别四:内存占用不同

生成器最重要的实际价值之一,就是节省内存。

假设我们需要生成一百万个整数。

使用普通函数和列表

def create_numbers(n):
    numbers = []

    for i in range(n):
        numbers.append(i)

    return numbers

data = create_numbers(1_000_000)

这个函数会先创建一个包含一百万个整数的列表,然后一次性返回。

所有数据都要保存在内存中。

使用生成器函数

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

data = generate_numbers(1_000_000)

生成器不会一次性创建一百万个整数。

它每次只生成一个值。

for number in data:
    print(number)

每次循环只处理当前需要的数据。

可以使用 sys.getsizeof() 做一个简单对比:

import sys

number_list = [i for i in range(1_000_000)]
number_generator = (i for i in range(1_000_000))

print(sys.getsizeof(number_list))
print(sys.getsizeof(number_generator))

在不同 Python 版本和运行环境中,具体数值会有所不同,但通常可以观察到:

列表占用数 MB 甚至更多
生成器对象只占用几百字节

这是因为列表保存了全部元素,而生成器主要保存的是:

  • 当前执行位置
  • 局部变量
  • 运行状态
  • 生成逻辑

生成器并不提前保存所有数据。


七、核心区别五:普通函数没有暂停状态,生成器会保存上下文

生成器函数暂停时,会保存当前上下文。

例如:

def counter():
    current = 0

    while current < 3:
        print(f"当前状态:{current}")
        yield current
        current += 1

调用:

generator = counter()

print(next(generator))
print(next(generator))
print(next(generator))

输出:

当前状态:0
0
当前状态:1
1
当前状态:2
2

变量 current 的值没有在每次调用 next() 时重新变成 0

这是因为生成器保存了函数运行现场,包括:

局部变量
执行位置
异常处理状态
循环状态

普通函数执行完成后,调用栈通常会被释放。

生成器则可以在暂停和恢复之间保留运行环境。


八、普通函数和生成器函数对比表

对比维度 普通函数 生成器函数
关键字 return yield
调用后是否立即执行
返回结果 实际值 生成器对象
是否可以暂停 不可以 可以
是否保存执行状态 通常不保存 自动保存
数据生成方式 一次性计算 按需生成
内存占用 可能较高 通常较低
是否可直接使用 next() 通常不可以 可以
是否适合大数据 视情况而定 非常适合
是否可重复遍历 取决于返回对象 通常只能消费一次
典型场景 常规计算、业务逻辑 流式处理、大文件、数据管道

九、实战案例一:读取超大日志文件

假设服务器每天产生一个数十 GB 的日志文件。

如果使用普通函数一次性读取:

def read_log_file(file_path):
    with open(file_path, "r", encoding="utf-8") as file:
        return file.readlines()

调用:

logs = read_log_file("server.log")

for line in logs:
    print(line)

readlines() 会将全部内容读入内存。

当日志文件非常大时,程序可能出现:

内存占用过高
程序响应变慢
系统开始交换内存
进程被操作系统终止

更合理的方式是使用生成器。

def read_log_file(file_path):
    with open(file_path, "r", encoding="utf-8") as file:
        for line in file:
            yield line.rstrip("\n")

调用:

for log_line in read_log_file("server.log"):
    print(log_line)

这种方式一次只读取一行。

进一步过滤错误日志:

def read_log_file(file_path):
    with open(file_path, "r", encoding="utf-8") as file:
        for line in file:
            yield line.rstrip("\n")


def filter_error_logs(lines):
    for line in lines:
        if "ERROR" in line:
            yield line


logs = read_log_file("server.log")
error_logs = filter_error_logs(logs)

for error in error_logs:
    print(error)

这段代码形成了一个流式数据管道:

日志文件
   ↓
逐行读取
   ↓
筛选 ERROR
   ↓
逐条输出

整个过程不需要把全部日志同时放入内存。


十、实战案例二:分批处理数据库数据

在实际项目中,一张数据库表可能有几百万条记录。

如果一次性查询全部数据:

rows = database.query("SELECT * FROM orders")

可能带来很大的内存压力。

可以使用生成器分批获取。

def fetch_orders_in_batches(database, batch_size=1000):
    offset = 0

    while True:
        rows = database.query(
            "SELECT * FROM orders LIMIT %s OFFSET %s",
            (batch_size, offset),
        )

        if not rows:
            break

        yield rows
        offset += batch_size

使用方式:

for batch in fetch_orders_in_batches(database):
    for order in batch:
        process_order(order)

这样每次只处理一批数据。

更进一步,还可以逐条产生记录:

def fetch_orders(database, batch_size=1000):
    for batch in fetch_orders_in_batches(database, batch_size):
        for order in batch:
            yield order

调用:

for order in fetch_orders(database):
    process_order(order)

上层业务代码不需要关心分页细节。

生成器将复杂的数据获取过程封装成了一个连续的数据流。

不过在真实数据库场景中,处理超大表时应优先考虑主键游标分页,而不是大量使用 OFFSET,因为偏移量越大,数据库查询成本往往越高。

例如:

def fetch_orders(database, batch_size=1000):
    last_id = 0

    while True:
        rows = database.query(
            """
            SELECT *
            FROM orders
            WHERE id > %s
            ORDER BY id
            LIMIT %s
            """,
            (last_id, batch_size),
        )

        if not rows:
            break

        for row in rows:
            yield row

        last_id = rows[-1]["id"]

这种方式通常更适合大规模数据处理。


十一、实战案例三:构建数据处理流水线

生成器非常适合构建模块化的数据管道。

假设我们需要处理一批用户数据:

  1. 读取原始数据;
  2. 删除无效数据;
  3. 标准化姓名;
  4. 筛选成年用户;
  5. 输出结果。

可以使用多个生成器逐层处理。

def read_users(users):
    for user in users:
        yield user


def remove_invalid_users(users):
    for user in users:
        if user.get("name") and user.get("age") is not None:
            yield user


def normalize_names(users):
    for user in users:
        normalized = user.copy()
        normalized["name"] = normalized["name"].strip().title()
        yield normalized


def filter_adults(users):
    for user in users:
        if user["age"] >= 18:
            yield user

准备数据:

raw_users = [
    {"name": " alice ", "age": 20},
    {"name": "", "age": 25},
    {"name": "bob", "age": 16},
    {"name": " charlie", "age": 32},
]

组合处理流程:

users = read_users(raw_users)
users = remove_invalid_users(users)
users = normalize_names(users)
users = filter_adults(users)

for user in users:
    print(user)

输出:

{'name': 'Alice', 'age': 20}
{'name': 'Charlie', 'age': 32}

这种设计的优势在于:

  • 每个函数职责单一;
  • 每个处理阶段可以单独测试;
  • 数据按需流动;
  • 中间结果不必全部存储;
  • 新增处理步骤非常方便。

例如加入邮箱标准化:

def normalize_emails(users):
    for user in users:
        normalized = user.copy()

        if "email" in normalized:
            normalized["email"] = normalized["email"].strip().lower()

        yield normalized

只需要把它插入流水线即可。


十二、生成器只能遍历一次

这是初学者经常踩到的坑。

def generate_numbers():
    yield 1
    yield 2
    yield 3

numbers = generate_numbers()

print(list(numbers))
print(list(numbers))

输出:

[1, 2, 3]
[]

第一次调用 list(numbers) 时,生成器已经被完全消费。

第二次再遍历时,它已经到达终点,因此得到空列表。

如果需要再次遍历,应重新创建生成器:

print(list(generate_numbers()))
print(list(generate_numbers()))

输出:

[1, 2, 3]
[1, 2, 3]

也可以使用生成器工厂:

def number_factory():
    return (number for number in range(1, 4))

first = number_factory()
second = number_factory()

每次调用工厂函数都会创建一个新的生成器对象。


十三、生成器中的 return 有什么作用?

生成器函数中也可以使用 return

但这里的 return 不是像普通函数一样直接把值交给 for 循环,而是表示生成器结束。

def generate_numbers():
    yield 1
    yield 2
    return
    yield 3

遍历:

for number in generate_numbers():
    print(number)

输出:

1
2

yield 3 不会执行。

生成器中的:

return value

实际上会触发:

StopIteration(value)

例如:

def generator():
    yield 1
    return "生成完成"

g = generator()

print(next(g))

try:
    next(g)
except StopIteration as error:
    print(error.value)

输出:

1
生成完成

在普通 for 循环中,StopIteration 会被自动处理,因此通常看不到这个返回值。


十四、yield from:把子生成器交给外层生成器

当一个生成器需要遍历另一个可迭代对象时,可以使用 yield from

普通写法:

def generate_all():
    for number in [1, 2, 3]:
        yield number

    for letter in ["A", "B", "C"]:
        yield letter

使用 yield from

def generate_all():
    yield from [1, 2, 3]
    yield from ["A", "B", "C"]

调用:

print(list(generate_all()))

输出:

[1, 2, 3, 'A', 'B', 'C']

yield from 不只是语法缩写。

它还可以在生成器之间传递:

  • 数据;
  • 异常;
  • send() 发送的值;
  • 子生成器的返回值。

递归展开嵌套列表时,yield from 非常实用。

def flatten(items):
    for item in items:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

调用:

nested = [1, [2, 3], [4, [5, 6]]]

print(list(flatten(nested)))

输出:

[1, 2, 3, 4, 5, 6]

十五、生成器表达式和生成器函数有什么区别?

除了生成器函数,Python 还支持生成器表达式。

列表推导式:

numbers = [x * x for x in range(10)]

生成器表达式:

numbers = (x * x for x in range(10))

两者最大的区别是括号和执行方式。

print(type([x for x in range(3)]))
print(type(x for x in range(3)))

输出:

<class 'list'>
<class 'generator'>

生成器表达式适合简单逻辑:

even_numbers = (x for x in range(100) if x % 2 == 0)

生成器函数更适合复杂流程:

def generate_even_numbers(limit):
    for number in range(limit):
        if number % 2 == 0:
            yield number

一般来说:

逻辑简单:使用生成器表达式
逻辑复杂:使用生成器函数

十六、生成器的 send 方法

生成器不仅可以向外产生数据,还可以通过 send() 接收数据。

def receiver():
    while True:
        value = yield
        print(f"接收到:{value}")

使用:

generator = receiver()

next(generator)

generator.send("Python")
generator.send("Generator")

输出:

接收到:Python
接收到:Generator

第一次必须调用:

next(generator)

或:

generator.send(None)

把生成器推进到第一个 yield

这是因为生成器启动前,还没有暂停在某个可以接收数据的位置。

再看一个简单的累计器:

def accumulator():
    total = 0

    while True:
        value = yield total

        if value is None:
            break

        total += value

调用:

generator = accumulator()

print(next(generator))
print(generator.send(10))
print(generator.send(20))
print(generator.send(5))

输出:

0
10
30
35

虽然现代 Python 异步开发更多使用 asyncawait,但理解 send() 有助于深入理解协程的历史与底层思想。


十七、如何正确关闭生成器?

生成器对象支持 close() 方法。

def generate_data():
    try:
        yield 1
        yield 2
        yield 3
    finally:
        print("释放资源")

调用:

generator = generate_data()

print(next(generator))
generator.close()

输出:

1
释放资源

调用 close() 后,Python 会在生成器内部触发 GeneratorExit

这意味着生成器可以通过 try...finally 释放资源。

但在处理文件时,更推荐直接将资源管理放在 with 语句中。

def read_lines(file_path):
    with open(file_path, "r", encoding="utf-8") as file:
        for line in file:
            yield line

当生成器正常结束或被正确关闭时,文件会被释放。


十八、性能测试:生成器一定更快吗?

很多人会把“生成器更省内存”误解为“生成器一定更快”。

实际上,两者不能简单画等号。

生成器的主要优势是:

降低峰值内存
支持流式处理
减少无效计算
缩短首个结果等待时间

但在某些需要反复访问全部数据的场景中,列表可能更快。

可以进行简单测试:

import time


def list_version(n):
    return [x * x for x in range(n)]


def generator_version(n):
    return (x * x for x in range(n))


n = 1_000_000

start = time.perf_counter()
numbers = list_version(n)
result = sum(numbers)
print("列表版本:", time.perf_counter() - start)

start = time.perf_counter()
numbers = generator_version(n)
result = sum(numbers)
print("生成器版本:", time.perf_counter() - start)

测试结果会受到 Python 版本、硬件和运行环境影响。

通常应该从业务需求出发:

  • 数据量大,优先考虑生成器;
  • 只遍历一次,适合生成器;
  • 需要快速获得第一个结果,适合生成器;
  • 需要索引、切片和重复访问,列表更方便;
  • 数据量很小,不必为了生成器增加复杂度。

生成器不是列表的替代品,而是面向不同问题的数据处理工具。


十九、什么时候应该使用普通函数?

以下场景更适合普通函数。

1. 返回单个明确结果

def calculate_total(price, quantity):
    return price * quantity

2. 数据规模较小

def get_weekdays():
    return [
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
    ]

3. 结果需要多次遍历

users = load_users()

for user in users:
    validate(user)

for user in users:
    save(user)

如果 load_users() 返回列表,就可以方便地重复遍历。

4. 需要随机访问

numbers = get_numbers()

print(numbers[100])
print(numbers[-1])
print(numbers[10:20])

生成器不支持直接索引和切片。


二十、什么时候应该使用生成器函数?

以下场景通常非常适合生成器。

1. 处理大文件

def read_large_file(file_path):
    with open(file_path, encoding="utf-8") as file:
        for line in file:
            yield line

2. 处理无限序列

def infinite_counter(start=0):
    current = start

    while True:
        yield current
        current += 1

调用:

counter = infinite_counter(100)

print(next(counter))
print(next(counter))
print(next(counter))

输出:

100
101
102

无限序列无法一次性存入列表,但可以由生成器持续产生。

3. 流式数据处理

def parse_events(event_stream):
    for event in event_stream:
        if event.is_valid():
            yield event.to_dict()

4. 分页接口处理

def fetch_all_pages(client):
    page = 1

    while True:
        response = client.fetch(page=page)
        items = response["items"]

        if not items:
            break

        yield from items
        page += 1

5. 提前终止的数据搜索

def generate_candidates():
    for number in range(10_000_000):
        yield number

查找第一个符合条件的值:

result = next(
    number
    for number in generate_candidates()
    if number > 1000 and number % 97 == 0
)

print(result)

生成器只计算到找到目标为止,后续数据不会被生成。


二十一、常见错误与解决方案

错误一:忘记生成器不会立即执行

def task():
    print("任务执行")
    yield 1

task()

这段代码不会输出任何内容。

正确方式:

generator = task()
next(generator)

或者:

for value in task():
    print(value)

错误二:重复消费同一个生成器

numbers = (x for x in range(5))

print(sum(numbers))
print(sum(numbers))

输出:

10
0

解决方式是重新创建:

def create_numbers():
    return (x for x in range(5))

print(sum(create_numbers()))
print(sum(create_numbers()))

错误三:需要列表操作却返回生成器

numbers = (x for x in range(10))

print(numbers[0])

会报错:

TypeError: 'generator' object is not subscriptable

需要随机访问时,应转成列表:

numbers = list(numbers)

print(numbers[0])

错误四:生成器内部资源未及时释放

def read_file(path):
    file = open(path, encoding="utf-8")

    for line in file:
        yield line

这种写法在生成器未完全消费时,文件资源可能不能及时释放。

推荐使用:

def read_file(path):
    with open(path, encoding="utf-8") as file:
        for line in file:
            yield line

使用方也可以显式关闭:

generator = read_file("data.txt")

try:
    print(next(generator))
finally:
    generator.close()

二十二、生成器最佳实践

1. 保持单一职责

不要让一个生成器承担太多业务逻辑。

不推荐:

def process_everything(data):
    for item in data:
        # 校验
        # 清洗
        # 转换
        # 保存
        # 记录日志
        yield item

推荐拆分:

def validate(items):
    for item in items:
        if item:
            yield item


def transform(items):
    for item in items:
        yield item.strip().lower()


def save(items):
    for item in items:
        save_to_database(item)
        yield item

2. 明确生成器是一次性对象

函数命名和文档中应说明结果只能消费一次。

def iter_active_users(users):
    """逐个产生活跃用户。

    返回一次性生成器对象。
    """
    for user in users:
        if user.is_active:
            yield user

使用 iter_ 作为命名前缀,是一种常见且清晰的表达方式。

3. 使用 yield from 简化嵌套逻辑

def iter_records(files):
    for file_path in files:
        yield from read_records(file_path)

比下面的写法更简洁:

def iter_records(files):
    for file_path in files:
        for record in read_records(file_path):
            yield record

4. 不要为了使用生成器而使用生成器

下面这种代码没有明显收益:

def get_status():
    yield "success"

如果永远只返回一个简单值,普通函数通常更合适:

def get_status():
    return "success"

5. 配合 itertools 使用

Python 标准库中的 itertools 为迭代器和生成器提供了大量高效工具。

from itertools import islice

numbers = (x * x for x in range(1_000_000))

first_ten = islice(numbers, 10)

print(list(first_ten))

还可以使用:

from itertools import chain, count, cycle, repeat

例如合并多个数据流:

from itertools import chain

users_a = ["Alice", "Bob"]
users_b = ["Charlie", "David"]

all_users = chain(users_a, users_b)

print(list(all_users))

二十三、普通函数与生成器的选择原则

在工程开发中,可以通过下面几个问题快速判断。

问题一:数据量是否很大?

如果数据可能达到数十万、数百万甚至更多,优先考虑生成器。

问题二:是否只需要遍历一次?

如果数据只需要消费一次,生成器通常很合适。

问题三:是否需要索引和切片?

如果需要:

data[0]
data[-1]
data[10:20]

更适合使用列表、元组等容器。

问题四:是否需要尽快得到第一个结果?

生成器可以边计算边返回,更适合低延迟场景。

问题五:是否需要重复遍历?

如果需要重复遍历,应返回容器,或者提供一个每次都能创建新生成器的工厂函数。

可以总结为:

返回完整结果:普通函数
逐步产生结果:生成器函数
数据规模较小:优先保证代码清晰
数据规模巨大:优先考虑流式处理
需要重复访问:使用列表或重新创建生成器
只消费一次:使用生成器

二十四、一个综合实战:统计大型日志中的接口错误

下面通过一个完整案例,展示生成器在真实项目中的应用。

需求如下:

  1. 逐行读取日志;
  2. 只处理 JSON 格式日志;
  3. 过滤错误请求;
  4. 按接口统计错误次数;
  5. 输出错误最多的接口。

示例日志:

{"path": "/api/users", "status": 200}
{"path": "/api/orders", "status": 500}
{"path": "/api/users", "status": 404}
{"path": "/api/orders", "status": 503}

第一步,逐行读取:

def read_lines(file_path):
    with open(file_path, "r", encoding="utf-8") as file:
        for line in file:
            line = line.strip()

            if line:
                yield line

第二步,解析 JSON:

import json


def parse_json_lines(lines):
    for line in lines:
        try:
            yield json.loads(line)
        except json.JSONDecodeError:
            continue

第三步,过滤错误请求:

def filter_error_requests(records):
    for record in records:
        status = record.get("status", 0)

        if status >= 400:
            yield record

第四步,统计接口错误:

from collections import Counter


def count_errors(records):
    counter = Counter()

    for record in records:
        path = record.get("path", "unknown")
        counter[path] += 1

    return counter

组合流程:

def analyze_log(file_path):
    lines = read_lines(file_path)
    records = parse_json_lines(lines)
    errors = filter_error_requests(records)

    return count_errors(errors)

调用:

result = analyze_log("access.log")

for path, count in result.most_common():
    print(f"{path}: {count}")

整个处理过程可表示为:

access.log
    ↓
read_lines
    ↓
parse_json_lines
    ↓
filter_error_requests
    ↓
count_errors
    ↓
统计结果

前面三个步骤全部采用生成器,因此无论日志文件多大,都不需要一次性加载全部内容。

只有最终的统计字典需要保存在内存中。

这正是生成器在工程中的典型价值:

把庞大的数据集变成持续流动的小数据片段。


二十五、总结:生成器不仅是一种语法,更是一种思维方式

普通函数和生成器函数最本质的区别,可以归纳为一句话:

普通函数负责计算并返回结果,生成器函数负责按需产生结果。

普通函数通过 return 一次性结束执行,而生成器函数通过 yield 暂停执行并保存状态。

普通函数更适合:

  • 常规业务逻辑;
  • 返回单个结果;
  • 返回小规模集合;
  • 需要重复访问的数据;
  • 需要索引和切片的场景。

生成器函数更适合:

  • 大文件读取;
  • 海量数据处理;
  • 流式计算;
  • 分页接口;
  • 无限序列;
  • 数据处理管道;
  • 按需计算;
  • 提前终止的搜索任务。

学习生成器时,不必一开始就深入研究所有内部细节。

可以先记住三个关键点:

调用生成器函数不会立即执行
yield 会返回数据并暂停
生成器通常只能遍历一次

当你开始面对大日志、数据库批处理、接口分页和实时数据流时,就会真正体会到生成器的价值。

它不会让所有数据同时涌入内存,而是让数据在程序中有节奏地流动。

这种“只在需要时工作,只产生当前需要的数据”的设计思想,也体现了 Python 编程中简洁、克制而高效的一面。

最后,也欢迎你思考两个问题:

你在实际项目中,是否遇到过因为一次性加载大量数据而导致内存占用过高的问题?

在你的业务场景中,还有哪些普通函数可以改造成生成器,从而让程序更加高效和优雅?

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐