昨天晚上十一点多吧,我在公司楼下抽烟(别学啊…就是加班烦),我们组小李突然甩我一句:“哥,为啥我这项目目录里又有 .pyc 又有 .pyd,还有人给我塞了个 .pyi,我都快怀疑人生了。”我说你先别急,先把烟掐了…咱把这几种文件当成“同一个人不同证件照”,你就通了。
先说最常见的 .py,这个就不用装了吧,就是你写的源码。你写 print("hello"),解释器就一行一行读、一行一行跑。它最大特点是“人能读懂,机器也能跑”,但机器跑的时候其实会偷偷做一件事:把你这堆文本先编译成字节码,方便下次启动快一点点。这个“偷偷干活”的产物,就是 .pyc 的来源。
我之前有次线上急救,容器里代码没变但启动突然变慢,最后发现镜像里把 __pycache__ 清掉了…咳,扯远了。反正 .py 是原材料,后面那些都是加工品。
你要真想亲眼看 .pyc 怎么出来的,不用等它“偷偷生成”,你可以自己手动来一把:
# demo.py 先随便写点东西# def add(a, b): return a + bimport py_compileimport marshalimport structpy_compile.compile("demo.py", cfile="demo.pyc")with open("demo.pyc", "rb") as f: magic = f.read(4) # 魔数:不同Python版本不一样 flags = struct.unpack("<I", f.read(4))[0] f.read(8) # 时间戳/哈希相关字段(看flags) code_obj = marshal.load(f) # 真正的字节码对象print("magic:", magic)print("flags:", flags)print("co_names:", code_obj.co_names)
你运行完会看到一个 demo.pyc。重点来了:.pyc 不是“加密源码”,别想着拿它当防泄露(很多人真这么想过…)。它就是字节码缓存,而且跟 Python 版本、实现、甚至一些编译选项相关,版本不匹配就直接废掉。也就是为啥你经常看到 __pycache__/demo.cpython-311.pyc 这种带版本味道的名字。
然后是 .ipynb,这个是 Jupyter Notebook 的本体。它其实不是“代码文件”,它是一个 JSON 文档,里面塞了:一段段 cell、输出、执行计数、元数据。你说它像啥…像你开会时记的笔记:正文、截图、谁说的、哪天说的,全混在一起。它好处是交互、可视化、实验超爽;坏处是合并冲突能把人整崩溃(别问我怎么知道的)。
你不信它是 JSON?来,随手扒一下 code cell:
import jsonfrom pathlib import Pathnb = json.loads(Path("note.ipynb").read_text(encoding="utf-8"))for i, cell in enumerate(nb.get("cells", []), 1):if cell.get("cell_type") == "code": src = "".join(cell.get("source", [])) print(f"\n--- cell {i} ---") print(src.strip())
所以很多团队会约定:实验在 .ipynb,落地交付要整理成 .py,不然后面做工程化(测试、CI、代码审查)会很难受。尤其是你把输出也提交了,Git diff 跟彩票开奖一样花。
再说 .pyi,这个东西我一开始也觉得别扭,“我明明有 .py 了,还要再写个壳子?”但你把它当“类型说明书”就舒服了。.pyi 叫 stub file,主要给类型检查器(mypy、pyright 这种)看的,运行时基本不靠它。典型场景是:你库里实现很动态、或者你是二进制扩展(后面 .pyd 那种),那类型检查器啥也猜不准,就靠 .pyi 把接口说清楚。
举个很小的例子,你写了个动态函数,在 .py 里很随意:
# math_utils.pydefadd(a, b):return a + b
然后你配一个同名的 .pyi,把类型写清楚(注意:这不是给解释器看的,是给“检查器”看的):
# math_utils.pyifrom typing import overload@overloaddefadd(a: int, b: int) -> int: ...@overloaddefadd(a: float, b: float) -> float: ...defadd(a, b): ...
你写业务代码的时候,IDE 自动补全、静态检查就会“像个人一样懂你”。我们组之前接手一个老项目,函数全是 dict 里掏来掏去,补全等于没有,后来补了一层 .pyi,体验直接从石器时代到电动牙刷时代…就是那种感觉。
最后这个 .pyd,很多人第一次见是在 Windows 上,心里咯噔一下,“这是不是病毒文件?”不是不是,它是 Python 扩展模块,本质上是动态链接库,只不过扩展名在 Windows 叫 .pyd(Linux/macOS 常见的是 .so)。它里面一般是 C/C++(或者 Cython、Rust、啥都行)编译出来的,性能猛,缺点也明显:平台相关、Python 版本相关、架构相关(32/64),配错一个就 ImportError 给你脸色看。
你可以用 Python 自己看看“我这环境支持哪些扩展后缀”:
import importlib.machinery as mprint("extension suffixes:")for s in m.EXTENSION_SUFFIXES: print(" ", s)
在 Windows 上你大概率能看到 .pyd 在列表里。也就是说,import xxx 的时候,解释器会按一堆规则去找:先找包、再找 .py、再找缓存、再找二进制扩展……所以你项目里如果同时有 fast.py 和 fast.pyd,实际加载谁,有时候会把新人坑到怀疑人生(别搞同名,真的)。
对了,.pyd 往往会配一个 .pyi,这就很合理:二进制里你看不到函数签名,类型工具也看不懂,那就用 .pyi 把接口补齐。我们之前做过一个图像处理模块,核心算子放 .pyd 里跑,外面套一层 Python API,然后 .pyi 把类型标上,既快又好用。
你看,串起来就清楚了:.py 是你写的,.pyc 是它编译后的缓存件,.ipynb 是实验台账本,.pyi 是接口说明书,.pyd 是“性能外挂”(但带平台限制)。下次谁再问你目录里这些玩意是啥,你就说:别慌,都是同一个生态里的不同零件,放对位置就顺手,放错位置就…哎就像把螺丝拧在木头上,迟早裂。
行了我先不说了,刚才群里又有人喊我看个“奇怪的 import 报错”,我手机都震半天了,走了走了。