Python map、zip、filter 三大内置函数实战精讲

Python map、zip、filter 三大内置函数实战精讲

1. 为什么这三个函数值得你花一整个下午认真琢磨

刚学 Python 的时候,我总以为写循环就是天经地义的事——for item in data:一敲,逻辑就跑起来了。直到有次在 Code Review 里被一位老同事指着一段嵌套三层的 for 循环说:“这代码读起来像在解九连环,能不能先让它呼吸一下?”我才意识到:不是所有遍历都非得靠for来扛。mapzipfilter这三个内置函数,表面看只是语法糖,实则是一把把精巧的“思维减负刀”:它们不改变功能,但能瞬间剥离掉大量与业务无关的控制结构噪音,让代码真正聚焦在“你想对数据做什么”,而不是“你怎么一步步挪动指针”。

这三个函数共同构成了 Python 函数式编程的最小可行三角——转换(map)、配对(zip)、筛选(filter)。它们不依赖外部库,不引入新概念,却能在日常数据清洗、API 响应处理、配置批量生成等高频场景中,把原本需要 8 行的逻辑压缩成 1 行可读性更强的表达式。比如,你有一组用户 ID 列表,要批量查数据库并提取昵称;或者你有两个传感器的时间戳序列,需要按时间对齐做差值计算;又或者你从 Excel 导入了一千条订单记录,要快速剔除金额为零或状态异常的脏数据——这些都不是理论题,而是我上周帮运营团队写自动化报表时真实遇到的三类问题。而解决它们的核心,恰恰就是mapzipfilter的组合使用。

更重要的是,它们天然适配 Python 的惰性求值机制。mapfilter返回的是迭代器,不是立刻生成全部结果的列表。这意味着当你处理百万级日志行、GB 级 CSV 文件,甚至实时流数据时,它们不会一次性把所有中间结果塞进内存,而是“用到哪,算到哪”。这种设计不是为了炫技,而是直面工程现实:内存永远比 CPU 更稀缺。我曾用filter替换一个for+append的脏数据过滤逻辑,内存峰值从 1.2GB 降到 47MB,而执行时间反而快了 13%,因为避免了频繁的内存分配和垃圾回收。这不是优化技巧,这是 Python 内置的工程直觉。

你不需要成为函数式编程信徒才能用好它们。事实上,我建议初学者先忘掉“函数式”这个词,把它当成三个更聪明的for循环替代品来理解。它们的参数签名极简,行为边界清晰,错误反馈直接——传错类型?报TypeError;传空对象?返回空迭代器;传 None 作函数?map会帮你自动做恒等映射。没有隐藏状态,没有魔法方法,只有输入、处理、输出的干净链条。这篇文章,就是我过去三年在真实项目中反复打磨、验证、踩坑后整理出的实战手册。它不讲抽象理论,只告诉你:什么时候该用、怎么用得稳、为什么这么用、以及最容易在哪一步栽跟头。

2. 核心原理与设计哲学:为什么是这三个,而不是别的?

2.1 map:单输入 → 单输出的确定性转换引擎

map(function, iterable)的本质,是将一个纯函数(pure function)应用到可迭代对象的每一个元素上,生成一个新的迭代器。这里的关键词是“纯函数”和“确定性”。所谓纯函数,是指给定相同输入,永远返回相同输出,且不产生任何副作用(比如修改全局变量、写文件、发网络请求)。map的设计哲学非常朴素:它只负责“调用”,不负责“判断”;只传递“数据”,不携带“状态”。

举个最典型的例子:把一串字符串数字转成整数。

data = ['1', '2', '3', '4'] result = map(int, data)

这段代码的执行过程,可以拆解为三步:

  1. map拿到int这个类型构造函数(它本身就是一个可调用对象)和data这个列表;
  2. 它内部维护一个游标,逐个取出data中的元素'1','2','3','4'
  3. 对每个元素,调用int(element),并将返回值(1,2,3,4)放入结果迭代器。

注意,此时result是一个map对象,不是列表。你必须显式调用list(result)才能看到[1, 2, 3, 4]。这个“惰性”特性是核心设计选择:如果data有 100 万个元素,map不会立刻创建一个包含 100 万个整数的新列表,而是只准备好“当你要第 500001 个时,我就去算它”的能力。这背后是 Python 的迭代器协议(__iter____next__方法),它让map能无缝接入for循环、生成器表达式,甚至itertools工具链。

提示:map允许传入None作为第一个参数,此时它会执行恒等映射(identity mapping),即map(None, iterable)等价于iterable本身。但这在 Python 3 中已被移除,属于历史遗留知识,实际开发中请勿使用。

2.2 zip:多序列的“时间对齐器”与“结构重组器”

zip(*iterables)的名字很形象——它像拉链(zipper)一样,把多个可迭代对象“拉”在一起。它的核心行为是:取每个可迭代对象的第 i 个元素,打包成一个元组,作为结果迭代器的第 i 个元素。当任意一个输入序列耗尽时,zip立即停止,不会填充空值。这个“最短原则”(shortest-first)是zip最关键的设计约束,也是新手最容易误解的地方。

想象你有两个传感器:温度计每秒记录一次,湿度计每 1.5 秒记录一次。你拿到两段原始数据:

temp_readings = [23.1, 23.2, 23.0, 23.3] humidity_readings = [45, 46, 44]

zip(temp_readings, humidity_readings)得到的结果是:

[(23.1, 45), (23.2, 46), (23.0, 44)]

它自动截断了温度数据的最后一个值23.3,因为湿度数据已经没了。这不是 bug,而是zip的明确契约:它不做插值、不补缺失、不猜测意图,只做严格的一一对应。这种“宁缺毋滥”的设计,恰恰保证了数据对齐的绝对可靠性。在金融风控系统中,我们用zip同时处理交易流水和对应的风控评分,一旦某笔交易没拿到评分,zip就会自然丢弃它,避免了因数据错位导致的误判。

zip的另一个强大能力是“解包”(unpacking)。当你有一个由元组组成的列表,想把它拆回原来的多个序列时,只需在前面加个*

paired_data = [('a', 1), ('b', 2), ('c', 3)] letters, numbers = zip(*paired_data) # letters=('a','b','c'), numbers=(1,2,3)

这行代码的执行逻辑是:zip(*paired_data)等价于zip(('a',1), ('b',2), ('c',3)),而zip会把每个元组的第一个元素拉出来组成一个元组,第二个元素拉出来组成另一个元组。这是一种极其优雅的“矩阵转置”操作,比手写嵌套循环清晰十倍。

2.3 filter:基于布尔逻辑的“数据守门员”

filter(function, iterable)的职责非常专一:它遍历iterable的每个元素,用function对其进行测试,只保留那些让function(element)返回True的元素function在这里扮演的是“守门员”的角色,它的返回值决定了数据的去留。

关键点在于function的返回值类型。Python 的布尔上下文(truthy/falsy)规则在这里起作用:0,None,[],{},""等被视为False,其余大部分值为True。因此,你可以传入一个返回布尔值的函数,也可以传入None,此时filter会自动过滤掉所有 falsy 值:

mixed_data = [0, 1, '', 'hello', [], [1,2], None, 42] truthy_only = list(filter(None, mixed_data)) # [1, 'hello', [1, 2], 42]

这个None用法是filter的一个隐藏彩蛋,特别适合清理混合数据中的空值。但要注意,它无法区分0(数值零)和''(空字符串)——两者都是 falsy,都会被干掉。如果你只想剔除空字符串而保留0,就必须写一个明确的函数:

def non_empty_string(x): return isinstance(x, str) and len(x) > 0 # 或者用 lambda: lambda x: isinstance(x, str) and x

filter的设计哲学是“声明式过滤”。你告诉它“我要什么”,而不是“怎么一步步挑出来”。这带来的好处是逻辑可组合性极强。比如,你想从用户列表中找出“年龄大于 18 且邮箱已验证”的人,可以这样写:

adults = filter(lambda u: u.age > 18, users) verified_adults = filter(lambda u: u.email_verified, adults)

虽然这里用了两次filter,但底层迭代器是链式调用的,内存占用仍是 O(1),而不是创建两个中间列表。这种“管道式”(pipeline)思维,正是函数式风格的精髓所在。

3. 实操详解:从入门到写出生产级代码的完整路径

3.1 map 的五种典型用法与避坑指南

场景一:基础类型转换(最常用,也最容易翻车)
# ✅ 正确:字符串转浮点 prices_str = ['19.99', '29.50', '9.95'] prices_float = list(map(float, prices_str)) # ❌ 错误:混入非法字符 prices_dirty = ['19.99', '29.50', 'free', '9.95'] # list(map(float, prices_dirty)) 会抛出 ValueError: could not convert string to float: 'free'

避坑心得map不做容错。一旦输入数据质量不可控,必须搭配try/except或预处理。我的标准做法是写一个安全转换函数:

def safe_float(s): try: return float(s) except (ValueError, TypeError): return 0.0 # 或返回 None,取决于业务需求 prices_safe = list(map(safe_float, prices_dirty))
场景二:自定义函数处理(释放业务逻辑)

假设你有一组用户字典,需要提取姓名并转为大写:

users = [ {'id': 1, 'name': 'alice', 'email': 'a@x.com'}, {'id': 2, 'name': 'bob', 'email': 'b@y.com'}, {'id': 3, 'name': 'charlie', 'email': 'c@z.com'} ] # ✅ 推荐:用命名函数,语义清晰 def get_upper_name(user): return user['name'].upper() names_upper = list(map(get_upper_name, users)) # ['ALICE', 'BOB', 'CHARLIE'] # ⚠️ 谨慎:lambda 在复杂逻辑中可读性差 # names_upper = list(map(lambda u: u['name'].upper(), users)) # 可以,但不推荐用于多行逻辑

实操心得:我坚持一个原则——lambda 只用于单表达式、无副作用、生命周期短的操作。一旦逻辑涉及属性访问、条件判断、字符串拼接等,立刻拆成独立函数。这不仅提升可读性,更便于单元测试和复用。上周重构一个电商价格计算模块时,我把 7 个嵌套 lambda 全部替换成命名函数,测试覆盖率从 42% 直接升到 89%。

场景三:多参数函数的 map 化(常被忽略的技巧)

map默认只支持单参数函数。但如果你有一个需要两个参数的函数,比如pow(base, exp),如何用map处理两组底数和指数?

bases = [2, 3, 4] exponents = [2, 3, 4] # ✅ 正确:用 zip 预组装参数对,再用 lambda 解包 powers = list(map(lambda pair: pow(pair[0], pair[1]), zip(bases, exponents))) # 或更清晰的写法: powers = list(map(lambda b, e: pow(b, e), bases, exponents))

原理说明map其实支持多个可迭代对象!当你传入map(func, iter1, iter2, ...)时,func会被调用n次(n是最短序列长度),每次调用时,iter1[i],iter2[i], ... 作为参数依次传入func。所以上面的lambda b, e: pow(b, e)会分别收到(2,2),(3,3),(4,4)

场景四:与生成器表达式协同(性能最优组合)

map和生成器表达式(generator expression)是黄金搭档。生成器表达式语法更灵活,map在简单函数调用时更简洁。我通常这样决策:

  • map(func, iterable):当func是现成的、无参数的内置函数(如int,str.upper,len);
  • 用生成器表达式:当需要内联计算、条件过滤或调用带参数的函数。
# ✅ map 更简洁 numbers = [1, 2, 3, 4] squares_map = list(map(lambda x: x**2, numbers)) # ✅ 生成器表达式更直观(尤其带条件时) squares_gen = [x**2 for x in numbers if x % 2 == 0] # 只算偶数的平方
场景五:处理嵌套结构(真实项目高频痛点)

在解析 JSON API 响应时,经常遇到嵌套列表。比如,一个订单列表,每个订单有多个商品项:

orders = [ { 'id': 'ORD-001', 'items': [{'name': 'book', 'price': 12.99}, {'name': 'pen', 'price': 1.50}] }, { 'id': 'ORD-002', 'items': [{'name': 'notebook', 'price': 8.99}] } ] # ✅ 目标:提取所有商品名称 # 方案1:两层 map(清晰但略啰嗦) all_names = [] for order in orders: names_in_order = list(map(lambda item: item['name'], order['items'])) all_names.extend(names_in_order) # ✅ 方案2:用 itertools.chain.from_iterable(推荐) from itertools import chain all_names = list(chain.from_iterable( map(lambda order: map(lambda item: item['name'], order['items']), orders) ))

深度解析:内层map(lambda item: item['name'], order['items'])对每个订单的商品列表做映射,返回一个map对象(即一个迭代器);外层map对所有订单做这个操作,返回一个由多个map对象组成的迭代器;最后chain.from_iterable把这个“迭代器的迭代器”拍平成一个单一迭代器。这比写三层 for 循环少 5 行代码,且内存友好。

3.2 zip 的四种高阶用法与实战陷阱

场景一:并行遍历与索引绑定(替代 enumerate)

enumerate是获取索引的好工具,但当你需要同时遍历多个序列并绑定索引时,zip更强大:

# ✅ 同时遍历两个列表,并知道当前索引 fruits = ['apple', 'banana', 'cherry'] colors = ['red', 'yellow', 'red'] for i, (fruit, color) in enumerate(zip(fruits, colors)): print(f"Index {i}: {fruit} is {color}") # 输出: # Index 0: apple is red # Index 1: banana is yellow # Index 2: cherry is red

为什么不用range(len(...))因为zip自动处理长度不一致,而range(len(fruits))如果colors更短,colors[i]会报IndexErrorzip的安全性是硬性保障。

场景二:构建字典(最 Pythonic 的方式)

从两个等长列表快速构建字典,zip是唯一推荐方案:

keys = ['name', 'age', 'city'] values = ['Alice', 28, 'Beijing'] user_dict = dict(zip(keys, values)) # {'name': 'Alice', 'age': 28, 'city': 'Beijing'}

原理dict()构造函数接受一个可迭代对象,其中每个元素是一个二元组(key, value)。zip(keys, values)正好生成('name', 'Alice'),('age', 28),('city', 'Beijing')这样的元组流。

场景三:矩阵转置(数据科学必备)

在 Pandas 或 NumPy 不可用的轻量脚本中,zip(*matrix)是转置二维列表的杀手锏:

matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] transposed = list(zip(*matrix)) # [(1, 4, 7), (2, 5, 8), (3, 6, 9)] # ✅ 如果需要真正的列表(而非元组),再 map 一层 transposed_lists = [list(row) for row in zip(*matrix)] # [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
场景四:与 filter 组合实现“条件配对”(高级技巧)

有时你不想简单配对,而是只配对满足某个条件的元素。比如,只把正数温度和对应湿度配对:

temps = [-5.2, 23.1, -2.0, 23.3, 24.0] hums = [30, 45, 28, 46, 44] # ✅ 步骤1:用 filter 找出正数温度的索引 positive_indices = [i for i, t in enumerate(temps) if t > 0] # ✅ 步骤2:用 zip 配对这些索引位置的值 paired_positive = list(zip( [temps[i] for i in positive_indices], [hums[i] for i in positive_indices] )) # [(23.1, 45), (23.3, 46), (24.0, 44)] # ✅ 更优雅:用 map + zip + filter 一行搞定(需理解顺序) # 先 zip,再 filter,再 map 解包(如果需要) valid_pairs = list(filter(lambda pair: pair[0] > 0, zip(temps, hums))) # [ (23.1, 45), (23.3, 46), (24.0, 44) ]

3.3 filter 的三种核心模式与生产环境加固

模式一:布尔函数过滤(标准用法)
# ✅ 筛选偶数 numbers = range(1, 11) evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4, 6, 8, 10] # ✅ 筛选非空字符串(注意:'0' 是字符串,非空) strings = ['hello', '', 'world', '0', None] non_empty = list(filter(lambda s: s and isinstance(s, str), strings)) # ['hello', 'world', '0']
模式二:None 过滤(高效清理混合数据)
# ✅ 清理可能含 None、空列表、空字典的列表 mixed = [1, None, [1,2], {}, '', 'ok', 0] # 注意:0 是 falsy,会被过滤掉 cleaned = list(filter(None, mixed)) # [1, [1, 2], 'ok'] # ✅ 如果想保留 0,必须显式判断 cleaned_keep_zero = list(filter(lambda x: x is not None and not (isinstance(x, (str, list, dict)) and len(x) == 0), mixed)) # [1, [1, 2], 'ok', 0]
模式三:与 map 链式组合(构建数据处理流水线)

这是我在 ETL 脚本中最常用的模式。假设你从 CSV 读取原始数据,需要:清洗(去空)、转换(字符串转数字)、过滤(剔除异常值):

import csv from io import StringIO # 模拟 CSV 数据 csv_data = """name,age,score alice,25,89 bob,,95 charlie,30, david,22,78""" # ✅ 生产级写法:链式调用,惰性求值 raw_rows = csv.DictReader(StringIO(csv_data)) # Step1: 过滤掉 age 或 score 为空的行 filtered_rows = filter(lambda row: row['age'] and row['score'], raw_rows) # Step2: 转换 age 和 score 为整数 converted_rows = map(lambda row: { 'name': row['name'], 'age': int(row['age']), 'score': int(row['score']) }, filtered_rows) # Step3: 过滤掉分数异常的(<0 或 >100) final_data = list(filter(lambda row: 0 <= row['score'] <= 100, converted_rows)) # [{'name': 'alice', 'age': 25, 'score': 89}, {'name': 'david', 'age': 22, 'score': 78}]

关键优势:整个流程只遍历原始数据一次,内存中始终只存在一个row字典,而不是创建多个中间列表。对于处理 10GB 日志文件,这是决定成败的关键。

4. 常见问题与排查技巧实录:那些文档里不会写的坑

4.1 “为什么我的 map/filter 没有返回结果?”

这是新手最常问的问题。典型代码:

data = [1, 2, 3, 4] result = map(lambda x: x * 2, data) print(result) # <map object at 0x...>

原因mapfilter返回的是迭代器(iterator),不是列表(list)。迭代器是“一次性的”,它只在被消费(consumed)时才产生值。print(result)只是打印了迭代器对象本身,没有触发计算。

排查步骤

  1. 确认是否已消费:检查你是否对结果对象调用了list(),tuple(),for循环,或任何会触发__next__()的操作。
  2. 检查是否已被消费过:迭代器只能被遍历一次。如果之前已经list(result)过,再次list(result)会得到空列表[]
  3. 验证输入是否为空mapfilter对空可迭代对象返回空迭代器,这是正确行为,不是 bug。

解决方案

  • 开发调试阶段,强制转为listlist(map(...))
  • 生产环境,直接在for循环中使用:for item in map(...): process(item)
  • 需要多次使用?存为列表:result_list = list(map(...))

4.2 “zip 为什么只返回了 3 个元素,我的列表明明有 5 个?”

a = [1, 2, 3, 4, 5] b = ['x', 'y', 'z'] zipped = list(zip(a, b)) # [(1, 'x'), (2, 'y'), (3, 'z')]

原因zip严格遵循“最短序列原则”。b只有 3 个元素,所以zip只产出 3 个元组,a的后两个元素4,5被静默丢弃。

排查技巧

  • len()检查所有输入序列长度:print([len(seq) for seq in [a, b]])
  • 如果需要填充缺失值,改用itertools.zip_longest
from itertools import zip_longest zipped_long = list(zip_longest(a, b, fillvalue='N/A')) # [(1, 'x'), (2, 'y'), (3, 'z'), (4, 'N/A'), (5, 'N/A')]

4.3 “filter 返回了空列表,但我知道数据里有符合条件的!”

data = ['1', '2', '3', '4'] # 想筛选出大于 2 的字符串数字 result = list(filter(lambda x: x > '2', data)) # ['3', '4'] ✅ # 但如果误写成数字比较 result_wrong = list(filter(lambda x: int(x) > 2, data)) # ['3', '4'] ✅ # 但若数据含非数字,就会崩溃 data_dirty = ['1', '2', 'three', '4'] # list(filter(lambda x: int(x) > 2, data_dirty)) -> ValueError

根本原因filter的函数体内部抛出异常,会导致整个filter调用失败,而不是跳过该元素。

排查与加固

  • 日志先行:在filter函数中加入printlogging,观察哪个元素触发了异常。
  • 防御性编程:用try/except包裹转换逻辑:
def safe_gt_two(s): try: return int(s) > 2 except ValueError: return False # 或 True,取决于业务:无效数据算合格还是不合格? result_safe = list(filter(safe_gt_two, data_dirty))

4.4 “map 和 filter 在大型数据集上变慢了,是不是它们效率低?”

现象:处理 100 万行 CSV 时,map+filter链比等效的for循环慢 20%。

真相剖析mapfilter本身开销极小(C 语言实现),但性能瓶颈往往来自:

  • 函数调用开销lambda或自定义函数的调用比内联代码稍慢;
  • 对象创建开销map返回的迭代器对象、filter的内部状态管理;
  • 最常见原因:你在map里做了重 IO 或重计算

实测对比(100 万次)

方式耗时(秒)说明
for循环 + 内联int()0.12最快,无函数调用
map(int, data)0.15快,内置函数优化好
map(lambda x: int(x), data)0.28慢,lambda 调用开销
map(custom_func, data)0.35最慢,自定义函数开销

优化策略

  • 优先用内置函数(int,str.upper,len);
  • 避免在map/filter的函数体中做文件读写、数据库查询、网络请求;
  • 对于极致性能要求,用numpy.vectorizepandas.Series.map替代。

4.5 “为什么在 Jupyter Notebook 里,map 结果显示不全?”

Jupyter 默认只显示迭代器的repr,即<map object at 0x...>

速查表:Jupyter 中的正确查看方式

目的正确代码说明
查看前 5 个list(islice(map(...), 5))from itertools import islice
查看全部(小数据)list(map(...))最简单
查看类型和长度result = map(...); print(type(result)); print(len(list(result)))注意:len()会消耗迭代器
转为 DataFrame 分析pd.DataFrame(list(map(...)))适合后续分析

注意:在 Jupyter 中,直接写map(...)并回车,不会触发显示。必须用print()或赋值给变量再调用list()

5. 进阶实战:用三个函数重构一个真实的数据清洗脚本

上周,我接手了一个爬虫团队的脏数据清洗任务:他们从 12 个不同网站抓取了商品信息,汇总成一个 CSV,但字段混乱、类型不一、缺失严重。原始脚本用 87 行for循环和if/elif/else堆砌,可读性为零。我用mapzipfilter重构后,核心逻辑压缩到 23 行,且性能提升 35%。以下是关键片段:

原始问题摘要

  • 输入:CSV 有title,price_str,rating_str,review_count_str,url字段;
  • price_str格式多样:'$19.99','Free','From $29.99',''
  • rating_str'4.5 out of 5 stars','Not rated',None
  • review_count_str'1,234','No reviews',''
  • 目标:生成标准化字典列表,pricefloatratingfloat(0.0 表示未评分),review_countint(0 表示无评论),并过滤掉title为空或url无效的记录。

重构后的核心逻辑

import re from csv import DictReader from io import StringIO # 模拟原始数据 raw_csv = """title,price_str,rating_str,review_count_str,url Laptop,'$999.99','4.2 out of 5 stars','2,345','https://a.co/123' Mouse,'Free','Not rated','No reviews','https://b.co/456' Keyboard,'From $79.99','3.8 out of 5 stars','','https://c.co/789' ,,'Not rated','','' Headphones,'$129.99',,'1,001','https://d.co/012' """ def parse_price(s): if not s or 'Free' in s: return 0.0 match = re.search(r'\$(\d+\.\d+)', s) return float(match.group(1)) if match else 0.0 def parse_rating(s): if not s or 'Not rated' in s: return 0.0 match = re.search(r'(\d+\.\d+) out of \d+ stars', s) return float(match.group(1)) if match else 0.0 def parse_reviews(s): if not s or 'No reviews' in s: return 0 match = re.search(r'(\d{1,3}(?:,\d{3})*)', s) return int(match.group(1).replace(',', '')) if match else 0 # ✅ 重构主流程:声明式、可读、可测 def clean_products(csv_text): rows = list(DictReader(StringIO(csv_text))) # Step 1: 过滤掉 title 或 url 为空的脏行 valid_rows = filter(lambda r: r['title'].strip() and r['url'].startswith('http'), rows) # Step 2: 并行解析所有字段(map + zip) titles = map(lambda r: r['title'].strip(), valid_rows) # 重置迭代器:filter 是一次性,需重新生成 valid_rows_again = filter(lambda r: r['title'].strip() and r['url'].startswith('http'), rows) prices = map(parse_price, map(lambda r: r['price_str'], valid_rows_again)) # 重置迭代器(第三次)—— 这里暴露了 filter 的局限性 # 更优解:先转为 list,再 map(对百万数据谨慎) valid_list = list(filter(lambda r: r['title'].strip() and r['url'].startswith('http'), rows)) # Step 3: 用 zip 组装最终产品 cleaned = list(zip( titles, prices, map(parse_rating, map(lambda r: r['rating_str'], valid_list)), map(parse_reviews, map(lambda r: r['review_count_str'], valid_list)), map(lambda r: r['url'], valid_list) )) # Step 4: 转为字典列表 return [ { 'title': t, 'price': p, 'rating': r, 'review_count': rc, 'url': u } for t, p, r, rc, u in cleaned ] # 执行 cleaned_products = clean_products