写代码这些年我见过太多人把“示例代码”当成“复制粘贴的原料”CtrlC 一下CtrlV 一下跑通了就完事。但我自己折腾下来真正让人水平提升的恰恰是那些不那么丝滑的时刻——跑不通的报错、没考虑到的边界情况、性能突然拉胯的瞬间。这个项目系列做到第 3 篇主题就是 Python examples我想换个角度聊聊示例到底应该怎么写、怎么看、怎么改才能从一个能跑的 Demo 变成真正属于你自己的工具。这篇东西适合刚学 Python 不久、看了不少教程但动起手来还是发怵的新手也适合写过一阵子业务代码、想系统整理自己代码库的同学。我会拿我实际整理过的示例库当案例把拆解思路、代码示范、常见坑位一次说清楚。1. 示例代码的规划思路先想清楚“给谁看、解决什么”1.1 明确示例的定位别贪多求全做示例和做项目不一样。项目追求的是稳定可用示例追求的是“把某个点讲透”。我自己在整理这个 Python 示例库时第一件事不是写代码而是把示例分成三类语言特性示例装饰器、生成器、上下文管理器、元类这类 Python 特有的语法点常用场景示例网络请求、文件处理、数据清洗、并发任务这类工作中反复用到的东西工程化示例日志、配置管理、测试、打包这类让代码从“脚本”变成“项目”的内容这种分类有个直接的好处每个示例的篇幅能控制在几十行到一百行左右读者不会被动辄几百行的代码吓退。更重要的是每一类都有自己的“主线逻辑”后续维护和扩展时想找哪块内容直接按目录走就行不费脑子。不建议把多个知识点硬塞进一个示例里。我见过有人写一个“爬虫示例”里面同时用了异步、代理、BeautifulSoup、正则、数据库存储、发邮件通知最后评论区全在问“这一行是干嘛的”。示例的价值在于“单一职责”一个示例讲清楚一个点组合起来自然就是一个体系。1.2 按“最小可运行”原则组织文件我整理示例库时刻意遵循一个原则拿到手就能跑跑起来就能看到效果。不需要配数据库不需要装复杂的服务可能最多就是一个pip install requests的事。每个示例目录里我会放三样东西demo.py主示例文件代码注释写得比较详细README.md说明这个示例解决什么问题、关键代码在哪几行requirements.txt依赖列表方便直接用pip install -r requirements.txt装环境有人会觉得 README 多余但实际用下来隔两三个月自己回来看README 能帮你节省大量“回忆时间”。而且如果这个示例库要分享给别人README 更是门面写得清楚一点沟通成本低很多。1.3 命名规范从文件名开始让人看懂示例库的文件命名也值得讲究。我早期整理时文件名叫test1.py、test2.py三个月后看目录完全想不起来哪个是干嘛的。现在统一用“场景-功能”的格式network_retry.py网络请求重试data_pandas_groupby.pyPandas 分组聚合async_download_pool.py异步下载池这样一眼就能看出“这是什么场景、用了什么技术、实现什么功能”。目录一展开整个示例库的能力边界清清楚楚找东西基本靠肉眼扫描就行。2. 核心示例场景拆解日常开发最常用的三类代码2.1 网络请求重试、超时、会话复用一个都不能少网络请求是 Python 日常开发里绕不开的场景。很多人写requests.get()就用默认参数本地跑没问题一到线上网络抖动就挂。我在示例库里的网络请求示例核心是展示一套“健壮请求”的写法。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session_with_retry( retry_total: int 3, backoff_factor: float 0.5, status_forcelist: tuple (500, 502, 503, 504), ) - requests.Session: session requests.Session() retry_strategy Retry( totalretry_total, backoff_factorbackoff_factor, status_forceliststatus_forcelist, allowed_methodsfrozenset([GET, POST]), ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(http://, adapter) session.mount(https://, adapter) return session session build_session_with_retry() try: resp session.get(https://httpbin.org/json, timeout(3, 10)) resp.raise_for_status() print(resp.json()) except requests.exceptions.RequestException as e: print(f请求失败: {e})这里最容易被忽略的是Retry里的backoff_factor。它控制重试间隔的递增规则公式是backoff_factor * (2 ** (重试次数 - 1))。设成 0.5 的话第一次重试等 0.5 秒第二次等 1 秒第三次等 2 秒。这个递增设计的意义在于如果服务器正处于过载或重启状态立刻重试大概率还是失败等一小段时间反而能提高成功率。设得太小等于没等设得太大用户会等到不耐烦。另外timeout参数务必显式设置。(3, 10)的意思是连接超时 3 秒、读取超时 10 秒。不设超时的请求理论上可能永远挂着这在生产环境里是致命的——线程池会被占满任务队列越堆越长整个服务跟着雪崩。2.2 数据清洗Pandas 里的“链式操作”思路日常数据处理场景里Pandas 的链式操作能让人少写一半代码但前提是理解每一步在干什么。我在示例里常用这样一段代码展示数据清洗的基本流程import pandas as pd def load_and_clean(file_path: str) - pd.DataFrame: df ( pd.read_csv(file_path) .drop_duplicates(subset[order_id]) .assign(create_timelambda x: pd.to_datetime(x[create_time])) .pipe(remove_outliers, columnamount, lower0, upper10000) ) return df def remove_outliers(df: pd.DataFrame, column: str, lower: float, upper: float) - pd.DataFrame: return df[(df[column] lower) (df[column] upper)]这个例子的关键点有两个。第一个是assign配合lambda。它会生成新列并在链式调用里“记住”前一步的结果。很多人一上来用df[create_time] pd.to_datetime(df[create_time])也没错但那个写法是在修改原 DataFrame而且链式操作时容易因为视图复制出现SettingWithCopyWarning。用assign可以保持整条链的纯净性每步返回的都是新对象心智负担小很多。第二个是pipe。它可以把一个独立的清洗函数“插”进链式流程里数据流是“读入 → 去重 → 换类型 → 去异常值”每一步都可插拔、可复用。想改异常值范围直接改remove_outliers的参数不动主流程。这种风格写多了之后你会自然地把数据处理拆成小函数而不是一个几十行的巨无霸函数从头写到尾。2.3 并发任务线程池的优雅写法与陷阱并发这块Python 的concurrent.futures是首选。我示例库里的并发下载示例代码不长但包含一个经典易错点import concurrent.futures import time from typing import Callable, Iterable, List def run_concurrent( func: Callable, items: Iterable, max_workers: int 4, timeout: float 60, ) - List: results [] with concurrent.futures.ThreadPoolExecutor(max_workersmax_workers) as executor: future_map {executor.submit(func, item): item for item in items} for future in concurrent.futures.as_completed(future_map, timeouttimeout): item future_map[future] try: result future.result() results.append((item, result, None)) except Exception as e: results.append((item, None, e)) return results def fake_download(url: str) - str: time.sleep(1) return f downloaded: {url} if __name__ __main__: urls [fhttps://example.com/{i} for i in range(10)] output run_concurrent(fake_download, urls, max_workers4) for item, result, error in output: if error: print(f{item} 失败: {error}) else: print(f{item} - {result})这里我特意保留了future.result()被try包裹的做法。很多人初学时不加 try任务一抛异常整个程序就中断了。但在批量任务里一个 URL 失败不代表其他任务没价值正确的姿势是捕捉异常、记录日志、继续处理剩余任务最后统一汇总哪些失败、失败原因是什么。另一个细节是max_workers的设置。对 CPU 密集型的 Python 代码来说线程池受 GIL 限制开 32 个线程也未必比 4 个快多少对 I/O 密集型任务比如下载、读文件、查数据库线程数可以开大一些但也不是越大越好我一般按5~10 倍于 CPU 核心数来试压测后取一个稳定值。示例里写max_workers4是保守起步真正用的时候还是要根据任务类型调参别盲目抄。3. 从示例到工程化让代码从“能跑”变成“好用”3.1 异常处理不要只写“try...except”就完事示例代码常见的毛病是有 try 但没 except或者 except 后面直接pass。这种代码跑起来很“安静”出问题也不吭声等到用户反馈“没数据”时才发现早不知道错哪了。我自己整理示例时会专门写一节“异常日志化”原则有三条捕获什么就记录什么except Exception as e然后把e写进日志不吞异常要么raise出去要么返回一个带有错误信息的默认值外部调用方要能感知到什么情况下会抛出哪个异常给一段我在示例库里的实用模板import logging logging.basicConfig( levellogging.INFO, format%(asctime)s | %(levelname)s | %(name)s | %(message)s, ) logger logging.getLogger(demo) class DataNotFoundError(Exception): 数据不存在时抛出 def fetch_data(record_id: int): logger.info(f开始获取数据, id{record_id}) if record_id 0: raise ValueError(record_id 不能为负数) try: # 模拟数据源返回空 data query_from_database(record_id) except Exception as e: logger.error(f数据库查询失败, id{record_id}, error{e}) raise if data is None: raise DataNotFoundError(fid{record_id} 对应的数据不存在) return data def query_from_database(record_id: int): if record_id 999: return None return {id: record_id, name: samplename}自定义异常DataNotFoundError是有意义的。它让调用方可以用except DataNotFoundError来精准捕获“数据缺失”这一种情况而不是看到Exception就手足无措。实际项目中异常体系设计得越细后续排查问题的速度越快。3.2 日志配置打印不该是唯一的信息出口有些示例代码里全是print()这个习惯得改。小规模调试可以但代码一旦在后台跑、或作为服务被调用print的输出没人看出了错也无迹可寻。我在示例库里的日志示例会展示logging的基本用法并解释几个关键配置的含义level日志级别最低是 DEBUG生产建议 INFO 或 WARNINGformat输出格式把时间、级别、模块名、消息都带出来Handler默认StreamHandler输出到控制台也可以加FileHandler写到文件import logging logger logging.getLogger(my_app) if not logger.handlers: handler logging.StreamHandler() handler.setFormatter( logging.Formatter(%(asctime)s | %(levelname)s | %(name)s | %(message)s) ) logger.addHandler(handler) logger.setLevel(logging.DEBUG) def divide(a: float, b: float) - float: if b 0: logger.warning(除数为 0入参 a%s, b%s, a, b) return float(inf) return a / b注意我加了if not logger.handlers这个判断。原因是在交互式环境或者反复 import 的场景下logger可能被重复添加 Handler导致日志重复打印。这个细节是我实际踩过坑之后发现的写进示例里算是给后来者提个醒。3.3 虚拟环境每个示例项目都应该有独立环境这个点看起来和“写示例代码”无关但实际非常影响体验。我在整理示例库时每个示例目录都带上requirements.txt并建议使用者创建独立的虚拟环境。原因很简单不同示例依赖的包版本可能冲突放在同一个环境里很可能出现“这个示例跑通、那个示例报错”的情况最后连是不是代码问题都分不清。cd examples/network_retry_demo python -m venv venv source venv/bin/activate pip install -r requirements.txt python demo.py分环境的成本很低但能省去大量“依赖地狱”的烦恼。这条经验是我早期把所有示例都装在一个环境里、被各种版本冲突折磨过后总结出来的也算是给所有示例项目套了一层保险。4. 调试与性能优化把“跑不起来”变成“跑得飞快”4.1 定位问题的三步策略读栈、断点、拆分写示例代码时我特意把“调试手法”作为一条主线反复强调。面对报错多数人的第一反应是百度或问 AI但更高效的做法是先自己看报错信息里的 Python 异常栈它已经标出了“哪个文件的哪一行”触发了问题。三步策略我写在示例库的 README 里看异常类型和消息TypeError、KeyError、ValueError各代表不同类型的问题消息里通常有“为什么”看 Traceback 最底部的调用关系那里是异常真正的来源而不是只看最上面用二分法隔离注释掉一半代码看是否复现复现则问题在前半段不复现则在后半段这不是什么高深理论但非常实用。很多时候代码报错了问题并不在报错的那一行而是上一步的数据结构不对。比如你调用了item[name]报KeyError: name那大概率是item的结构和预期不一致得往上看item是怎么生成的而不是在那行代码上反复猜测。4.2 利用日志和断言快速缩小范围除了print大法我推荐在示例里用assert和logging组合定位问题。给一段示例库里的演示import logging logger logging.getLogger(__name__) def process_record(record: dict) - str: assert isinstance(record, dict), frecord 必须是 dict实际是 {type(record)} assert user_id in record, frecord 缺少 user_id 字段内容是 {record} logger.debug(开始处理记录: %s, record) name record.get(name, unknown) return f{record[user_id]}: {name} records [ {user_id: 1, name: A}, {user_id: 2}, # 故意构造一条缺 name 的 ] for r in records: try: print(process_record(r)) except AssertionError as e: logger.error(断言失败: %s, e)assert的本质是在代码的“关键路口”设置检查点。开发阶段可以快速暴露“数据长歪”的情况但注意上线时可以用python -O关掉断言所以重要逻辑的校验不要完全依赖断言业务上需要强校验的地方仍然要写显式的if判断。4.3 用 cProfile 找出性能瓶颈示例代码性能优化的内容是很多同学的盲区。一个脚本跑得慢最常见的瓶颈不是“Python 本身慢”而是“你用了慢的写法”或“在循环里做了太多不必要的事”。我给你一个简单可复现的帧测流程python -m cProfile -s cumulative slow_script.py输出会列出每个函数的调用次数和累计耗时重点看tottime和cumtime。tottime是该函数本身排除子调用后的耗时cumtime是包含子调用的总耗时。如果一个函数的cumtime特别高你就要思考能不能减少调用次数能不能把循环里的重复计算提到循环外能不能用内置函数或标准库替代手写逻辑我通常还会在示例里提醒一句性能分析要在“数据量接近真实值”的时候做。拿 100 条数据测出来“很快”不代表 100 万条数据也快。不同量级下复杂度差异才会体现出来。4.4 常见问题速查表运行示例时的高频坑位我自己在整理示例库、以及帮身边人排查的时候反复遇到下面几个问题。清清爽爽列个表方便大家直接按图索骥。现象常见原因解决办法ModuleNotFoundError装错环境 / 没装依赖确认在正确的虚拟环境里执行pip install -r requirements.txtUnicodeDecodeError读 CSV 乱码文件编码不是 UTF-8读取时指定encodingutf-8或encodinggbk目标网站一直超时未设置 timeout / 被限流显式设置超时加Retry拉大间隔并发任务一堆报错future.result()没做异常捕捉仿照第 2.3 节代码在每个 future 上单独 try/exceptSettingWithCopyWarning链式操作修改了 DataFrame 副本用loc显式修改或使用assign生成新列这张表最初是写给自己的后来发现帮别人排查时百分之七八十的问题都逃不出这几类。当你照着顺序排查一圈大部分问题都能定位干净。5. 测试与文档化示例代码的“长期价值”所在5.1 给示例补上“微测试”到了这个阶段示例不再只是给人看的还得保证“以后还能跑”。我给几个核心示例写了简单的unittest或pytest用例不求全覆盖只求验证主路径和几个关键边界import unittest from demo_utils import build_session_with_retry, divide class TestDemoUtils(unittest.TestCase): def test_retry_session_success(self): session build_session_with_retry() self.assertIsNotNone(session) def test_divide_zero(self): result divide(1, 0) self.assertEqual(result, float(inf)) if __name__ __main__: unittest.main()有人会觉得示例写测试太小题大做但我的实际体验是示例代码最容易被“稍微改一改”弄坏。有了微测试改动后扫一遍立刻知道哪些功能被影响这在示例库持续更新时非常有用。而且写测试的过程本身也会倒逼你把示例里那些隐性的假设写清楚——函数入参是什么、返回什么、异常怎么处理全都会浮出水面。5.2 README 模板让你的示例“易于消费”我分享到社区时最怕看到只有代码、没有任何说明的仓库。读代码本身是件费力的活儿给人一条明路反而能让你的示例被更多人真正用起来。下面是我慢慢打磨出的一套极简 README 模板# 示例名称 这个示例解决什么问题一句话说清楚。 - 适用场景什么时候用得上 - 依赖Python 版本、第三方库 - 快速开始 1. 创建虚拟环境 2. 安装依赖 3. 运行 demo.py 关键代码位置文件里第几行到第几行核心逻辑是什么。 注意事项有哪些边界情况需要额外处理。模板不需要花哨但贵在“让人三分钟之内知道该干什么”。我见过有人 README 写得比代码长几倍但核心信息全被淹没在背景介绍里那也不行。好的 README 是“地图”不是“论文”。6. 持续维护示例库从个人收藏到团队资产最后一个想聊的点是如何让示例库存活下来。很多人的示例库建了没几天就成了死库代码写完就再也不更新。我有几个小习惯能让这个库越用越顺手每次在真实项目里“踩坑并解决”之后把最小复现代码和解决方案提炼成一个示例保证每个示例都有真实场景背书定期跑一遍所有示例的测试确保 Python 版本升级后没有出现兼容性问题把“坏味道”的示例删掉或者重写宁可目录短一点也不要留一些过时的、连自己都看不懂的代码这样做的好处是示例库慢慢会长成你自己的“代码字典”。新项目开工时先到库里搜一圈有没有可复用的模块遇到问题查不到解法时新解法又会沉淀成新示例。这是一个越滚越大的正循环。我个人比较强烈的体会是示例库这东西维护成本比建立成本低但很多人恰恰输在“只想建立、不想维护”。所以从第一篇开始就给每个示例配上测试和说明这是我认为最值得坚持下来的做法。写到最后再说一个可能对你有用的小技巧给示例代码写注释时少写“这行代码做什么”多写“为什么要这样做”和“不这样做会怎样”。前者是代码的复读机后者才是真正的思考过程。这份 Python 示例库整理到现在我最满意的不是代码本身而是它让我把很多“以为懂了”的知识点真正过了一遍脑子变成了“确实懂了”的攻守兼备的工具。