☰
Python多写法实战:从列表推导到异步并发,提升代码质量与性能
2026/10/29 9:08:55 网站建设 项目流程

同一个功能,换个人写可能完全不一样。有人拿到需求先写 for 循环,有人直接列表推导式,有人会抽成生成器,还有人上来就套装饰器。代码最后都能跑,但可读性、扩展性、性能表现可能差很多。

今天这篇文章不聊某个具体开源项目,而是聊一个更底层的开发习惯:不要局限于一种写法。我们以 Python 为主要语言,把列表处理、字符串拼接、文件读写、并发任务、接口封装这些高频场景都拆开看一遍。重点是让你理解不同写法之间的权衡,以后拿到需求至少能给出两三种候选方案,再根据场景选最合适的一种。

文章会比较长,建议先收藏。适合正在学 Python 的初学者、写业务代码想提升代码质量的开发者,也适合做代码评审时不知道该怎么给建议的技术同学。

1. 为什么不要局限于一种写法

很多新人写代码时有个习惯:学会一种写法之后,所有地方都用同一种写法。比如会了for循环,就到处for;会了if-else,就所有分支都用if-else。这种做法不是错,但容易让代码变得僵硬。

1.1 写法差异来自哪里

写法差异本质上是抽象层次不同。同样是“把列表中所有偶数取出来再平方”,可以写循环,可以写推导式,也可以写map加filter。三种写法的运行结果一样,但代码表达的逻辑重心不一样。

循环写法强调“怎么一步步做”,属于命令式思维;列表推导式强调“我要什么结果”,属于声明式思维;map/filter强调“对数据做变换”,属于函数式思维的雏形。

如果你只会其中一种,碰到一些问题就会很难受。比如处理海量数据时,列表推导式一次性生成全量数据,内存压力可能很大;换成生成器表达式就能一边迭代一边计算。但如果业务逻辑很复杂,生成器表达式会写得非常难读,这时候拆分几个循环反而更清晰。

1.2 局限在一种写法的代价

第一个代价是代码可读性下降。某个功能用reduce可以一行写完,但团队里不是每个人都熟悉reduce。如果同事需要花很长时间才能看懂,这行代码的可维护性就差。

第二个代价是性能没有优化空间。不同写法的底层实现差异很大。文件读取是逐行读还是用readlines()一次读入,对内存占用影响非常明显。并发场景用线程还是进程、用同步还是异步,对吞吐量影响也很大。只会一种写法,等于放弃其他优化路径。

第三个代价是代码难扩展。一个函数从头到尾都是过程式if-else,需求一变就要继续往里面加条件。如果一开始用策略模式或字典映射,新逻辑只需要新增一个配置项,改动范围小很多。

1.3 多写法的学习路径

建议按“复制 → 对比 → 重构”的顺序学。

先把你现在能跑通的代码复制一份,换成另一种写法。比如今天用for实现,明天用列表推导式实现。然后对比两个版本:哪个更容易读?哪个更容易测试?哪个在数据量变大时更稳?最后把工作代码里某几个小函数用更合适的写法重写一遍,跑通测试再提交。

这个练习坚持一段时间,你自然就会在写第一版代码时考虑多种方案,而不是写完再返工。

2. 适用场景与使用边界

多写法不是万能药。不同项目、不同团队、不同生命周期阶段,对写法的容忍度不一样。

2.1 适合的场景

适合多写法探索的场景包括工具脚本、算法原型、个人项目、内部服务、非核心模块。这些场景里代码规模小,试错成本低,即使写得不完美,后续改动也不难。你可以放心尝试生成器、装饰器、上下文管理器、策略模式等不同写法。

比如写一个数据处理脚本,输入是几十个文件,输出是统计结果。这种任务用脚本方式、函数方式、类方式都能实现,而且都比较直观。你可以先用最简单的写法跑通,再逐步优化。

2.2 不适合的场景

不适合随意切换写法的场景包括核心交易系统、对外 API、长周期维护的公共组件、多人协作的大规模代码库。原因不是这些写法有问题,而是统一性比个性化更重要。

一个大团队里,如果每个人对同一个需求都有自己的写法,代码评审成本会非常高。某个模块用的是函数式风格,另一个模块用的是面向对象风格,新成员阅读代码时会不断切换思维模式。

所以在团队项目里,优先遵守项目已有的代码风格和团队规范。个人练习时,再放开手脚尝试不同写法。

2.3 使用边界

两个边界需要特别提醒。

第一个边界是不要为了炫技而硬套写法。能两百行写完的业务逻辑,非要用函数式编程一行流去实现,只会增加阅读负担。

第二个边界是性能和可维护性要平衡。生成器很省内存,但如果你的数据量本来就不大,用普通列表更直观;异步并发能力强,但如果任务本来就是 CPU 密集型,用多进程可能更合适,用异步反而浪费时间。

3. 环境准备与前置条件

本文大部分示例基于 Python,不需要复杂环境。

  • 操作系统:Windows / macOS / Linux 均可。
  • Python 版本:建议 3.8 及以上。f-string、pathlib、asyncio在低版本里可能有差异,3.8 以上更省心。
  • 包管理器:pip或poetry都行。
  • 编辑器:任意支持 Python 的编辑器,VS Code 或 PyCharm 均可。
  • 不需要 GPU,不需要 CUDA,普通笔记本就能跑通本文所有示例。

如果你要用到requests、flask或fastapi等第三方库,先创建虚拟环境再安装依赖。

python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install requests flask fastapi uvicorn

如果只是跑基础语法示例,连第三方库都不需要。

4. 常见写法对比

下面用几个高频场景做对比。每个场景给出不同写法,并在最后说明各自的适用点。

4.1 列表处理:循环、推导式、生成器、函数式

先看一个最简单的需求:给定一个整数列表,取出所有偶数,计算它们的平方,并求和。

写法一:传统for循环。

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] result = 0 for n in numbers: if n % 2 == 0: result += n * n print(result)

这种方式最直白,每一步都能看清楚。适合初学者阅读,也适合内部逻辑非常复杂时逐步调试。

写法二:列表推导式。

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] squares = [n * n for n in numbers if n % 2 == 0] result = sum(squares) print(result)

代码量明显变少,思路变成了“筛出偶数,计算平方,再求和”。可读性不错,适合中间结果还需要继续使用的场景。

写法三:生成器表达式。

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] result = sum(n * n for n in numbers if n % 2 == 0) print(result)

生成器不会一次性在内存中生成完整的平方列表,而是边迭代边计算。当数据量很大时,这种写法更省内存。

写法四:函数式写法。

from functools import reduce numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] result = reduce( lambda acc, n: acc + n * n, filter(lambda n: n % 2 == 0, numbers), 0 ) print(result)

函数式写法强调数据流:先过滤,再累加。代码更通用,但新手阅读门槛高。如果团队里没人熟悉reduce,不建议在日常业务里硬用。

四种写法结果一样,但表达重点完全不同。实际选型时,先问三个问题:这个数据量有多大?这段逻辑下一次会不会改?团队里其他人能不能一眼看懂?

4.2 字符串格式化:%、format、f-string

字符串格式化是最常见的写法差异点。

写法一:百分号格式化。

name = "tester" score = 92 message = "Hello %s, your score is %d" % (name, score) print(message)

写法二:format方法。

name = "tester" score = 92 message = "Hello {}, your score is {}".format(name, score) print(message)

写法三:f-string。

name = "tester" score = 92 message = f"Hello {name}, your score is {score}" print(message)

从 Python 3.6 开始,f-string是推荐的首选写法。它更直观,变量直接写在大括号里,不用关心位置和类型。但要注意,f-string在拼接非常复杂的表达式时,也会让模板变得臃肿。如果模板内容很长,建议把计算结果先赋给变量,再放进f-string。

另外,日志场景要特别注意。很多日志库支持惰性格式化,推荐用%s占位,而不是提前拼接字符串,避免日志级别不输出时白白做格式化。

4.3 文件读写:open、with、pathlib

文件读写也有多种写法,最容易踩坑的是忘记关闭文件。

写法一:手动open和close。

f = open("data.txt", "r", encoding="utf-8") content = f.read() f.close()

这种写法在读取过程中如果抛出异常,close()可能不会执行,导致文件句柄泄漏。

写法二:with上下文管理器。

with open("data.txt", "r", encoding="utf-8") as f: content = f.read()

with会在代码块结束时自动关闭文件,即使中间发生异常也会进入清理流程。这是日常开发中最推荐的一种写法。

写法三:面向对象方式封装。

class DataLoader: def __init__(self, path): self.path = path def read_all(self): with open(self.path, "r", encoding="utf-8") as f: return f.read()

把文件读取封装成类,适合后续扩展:缓存、解析、压缩处理都可以逐步加进去。

写法四:使用pathlib。

from pathlib import Path path = Path("data.txt") content = path.read_text(encoding="utf-8")

pathlib把路径处理变成了对象操作,代码更现代化。实际项目中,建议统一用pathlib管理路径,再配合with读取文件。

4.4 并发任务:线程、进程、异步

并发写法的差异更明显。同一个批量任务,可以用线程池、进程池、协程分别实现。

批量下载一批 URL,内容彼此独立。

线程池实现:

from concurrent.futures import ThreadPoolExecutor def download(url): print(f"download: {url}") # 这里执行实际下载逻辑 return url urls = ["https://example.com/1", "https://example.com/2"] with ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(download, urls))

线程池适合 I/O 密集型任务,比如网络请求、文件读写。线程切换开销相对较小,实现也简单。

进程池实现:

from concurrent.futures import ProcessPoolExecutor def cpu_task(n): # 这里是 CPU 密集计算 return n * n if __name__ == "__main__": with ProcessPoolExecutor(max_workers=4) as executor: results = list(executor.map(cpu_task, range(10)))

进程池适合 CPU 密集型任务。它可以利用多核 CPU,但进程间通信成本更高,数据序列化也有开销。

异步协程实现:

import asyncio async def fetch(url): await asyncio.sleep(1) return url async def main(): urls = ["https://example.com/1", "https://example.com/2"] tasks = [fetch(url) for url in urls] results = await asyncio.gather(*tasks) print(results) asyncio.run(main())

异步协程适合大量 I/O 等待场景。单线程内可以同时挂起很多任务,理论上比线程更节省资源。但代码结构会变成async/await风格,对团队水平有要求。

实际项目中,如果只是调用第三方 API,ThreadPoolExecutor最简单;如果是视频转码、图像处理这类计算密集任务,ProcessPoolExecutor更合适;如果是高并发网关或长连接服务,才考虑asyncio。

4.5 配置读取:环境变量、JSON、YAML

同一个配置可以放在不同地方,读取写法也不同。

环境变量方式:

import os debug = os.getenv("DEBUG", "false").lower() == "true"

适合容器部署,配置跟随环境走,不写进代码仓库。

JSON 方式:

import json with open("config.json", "r", encoding="utf-8") as f: config = json.load(f) host = config.get("host", "127.0.0.1")

适合本地开发,简单直接。

YAML 方式:

from pathlib import Path try: import yaml except ImportError: raise SystemExit("请先安装 PyYAML: pip install pyyaml") config = yaml.safe_load(Path("config.yaml").read_text(encoding="utf-8"))

YAML 适合结构复杂的配置,注释友好,但需要额外依赖,解析时要注意safe_load而不是load,避免安全问题。

三种写法没有绝对优劣。需要根据部署方式选择:十二要素应用推荐环境变量,微服务配置中心按团队规范来,本地小工具用 JSON 或 YAML 都行。

5. 从“能跑”到“能维护”:如何评估写法优劣

选写法的核心不是“哪个更简洁”,而是“哪个更利于维护”。评估时可以从四个角度打分。

5.1 可读性

代码首先是给人看的。一个写法如果自己三天后再看都糊涂,那就不合适。判断可读性有个办法:把代码拿给同事看,不解释背景,看他多久能讲清楚这段逻辑。

简单逻辑用线性写法,中间逻辑抽成函数,复杂逻辑用类和模式。尽量避免“只有懂这个语言特性才能看懂”的写法,除非团队有统一约定。

5.2 性能与资源占用

不同写法的性能和资源占用差异需要实测。不要凭感觉认为“推导式一定比循环快”,要看具体数据量和运行环境。

建议用timeit做耗时测试。

python -m timeit "sum(n * n for n in range(1000) if n % 2 == 0)"
python -m timeit "[n * n for n in range(1000) if n % 2 == 0]"

对于内存占用,要注意列表和生成器的区别。列表推导式会一次性创建完整列表,生成器表达式是惰性计算。

# 一次性生成 1000 万个平方数,内存占用较高 squares = [i * i for i in range(10_000_000)] # 惰性计算,边迭代边生成,内存占用较低 squares = (i * i for i in range(10_000_000))

如果只需要遍历一次,用生成器;如果后续要随机访问、多次遍历、或者需要长度信息,用列表。

5.3 异常处理

写法越简洁,越容易忽略异常。比如f-string里调用了一个可能失败的函数,异常会直接抛出来;生成器表达式在迭代过程中出错,错误定位也不如循环直观。

所以复杂业务逻辑里,不要硬用一个表达式把所有事做完。先用普通的if/else、for循环把边界情况处理好,再考虑是否重构成更简洁的写法。

5.4 可测试性

可测试的写法通常满足两个特点:输入明确,输出稳定。函数式写法因为不修改外部状态,天然容易测试;纯函数比面向对象方法更容易写单元测试。

但测试好坏不只看写法,还看命名和边界处理。一个函数叫process_data,参数一大把,返回类型不确定,换成什么写法都不好测。先从命名和函数边界下手,再谈写法。

6. 把不同写法组织成可扩展的代码

写法不只是语法层面的选择。一个完整的业务功能,往往需要多种写法组合使用。下面给出一个比较常见的组合思路。

6.1 用接口抽象行为

假设我们要做一个数据处理管道,输入可以是 CSV、JSON、TXT。不同格式的解析逻辑不一样,但业务方期望调用方式一致。

from abc import ABC, abstractmethod class Parser(ABC): @abstractmethod def parse(self, path: str) -> list: pass class CsvParser(Parser): def parse(self, path: str) -> list: # 解析 CSV return [] class JsonParser(Parser): def parse(self, path: str) -> list: # 解析 JSON return []

这里用到了抽象类和继承。写法的好处是,以后新增一种格式,不用改业务主逻辑,只需要加一个新的Parser子类。

6.2 用字典替代分支

有些场景下,简单的三种格式解析没必要建类体系,用字典加分支会更快。

def parse_csv(path): return [] def parse_json(path): return [] PARSERS = { "csv": parse_csv, "json": parse_json, } def parse_file(path, file_type): parser = PARSERS.get(file_type) if parser is None: raise ValueError(f"unsupported file type: {file_type}") return parser(path)

这种写法把函数当成一等对象,用字典完成映射。新增格式时只需要增加一个函数和一个字典键,代码改动集中,非常容易评审。

6.3 用装饰器增强功能

如果要在所有解析行为上增加耗时统计,可以写一个装饰器,而不改动每个解析函数。

import time from functools import wraps def timed(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper @timed def load_data(path): return path

这样,基础函数保持纯逻辑,耗时统计通过装饰器叠加。以后想加日志、重试、缓存,都可以用类似方式。

6.4 不同写法组合的边界

设计模式不一定比简单分支好。如果业务只有两类情况,且以后基本不会扩展,用字典或if-else就够了。如果分支数量超过五六个,且每个分支的内部逻辑都在变,再考虑抽象类或策略模式。

没有永远最好的写法,只有当前阶段最合适的写法。这也正是“不要局限于一种写法”的核心。

7. 接口 API 与批量任务中的写法选型

本地脚本验证通过后,经常需要把逻辑对外暴露成接口,或者变成批处理任务。这个阶段,写法选型会直接影响服务的并发表现和维护成本。

7.1 用 FastAPI 暴露处理接口

以 FastAPI 为例,把数据处理逻辑封装成/api/process接口。

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ProcessRequest(BaseModel): text: str # 其他业务参数 class ProcessResponse(BaseModel): result: str @app.post("/api/process", response_model=ProcessResponse) def process_item(request: ProcessRequest): # 这里是业务逻辑,可以调用前面定义的 parser、filter 等函数 return ProcessResponse(result=request.text.upper()) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8000)

这种写法把输入输出模型定义清楚,接口文档由 FastAPI 自动生成,方便其他系统对接。

7.2 批量任务中的写法差异

批量任务常见两种写法:同步循环和线程池/进程池。同步写法简单,但遇到大量 I/O 等待时会很慢;并发写法快,但要注意任务间的隔离和资源限制。

同步批量处理:

items = ["a", "b", "c", "d"] def process(item): # 模拟业务处理 return item.upper() results = [process(item) for item in items]

线程池批量处理:

from concurrent.futures import ThreadPoolExecutor def process(item): return item.upper() items = ["a", "b", "c", "d"] with ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(process, items))

如果单条处理时间很长,可以把max_workers调大;如果任务依赖同一个数据库连接或同一个输出文件,需要注意并发安全,可以改用进程池或引入任务队列。

7.3 批量任务需要重构的时机

当任务满足以下条件时,建议从脚本重构为服务:

  • 输入来自外部系统,而不是本地文件。
  • 需要统一鉴权和限流。
  • 任务执行时间长,需要异步返回任务 ID。
  • 多台机器需要共享任务队列。

重构时,核心逻辑不要动,只把入口从命令行函数改成 API 函数。这就是前面接口抽象带来的价值——处理逻辑和调用方式分离,后续切换写法成本很低。

8. 资源占用与性能观察

不同写法在资源占用上的差异,往往只有数据量大了才会暴露。

8.1 怎么观察耗时

最简单的办法是使用timeit模块,前面已经给了命令。更细的性能分析可以使用cProfile。

python -m cProfile -s cumulative your_script.py

输出会告诉你每个函数累计耗时多少、调用多少次。定位到慢函数后,再针对性地用不同写法定量比较。

8.2 怎么观察内存占用

可以用tracemalloc统计 Python 内存分配。

import tracemalloc tracemalloc.start() # 执行要测试的代码 snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics("lineno") for stat in top_stats[:10]: print(stat)

如果比较列表推导式和生成器表达式,建议在 100 万条以上数据量时测试。小数据量下两者差别很小,没必要为了省内存刻意用生成器。

8.3 不同写法对资源的典型影响

  • 列表推导式:速度通常不慢,但会一次性分配完整列表,内存占用高。
  • 生成器表达式:按需生成,内存占用低,但不能重复遍历。
  • 多线程:适合 I/O 等待,线程本身开销小,但受 GIL 影响,CPU 密集任务不一定提速。
  • 多进程:能利用多核 CPU,但每个进程都有独立内存,整体内存占用更高。
  • 异步协程:适合高并发连接,资源占用低,但代码复杂度高。

这些结论是一般规律。具体项目里操作系统、Python 解释器版本、依赖库实现都可能改变结果,所以一定要在本机实测。

8.4 降低资源占用的通用手段

如果发现内存占用过高,优先检查是否一次性加载了过多数据。改成流式读取,再用生成器处理,通常能明显降低峰值内存。

如果发现 CPU 耗时过高,优先检查是否在循环里重复创建对象、重复打开文件、重复调用远程接口。把公共资源提到循环外,比更换写法更有效。

9. 常见问题与排查方法

下面把“多种写法”实际落地时容易遇到的问题整理成表格。

问题现象可能原因排查方式解决方案
代码太简洁,同事看不懂过度使用函数式技巧做代码评审,观察阅读成本拆成普通循环,加注释
生成器只能遍历一次生成器是迭代器,状态一次性消耗打印类型和长度,确认重复遍历场景数据量不大时改用列表
多线程批量处理反而变慢CPU 密集任务受 GIL 限制查看 CPU 使用率,对比耗时改用多进程
异步代码报错难排查async/await链路中异常堆栈不明显加日志,检查任务是否被gather吞掉逐个任务增加异常包装
文件读取后句柄泄漏没用with检查进程句柄数统一改用with open
依赖版本不一致导致写法报错Python 版本低,不支持f-string或pathlibpython --version升级 Python 或安装兼容性依赖
API 接口同步调用耗时太长处理逻辑是同步阻塞用压测工具观察吞吐量改为异步任务队列
配置读取用错方法用了yaml.load而不是safe_load检查依赖库告警改用safe_load
列表推导式嵌套太多,难以阅读单个表达式复杂度太高拆函数或循环控制单行复杂度
本地能跑,线上报错环境变量、路径、权限不同对比日志和配置用pathlib统一路径,增加启动检查

遇到问题时,不要第一时间怀疑“是不是这种写法有问题”。先看报错信息,再看依赖版本,最后再判断是否应该换一种写法。

10. 推荐写法优先级

结合前面内容,给出一个实用的优先级参考。

如果只记一条规则:业务代码优先考虑可读性,性能问题留到实测确认后再优化。

10.1 Python 日常场景推荐

  • 字符串拼接:优先f-string,模板复杂时先算变量。
  • 文件读写:优先with open和pathlib。
  • 列表转换:简单逻辑用列表推导式,大数据量或只需要遍历一次用生成器。
  • 异常处理:先普通if-else兜底,再考虑表达式写法。
  • 并发:I/O 密集优先线程池,CPU 密集优先进程池,长连接高并发再考虑异步。

10.2 团队协作场景推荐

团队协作中,一致性比个人偏好更重要。先约定一套基础风格,比如“文件读取一律用with”“配置读取统一用pydantic或dataclass”“不允许在新代码里写三层以上的循环嵌套”。在这个基础上,再允许个别人根据场景微调写法。

写代码前先看现有代码风格,不强行引入新特性,除非有把握能推动团队统一升级。

11. 从写代码的思路开始改变

从今天开始,拿到一个需求先不要急着写第一行代码。先想三件事:这个功能未来会不会扩展?数据量会不会变大?团队里谁会维护这段代码?

然后在脑子里列出两到三种候选写法,先在草稿上快速比较,再写正式代码。过程会稍慢,但后续省下的调试和维护时间远大于这几十秒的思考成本。

已经写过很多遍的代码,可以顺手用不同写法重构一遍。今天把列表推导式换成filter/map跑一遍,明天把for循环改成生成器跑一遍。这种练习的价值不在于炫技,而在于真正理解每种写法的边界。写多了你会发现,不是“哪一种最好”,而是“这个场景最合适的是什么”。

收藏这篇文章,等下次代码写得难受时再翻出来,对照里面的清单重新选一次写法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询