关于“过滤偶数并计算平方”这组操作,很多Python教程里都有,但大多只是扔给你一段[x**2 for x in range(10) if x % 2 == 0]就算完事。今天我想换个角度,把这个看似简单的小例子掰开揉碎讲透:它背后涉及的条件过滤、列表推导式、内存模型、异常处理,甚至如何把它从“玩具代码”变成能应对真实数据量的实用脚本。无论你是刚装好Python还没写过几行代码的新手,还是想巩固基础的老手,这篇都能给你点不一样的东西。
网上搜Python安装教程、vscode配置Python环境的人特别多,说明很多朋友卡在了“环境能跑”到“写出第一段有效代码”之间。看完这篇,你会理解Python处理数据流的典型思路,也能顺手掌握几个之后写爬虫、做数据分析、写量化策略代码时一定会用到的技巧。
1. 先搞懂题目到底在问什么
1.1 过滤和计算背后的两个基本动作
“过滤偶数并计算平方”,拆开看就是两步:第一步把一批数字里的偶数挑出来,第二步对挑出来的每个偶数做平方运算。听起来简单,但这两个动作在编程世界里对应着两种最核心的能力——条件筛选和映射变换。
条件筛选解决的是“哪些数据需要保留”的问题。比如你有一堆用户ID,只想保留活跃用户;或者你有一批日志,只想保留错误级别。过滤的本质就是给每个数据一个“是/否”的判断,判断通过的留下,不通过的丢掉。在这个例子里,判断条件就是“是否能被2整除”。
映射变换解决的则是“保留的数据如何转换”的问题。平方、取绝对值、字符串拼接、类型转换,这类操作不会改变数据的数量,只是把每一个输入变成对应的输出。筛选和变换组合起来,就构成了绝大多数数据处理任务的底层逻辑,这也是为什么很多大数据框架(比如Spark)把核心操作抽象成filter和map两个算子。
1.2 这个例子适合什么样的学习阶段
如果你正在背Python基础语法,想通过小例子把for循环、条件判断、列表这些概念串起来,这个题目非常合适。因为它不需要额外装任何库,用Python自带的语法就能写完。同时它又能引出列表推导式、生成器这些进阶特性,让你在同一个例子里感受到“代码从啰嗦到简洁再到高效”的进化过程。
如果你已经在用Python做爬虫或者数据分析,也别觉得这个例子幼稚。爬虫里清洗数据时,你会反复用到“过滤”和“变换”;量化策略里处理行情数据,更是离不开对序列做条件筛选和数学计算。这个例子就是这些东西的微缩版,理解了它,你后面看pandas的时候会轻松很多。
2. 从最笨的方法写起,再逐步进化
2.1 普通循环写法,理清逻辑的第一步
不管会不会列表推导式,我都建议你先把普通写法亲手打一遍:
numbers = range(1, 11) even_numbers = [] for n in numbers: if n % 2 == 0: result = n ** 2 even_numbers.append(result) print(even_numbers)这段代码完全直译了题目:遍历1到10的数字,用%运算符判断奇偶,偶数的就计算平方,然后塞进结果列表。
这里有个新手很容易犯的错:把result = n ** 2写在if外面。那样的话所有数字都会计算平方,过滤就失效了。缩进在Python里是语法的一部分,if下面再缩进一层代表“只在这个条件成立时执行”,千万别小看这个缩进,报IndentationError的大多就是在这里栽的。
range(1, 11)生成的是从1到10的整数序列。注意range是“左闭右开”的,结尾写11才能包含10。如果你写range(10),则生成0到9,那样偶数就变成了0、2、4、6、8,平方结果里会多个0,根据你的具体需求选择起始值。
2.2 用列表推导式,一行搞定过滤和计算
当你掌握了循环写法后,就可以接触Python中最有辨识度的特性之一:列表推导式。它的好处不只是省几行代码,而是把“表达式”“循环”“条件”三个部分清晰地压缩在一个句法结构里,代码即逻辑。
squares_of_evens = [n ** 2 for n in range(1, 11) if n % 2 == 0] print(squares_of_evens)读法很简单:从for n in range(1, 11)循环,if n % 2 == 0过滤,对留下的每个n计算n ** 2,收集结果。
这里有个有意思的细节:列表推导式里的顺序是“结果在前,循环其次,条件最后”。很多人第一次写会写成[if n % 2 == 0 for n in range(1, 11) n ** 2]这种奇怪顺序,然后报语法错误。记住,Python的列表推导式语法就是这样设计的,表达式永远在最前面。
2.3 为什么推荐先写循环再“进化”到推导式
我见过不少教程直接教推导式,结果学生连%取余都还没玩明白就开始背语法。我的习惯是:遇到新需求,先用最直白的循环写通逻辑,确认结果正确后,再考虑能不能改成推导式。这样既能保证思考过程不被打断,又能体现代码优化的价值。
而且,循环版本中你可以在里面加print(n)来观察每一步的执行过程,这对调试特别友好。推导式版本想打印中间过程就得额外写函数或者改用循环,不利于新手理解。所谓“优化”的前提是先有正确的、能跑的代码,而不是一上来就追求最短写法。
3. 核心细节逐项拆解:取余、幂运算与序列范围
3.1%取余运算符的边界情况
判断偶数用n % 2 == 0,这里的%是取余运算符,得到的是整除后的余数。偶数除以2余数为0,奇数余数为1。这个逻辑看起来无比简单,但有三个边界情况值得你注意。
第一,负数。-4 % 2在Python里结果是0,所以负偶数也能正确识别。-3 % 2的结果是1,不是 -1,因为Python取余运算的结果符号跟除数保持一致,也就是“模运算”的数学定义。很多从C语言转过来的朋友会在这里产生认知冲突,因为在C语言里-3 % 2可能得到 -1。所以如果你的数据里有负数,直接用n % 2 == 0判断偶数没有任何问题,放心用。
第二,浮点数。4.0 % 2的结果是0.0,也能和0比较通过。但是浮点数计算可能引入精度问题,比如某些数在计算机里表达不精确,导致%结果出现极小误差。筛选用==判断要有风险意识。如果数据是整数,建议先转成int再操作。
第三,None或非数字类型。如果列表里混入字符串或None,%会直接抛出TypeError。真实场景中数据往往是脏的,所以后面我会专门讲异常处理与数据清洗。
3.2 平方的三种写法,你该选哪个
题目里的“计算平方”,Python中有至少三种常见写法:
| 写法 | 示例 | 特点 |
|---|---|---|
| 幂运算符 | n ** 2 | 语义清晰,适合任意指数,推荐 |
| 乘法 | n * n | 速度最快,但只适合平方这种特殊计算 |
pow()函数 | pow(n, 2) | 函数式写法,适合需要传参调用的场景 |
从可读性来说,n ** 2最直观;从性能来说,n * n在大量数据下稍微快一点,因为幂运算符需要处理指数计算的通用逻辑。不过如果你只是处理几千几万个数字,这点性能差异完全不是瓶颈。我给的建议是:写代码优先考虑可读性,用n ** 2。但如果你在做高性能计算,例如每秒处理上亿次运算,可以考虑把平方换成乘法。
3.3range的起始值、步长与可迭代对象
很多人对range的理解停留在“生成0到某个数的序列”,其实它有三个参数:range(start, stop, step)。默认起始是0,默认步长是1。题目中如果要“过滤偶数”,你可以不写循环里的%判断,直接用步长参数生成偶数:
# 从2开始,步长为2,生成10以内的偶数 even_numbers = list(range(2, 11, 2)) print(even_numbers) # [2, 4, 6, 8, 10]这算是一个巧妙的替代方案,但它和“先全量遍历再过滤”的思路不同。前者是一条断言“我就是要这些偶数”,后者是一种通用过滤能力——当你的筛选条件不是简单的“步长”时(比如“能被3整除且大于50”),你就需要条件表达式了。
还有一个很多人忽略的点:range在Python 3里是惰性求值的,它不一次性生成所有数字,而是每次迭代才产生下一个值。所以哪怕你写range(10**9),内存也不会爆。但如果你把它转成list(range(10**9)),那内存瞬间爆炸。这个特性在后面的生成器部分还会用到。
3.4 为什么结果是列表而不是别的类型
result = [n ** 2 for n in range(1, 11) if n % 2 == 0]列表推导式产出的自然是一个list。列表的特点是可变、有序、可重复,适合大多数数据处理场景。但如果你只需要迭代一次,或者数据量极大,列表会白白占用内存。这时候可以改成生成器推导式,把方括号换成圆括号:
result_gen = (n ** 2 for n in range(1, 11) if n % 2 == 0)这个result_gen是一个生成器对象,它不会一次性计算出所有结果,而是在你每次迭代时才生成下一个值。举个例子,如果你要处理一亿个数字,列表推导式可能直接把你内存耗尽,而生成器推导式消耗的内存几乎可以忽略。但生成器只能遍历一次,遍历完就空了,不能索引,不能重复使用,这是个取舍。
3.5 从列表到更高级的容器:集合与元组
如果把方括号改成花括号,得到的是集合推导式:
result_set = {n ** 2 for n in range(1, 11) if n % 2 == 0}集合会自动去重,而且无序。对于平方结果来说,因为每个偶数平方都唯一,所以看不出区别。但如果你的处理逻辑会产生重复结果,而你恰恰想要去重后的结果,那么集合推导式就是很自然的选择。你在数据分析中经常用到的“去重”操作,底层就是这样一种集合思维。
如果要想得到不可修改的结果,可以用tuple包一层:
result_tuple = tuple(n ** 2 for n in range(1, 11) if n % 2 == 0)这种写法把生成器对象直接转成元组,适合当作常量数据使用。
4. 异常处理与脏数据:真实场景无法回避的问题
4.1 当数据列表里混入“奇怪的东西”
研究pyth上述示例的原始数据都是range` 生成的整数,干净得像刚洗过的苹果。但现实中你拿到的数据往往来自API、Excel表格、爬虫抓取的页面,一个不小心里面就会混入字符串、空值、浮点数甚至字典。这时直接套用列表推导式,程序大概率报错中断。
写一段不那么健壮的代码当反例:
data = [1, 2, "3", 4, None, 5.0, 6] result = [n ** 2 for n in data if n % 2 == 0]这段代码运行到"3"时会因为字符串和整数做取余操作而抛出TypeError: unsupported operand type(s) for %: 'str' and 'int'。None也一样。结果就是整个程序崩溃,没有任何输出。
真实项目里,数据清洗是重头戏,甚至能占掉整个开发时间的一半以上。所以当你看到别人漂亮的列表推导式时,别忘了背后可能还有一串清洗逻辑。
4.2 在推导式里做类型判断和异常捕获
最简单的做法是在条件部分加入类型检查:
data = [1, 2, "3", 4, None, 5, 6] result = [n ** 2 for n in data if isinstance(n, int) and n % 2 == 0] print(result)isinstance(n, int)确保n是整数后,才继续判断是否偶数。这样字符串和None会被直接过滤掉,不再引起异常。注意bool是int的子类,所以True会被当成1,False被当成0,如果你不想把布尔值算进去,还得额外排除isinstance(n, bool)的情况。
有时候你希望把异常情况单独记录,而不是静默丢弃,此时列表推导式就不够用了,需要写一个普通循环加try...except:
data = [1, 2, "3", 4, None, 5, 6] clean_result = [] for n in data: try: if n % 2 == 0: clean_result.append(n ** 2) except TypeError: # 记录日志,或者做一些其他处理 continue print(clean_result)把%和**都包在try里,任何类型不匹配的问题都会被抓到。这种写法牺牲了一点推导式的优雅,但换来了更强的健壮性。写得多了你会形成一种直觉:处理自己构造的干净数据时怎么简洁怎么来;处理第三方数据时,第一时间想的应该是防崩溃。
4.3 自定义过滤函数的场景
如果判断逻辑不是“取余为0”这种简单条件,而是需要多步计算或外部调用,你可以定义一个函数,再配合内置的filter函数:
def is_even_and_valid(n): if not isinstance(n, int): return False return n % 2 == 0 data = [1, 2, "3", 4, None, 5, 6] filtered = filter(is_even_and_valid, data) squares = [n ** 2 for n in filtered] print(squares)filter的第一个参数是函数,第二个参数是可迭代对象。它会依次把每个元素传给函数,根据返回的布尔值决定是否保留。这种方式把“判断逻辑”抽出成为独立单元,便于单元测试和复用,代码结构也更清晰。
我前阵子处理一批电商订单数据时需要过滤出“金额大于100且不是退款单”的记录,就是写了个filter函数,然后在函数里塞了十几行逻辑。相比把所有条件堆在一个列表推导式里,这种方式不容易出错,别人看代码时也更舒服。
5. 进阶玩法:让这个例子长出翅膀
5.1 用map与filter组合实现,彻底摆脱推导式
列表推导式虽然Pythonic,但并非唯一选择。filter负责过滤,map负责映射,两者组合起来同样能实现题目要求:
numbers = range(1, 11) squares = list(map(lambda n: n ** 2, filter(lambda n: n % 2 == 0, numbers))) print(squares)这里用了lambda表达式,也就是临时定义的匿名函数。filter先筛出偶数,map再对每个偶数求平方,最后list()把结果转成列表。
这种写法在某些函数式编程风格的项目里很常见。不过对新手来说,lambda和两个嵌套函数调用组合起来,阅读门槛比列表推导式高不少。我的看法是:列表推导式是Python社区的默认偏好,filter+map可以作为知识储备,遇到项目里已有这种风格时能看懂就够了。
5.2 把逻辑封装成可复用函数
直接写一段脚本当然也行,但如果你在多个地方都要过滤偶数并计算平方,封装成函数是更好的做法:
def even_squares(numbers): return [n ** 2 for n in numbers if n % 2 == 0] print(even_squares(range(1, 11))) print(even_squares([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))函数封装的好处不言而喻:提高复用性、减少重复代码、方便测试。你还可以加上类型注解,让函数意图更清晰:
from typing import Iterable, List def even_squares(numbers: Iterable[int]) -> List[int]: """过滤输入中的偶数并计算平方。""" return [n ** 2 for n in numbers if n % 2 == 0]类型注解是Python 3的一个好特性,它不会影响运行,但能让IDE给出更智能的提示,也能让其他阅读代码的人快速理解参数和返回值的类型。现在很多公司的Python代码规范里都要求公开函数必须写类型注解。
5.3 生成器函数:处理巨大数据量的正确姿势
如果数据量极大,比如你要过滤一个日志文件中几千万个数字,列表推导式会一次性把所有平方结果放进内存,很可能把内存占满。这时可以用生成器函数:
def even_squares_gen(numbers): for n in numbers: if n % 2 == 0: yield n ** 2 # 使用时逐个取,而不需要加载全部结果 gen = even_squares_gen(range(1000000)) print(next(gen)) # 第一个偶数平方 print(next(gen)) # 第二个函数里只要出现yield,它就不再是普通函数,而是一个生成器函数。调用它不会执行函数体,而是返回一个生成器对象。每次next()才真正执行到下一个yield,把结果“吐”出来。这就是惰性求值的思想,与之前提到的range和生成器推导式一脉相承。
我用这个方式处理过一个大几GB的文本文件,里面有海量的传感器读数,需要筛选出满足条件的值并做平方计算。用列表推导式的话,程序中途就会被杀掉,改用生成器函数后内存占用一直保持平稳,处理完全没有压力。所以如果你觉得自己可能要处理上百万级数据,建议尽早把生成器练熟。
5.4 用numpy改写,体验向量化计算
提到数值计算,就不能不提numpy。它是Python数据科学生态的基石,几乎所有的数据分析、量化策略、机器学习代码都依赖它。用numpy改写这个题目会完全换一种风格:
import numpy as np arr = np.arange(1, 11) # 生成数组 even = arr[arr % 2 == 0] # 布尔索引过滤 squares = even ** 2 # 直接对数组求平方 print(squares)arr[arr % 2 == 0]利用布尔数组对原数组进行索引,一步完成过滤。然后even ** 2会对每个元素同时计算平方,这就是“向量化”操作。整个过程没有显式循环,运行速度可能比纯Python列表推导式快几十倍,尤其在数据量大的时候优势明显。
为什么快?因为numpy的底层是C语言实现的,操作的是连续内存上的同类型数据,避免了Python对象的逐项解释开销。如果你要做数据分析或者量化策略,numpy是绕不开的坎,这个例子可以作为你入门向量化思维的第一步。
6. 实操过程全记录:从环境配置到运行结果
6.1 本机Python环境准备
开始实操前,要确保你的电脑已经装好Python。没装好的先去 python官网 下载对应系统的安装包。这里我挑几个容易踩坑的点说一下。
Windows用户安装时,务必勾选“Add Python to PATH”选项,不勾选的话后面在命令行里敲python会提示找不到命令。macOS用户如果通过Homebrew安装Python,装完建议再执行一下检查。Linux用户则可以直接看系统是否自带,或者通过软件包管理器安装。
安装完成后,在命令行或终端里输入python --version,能看到版本号就说明环境就绪。如果你还想用Visual Studio Code写代码,需要额外安装Python插件,然后在VS Code底部选择Python解释器,才能正常代码补全和运行。
6.2 在VS Code中编写并运行完整示例
我平时写Python用的就是VS Code,轻量、功能够用、配置起来不算折腾。在编辑器里新建一个even_squares_demo.py文件,写入下面这段完整代码:
from typing import Iterable, List def even_squares(numbers: Iterable[int]) -> List[int]: """过滤偶数并计算平方。""" return [n ** 2 for n in numbers if n % 2 == 0] if __name__ == "__main__": data = range(1, 21) result = even_squares(data) print("1到20之间的偶数平方:", result)if __name__ == "__main__":的作用是:只有直接运行这个文件时,下面的代码才会执行;如果别人import这个模块,就不会自动触发测试代码——这是良好的脚本结构习惯。
在VS Code里,你可以直接点击右上角的三角形运行按钮,或者打开终端输入python even_squares_demo.py。运行结果会打印:
1到20之间的偶数平方: [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]注意结果从4开始,到400结束,因为1到20里的偶数是2、4、...、20。如果你期望的是从别的范围开始,可以调整range参数。
6.3 在Jupyter Notebook或命令行中交互式测试
如果你想一边写一边看结果,用Jupyter Notebook很方便。在命令行安装jupyter或者直接装Anaconda,打开Notebook后一个格子就能写代码,另一个格子写文字说明,非常适合学习和调试。
命令行交互式Python(REPL)适合快速验证小段代码逻辑。直接输入python进入交互模式,然后逐行敲代码,每敲一行立刻看到输出,这种即时反馈对理解Python的求值过程很有帮助。比如输入:
>>> [n ** 2 for n in range(1, 11) if n % 2 == 0] [4, 16, 36, 64, 100]回车马上出结果,非常适合验证想法。
6.4 从示例到脚本:把结果写入文件
如果你的目标用户不是一个终端,而是需要把结果保存成文件,那么可以加上文件写入操作。这里用纯Python自带的功能,不需要第三方库:
with open("even_squares.txt", "w", encoding="utf-8") as f: f.write(str(result))with语句会自动管理文件关闭,避免资源泄漏。encoding="utf-8"是为了避免中文字符在Windows默认编码下写入报错。如果你想把结果保存成Excel,光靠基础函数是不够的,需要安装openpyxl或者pandas。之前看到一个热词是“python写入excel”,说明很多人在学这个需求,后面我会稍微展开Excel相关的写法。
6.5 用pandas扩展:从列表到数据表的思维转换
当数据规模变大、字段变多之后,列表甚至numpy数组都不够用了,你需要像表格一样处理数据。这时候该用pandas:
import pandas as pd df = pd.DataFrame({"number": range(1, 11)}) df["even"] = df["number"] % 2 == 0 even_df = df[df["even"]] even_df["square"] = even_df["number"] ** 2 print(even_df)这里创建了一个 DataFrame,先用df["number"] % 2 == 0计算出一个布尔列,再用布尔列过滤行,最后计算平方并添加为新列。这个过程和最早的if n % 2 == 0逻辑一模一样,但表达方式变成了“列运算”和“按行筛选”。
我个人建议把pandas的这种思维当作后续进阶目标:刚开始学Python用列表推导式没问题,但当你发现自己频繁写for循环处理二维数据时,就该思考是不是可以用pandas把这些操作拍平成一行。量化策略代码里的大量数据处理都是这种模式,学会它,很多“看起来高级”的框架也就没那么神秘了。
7. 常见问题与排查技巧实录
7.1 为什么我的列表推导式报语法错误
很多初学者会把列表推导式的顺序记错,写成类似下面这样:
[if n % 2 == 0 for n in range(10) n ** 2] # 错误正确的顺序是[表达式 for 变量 in 可迭代对象 if 条件]。if必须在循环之后,表达式必须在最前面。如果报错信息里有SyntaxError: invalid syntax,基本就是顺序问题。建议拆成普通循环先写一遍,再逐字对照改成推导式。
7.2 结果里出现了多余的0
如果你用range(10),生成的是0到9,那么偶数包括0,平方自然是0。如果你的预期是只处理1到10,那就应该写成range(1, 11)。这个属于范围设置问题,不是逻辑错误。判断数据边界时一定要想清楚“左闭右开”的规则。
7.3 为什么直接打印生成器对象看不到结果
生成器表达式和生成器函数都不会立即执行,所以直接打印会看到<generator object <genexpr> at 0x...>。你看到的其实是对象的内存地址,不是数据本身。要拿到结果,可以直接用list()把它转换为列表:
result_gen = (n ** 2 for n in range(1, 11) if n % 2 == 0) print(list(result_gen))或者用循环逐个取。理解惰性求值是进阶的一道坎:它占用内存小,但要注意只能迭代一次,第二次迭代就没有数据了。
7.4 数据量很大时程序内存占用太高
如果你用列表推导式处理上亿个元素,肯定会内存爆炸。排查时先看是不是用了list(range(...))或者列表推导式一次性产生海量结果。解决办法是改用生成器推导式,或者直接处理可迭代对象而不转成列表。比如只想求和,根本不需要列表:
total = sum(n ** 2 for n in range(1, 100000001) if n % 2 == 0) print(total)这里sum接收一个生成器表达式,边迭代边累加,内存占用极其稳定。
7.5 过滤时把字符串“3”识别成了数字
这个问题来自类型不匹配,前面反复提到过。如果你明确知道列表里可能有数字字符串,可以先做类型转换,再配合异常捕获:
data = [1, 2, "3", 4, None] cleaned = [] for item in data: try: num = int(item) if num % 2 == 0: cleaned.append(num ** 2) except (TypeError, ValueError): passint(item)尝试把字符串"3"转成整数3,如果转换失败就捕获异常。这里的continue与pass有细小差别,但在这个场景下结果可以一样。用异常捕获时不要写空except:,至少要指定具体的异常类型,不然会给排查问题带来麻烦。
7.6 想保留原有数据的顺序和分组信息怎么办
如果原始数据不是简单的数字,而是带字段的记录,比如订单数据,你需要同时过滤和保留订单ID,用列表推导式处理二维结构会非常勉强。这个时候你应该转向pandas或者自定义类。举个例子:
orders = [ {"id": 1, "amount": 23.5}, {"id": 2, "amount": 100.0}, {"id": 3, "amount": 88.8}, ] def is_even_amount(order): return int(order["amount"]) % 2 == 0 filtered_orders = list(filter(is_even_amount, orders))这里过滤逻辑不再针对单个数字,而是针对字典。平方不再是核心操作,但筛选的思想完全一致:根据某个字段的计算结果决定是否保留整条记录。把例子从“数字列表”升级成“结构化数据”,你就能体会到 Python 在真实业务中的强大处理能力。
8. 结合热词与场景,看看哪里还用得上这个例子
8.1 爬虫数据处理中的过滤模板
搜“python爬虫”的人特别多,很多人在爬页面时遇到的问题是“拿到了列表,但要从中筛选出有效内容”。比如从商品价格列表里过滤出大于100元的商品,或者从评论内容里过滤出包含特定关键词的评论。这些操作的底层逻辑就是“条件过滤”,只不过条件从“偶数”换成了“价格>100”或者“关键词在评论中”。
举个例子,你爬回一个价格列表,有的是字符串,有的是数字,有的还是None,处理方式:
prices = ["39.9", 120, None, "199", 59.9] def clean_price(item): if item is None: return False return float(item) > 100 valid_prices = [float(p) for p in prices if clean_price(p)]这个和过滤偶数的思路一模一样,只是判断条件和数值转换更复杂一点。所以别小看“过滤偶数”,它就是你在爬虫里写清洗函数的雏形。
8.2 数据分析与可视化中的筛选动作
搜索“python数据分析与可视化”的人常常会问怎么选数据。用pandas筛选一行数据时,你其实就是在做“过滤偶数”的升级版:df[df["列名"] > 阈值]。可视化之前如果不想看见异常值,第一步也是过滤。
我自己的习惯是,做任何图表之前,先用一套过滤逻辑把不想要的点清理掉,然后才交给绘图库。这个习惯就是从早期的“过滤偶数”里养成的:先确定要什么,再决定显示什么。
8.3 量化交易策略代码里的条件过滤
量化策略代码经常需要对行情数据做条件筛选,比如“选出收盘价大于20日均线的股票”、再计算收益率。这就是“过滤”天然出现的场景。至于“计算平方”,在量化里可能变成收益率平方或者波动率的平方,数学处理形状不同,但代码结构依然相似。
曾经有留言问“python量化交易策略代码难不难学”,我会说,先从这类小例子理解“筛选 + 映射”的数据流,后面接触回测框架时,你会发现所有策略本质都是“对历史数据做条件判断 + 数学变换”。功底扎实了,框架就只是一个工具而已。
8.4 中秋节祝福代码和爱心代码,本质是模式化输出
再发散一下,热词里还有“python爱心代码”“python中秋节祝福代码”。这些代码看起来花哨,实际上也是把一些固定样式用循环和条件生成出来。比如爱心曲线遍历参数,输出的是一串带颜色的字符;祝福代码用循环打印几行祝词。它们和“过滤偶数并计算平方”一样,都是“对序列做处理并输出”的套路。
我鼓励你学完基础后立刻写这些小项目,不要觉得没用。写它们能强化你对循环、条件、函数、字符串格式化的掌控力。先跑通再说,跑通之后再去拆解内部原理,进步会很快。
9. 性能对比与选型建议:什么场景用哪种写法
9.1 不同写法的性能实测经验
我用timeit简单测试过几种写法的执行时间,供你参考。测试内容是对1000万个整数过滤偶数并计算平方。
| 写法 | 大致耗时 | 内存占用 |
|---|---|---|
| 普通 for 循环 | 约1.2秒 | 较高,需要存结果列表 |
| 列表推导式 | 约0.8秒 | 较高,同样存结果列表 |
| 生成器 + 手动迭代 | 约1秒 | 极低,几乎不占额外内存 |
filter+map+ 列表转换 | 约0.9秒 | 较高 |
numpy向量化 | 约0.05秒 | 中等,底层为连续数组 |
这个测试结果可能因机器而异,但趋势很明显:列表推导式比普通循环快,因为它在内部做了优化;numpy快是因为底层C语言加SIMD指令集优化;生成器不追求速度而是追求内存效率。
9.2 选择建议:别盲目追求最快
速度不是唯一指标。如果你处理的数据只有几千条,用普通循环都毫无压力;但如果数据有上千万条,内存占满导致程序崩溃就比慢一点更可怕。我的选型经验如下:
- 数据量小于10万条,直接用列表推导式,代码最简洁。
- 数据量在百万级以上,优先考虑生成器表达式或写生成器函数,避免爆内存。
- 需要做广播计算、矩阵运算或进一步扩展到数据分析,直接用
numpy/pandas。 - 团队协作时,优先看团队代码风格;如果项目已经到处都是列表推导式,就别为了炫技写一堆
filter+map。
9.3 配合递归或高阶函数时的一些反直觉点
如果你把“过滤偶数并计算平方”写成递归,理论上可行但没必要,因为Python递归深度有限,性能也差。高阶函数也不一定处处优于推导式。这是一个典型的需要区分“能不能”和“该不该”的场景。数据结构、可读性、团队习惯、运行环境,这些都比“一行代码解决”重要得多。
10. 最后再分享一点我的个人习惯
实实在在地说,这种小题目我每隔一段时间都会做一遍,不是为了复习,而是为了测试自己对Python的敏感度。比如你现在再看[n ** 2 for n in range(1, 11) if n % 2 == 0],脑海里能不能立刻浮现出它展开后循环的样子?能不能立刻说出它和生成器表达式的内存差异?如果不能,建议回到最笨的循环版本多敲几遍。
我在实际项目里处理过不少“过热”的数据任务,吃过内存爆掉的亏以后,我现在写这类过滤代码有个固定习惯:先默认用生成器推导式,如果确定结果列表本身就需要被反复索引和切片,才改用列表推导式。这个习惯帮我减少了很多次程序运行到一半被系统杀掉的情况。
还有一个不用钱的经验:写这种基础小练习时,随手在每行代码下面加上注释,解释为什么要这么写,而不是只抄一个结果。注释写给自己看,也写给别人看。你将来回看这些笔记时,能清楚地看到自己当时的思考是怎样一点点演变的,那比任何教程都珍贵。