详解Python文件: .py、.ipynb、.pyi、.pyc、.pyd !
昨天晚上十一点多吧,我在公司楼下抽烟,我们组小李就拿手机怼我脸上:哥你看我这个目录里一堆奇怪文件,.py、.ipynb、.pyi、.pyc、还有个.pyd,我都不敢删……我当时困得要死,但这玩意你真要讲清楚,其实就跟“厨房里一堆调料瓶”一样,你不认识它,就觉得每个都能毒死你,认识了就知道谁是盐谁是味精。
先说最常见的 .py,这个就别装了,就是你写的源码文本。你写的每一行,解释器都会在运行时把它“翻译”成内部能跑的东西(字节码),然后执行。很多人以为 .py 一运行就是“机器码”,不是的啊,Python大部分情况是先变成字节码再跑。你要是想肉眼看看一个 .py 到底被编成啥,可以用 dis 把它拆开,像把菜刀磨给你看那种感觉:
import dis
defadd(a, b):
return a + b
dis.dis(add)
你会看到一堆 LOAD_FAST / BINARY_OP 之类的,反正意思就是:Python先把逻辑编成“指令列表”,再由解释器执行。这个过程就引出了 .pyc。
.pyc 这玩意就是“编译后的缓存”。你经常在项目里看到 __pycache__/xxx.cpython-311.pyc 这种,别慌,它不是病毒,也不是你同事在你电脑上偷偷塞的后门,默认就是这样。它的目的很朴素:下次导入模块时少干点活,省启动时间。你甚至可以自己主动批量生成 .pyc,比如线上容器冷启动很抠门那种场景:
import compileall
compileall.compile_dir(".", quiet=1) # 当前目录递归编译
然后你会看到 __pycache__ 冒出来。一般别手动改 .pyc,没意义,而且版本不匹配就直接废。哦对了,小李当时还问:那我删了 .pyc 会怎么样?我说删就删呗,顶多下次 import 的时候重新生成,跟你把外卖袋子扔了一个道理。
你要真想“确认一个文件是不是pyc”,可以读一下前几个字节(它有魔数),大概就像看酒瓶盖上的钢印那种:
import importlib.util
from pathlib import Path
p = next(Path(".").rglob("*.pyc"), None)
if p:
data = p.read_bytes()
print("pyc magic:", data[:4].hex())
print("cpython magic expected:", importlib.util.MAGIC_NUMBER.hex())
看到 magic 对得上,基本就知道这玩意是给哪个Python版本用的。对不上?那就是你拷贝来的缓存,或者版本换了,扔了也行。
再说 .ipynb,这个我一般跟产品经理解释成“会动的PPT”。它本质是 JSON 文件,里面装了:代码块、输出、图片、执行顺序、甚至你当时的报错。你在 Jupyter 里点一下就跑,所以它很适合做实验、做数据分析、做演示。但它也挺坑:合并冲突像噩梦,因为 JSON 一变就是一坨。你要是真想证明它就是 JSON,随便读一段就知道了:
import json
with open("demo.ipynb", "r", encoding="utf-8") as f:
nb = json.load(f)
print(nb.keys()) # 一般有 cells / metadata / nbformat ...
print(len(nb.get("cells", [])))
我们线上一般不让 .ipynb 直接进核心仓库,除非你是算法同学要保留实验记录。否则你哪天把输出也提交了,仓库体积直接起飞,CI还慢得跟拖拉机一样。
然后是 .pyi,这个就更像“说明书”,而不是代码本体。它是类型存根(type stub),给 IDE、给 mypy 这种静态检查工具用的。你可以理解成:你只写函数签名、类型、注释,不写实现。实现还是在 .py 里跑。这个在你们做 SDK、做库的时候特别好用:我可以把实现藏起来(或者实现很乱不想给人看),但把接口说明清清楚楚给出去。
举个很小的例子,假设你有个库 mylib.py,实现随便写,但你给一个 mylib.pyi:
# mylib.pyi (注意:这里只是示例,放到同名模块旁边)
from typing import Iterable, List
deftopk(nums: Iterable[int], k: int) -> List[int]: ...
那 IDE 就能推断 topk 的返回类型,调用方写错类型会被提前提示。小李当时听到这里就来劲了:那我是不是可以靠 .pyi 把团队的“口口相传接口文档”干掉?我说对…差不多,但你也得配合类型检查工具,不然就是“写了个摆设”。
最后这个 .pyd,很多人第一次见都以为是“加密Python文件”。不是啊兄弟,它在 Windows 上通常是“扩展模块”,本质是一个动态链接库(DLL),只是后缀叫 .pyd,Python 可以 import 它。你可以把它理解成:用 C/C++/Rust 写的高性能模块,编译好丢给 Python 用,速度快,或者封装系统能力。Linux/macOS 下对应常见的是 .so(shared object)。
最直观的体验就是:你 import someext,但目录里根本没有 someext.py,只有 someext.pyd,它照样能导入。你在代码里可以这么判断自己导入的是啥来源(别笑,我线上排查过这个):
import importlib
import platform
name = "someext"# 假设你有这个扩展模块
try:
m = importlib.import_module(name)
print("module:", m)
print("file:", getattr(m, "__file__", None))
print("platform:", platform.system())
except Exception as e:
print("import failed:", e)
一般 __file__ 会指向 .pyd 或 .so。这类东西你别手贱去改名,别用文本编辑器打开,说白了就是二进制。然后有个坑,小李也踩过:你把 .pyd 从一台机器拷到另一台,可能直接 import 失败,因为它跟 Python 版本、系统架构、依赖库都有关。就跟你把“只能装在某型号车上的涡轮”硬塞给另一辆车一样,能点火才怪。
所以你看,.py 是你写的菜谱,.pyc 是提前切好的配菜缓存,.ipynb 是边做边拍的视频笔记,.pyi 是给别人看的菜单说明,.pyd 就是你外包给厨师长做的半成品预制菜(而且还得配套厨房设备)。你把这个类比记住,基本就不会慌。
啊对了,我当时还顺手给小李说了句:目录里这些东西,你只要牢记一个原则——“能还原的就别当宝”,.pyc、__pycache__ 这类,删了会再生;.ipynb 要当资料就好好管理;.pyi 是接口契约;.pyd 别乱动,出问题先看版本匹配……行了我先不说了,刚刚钉钉又响了,估计线上又有人把缓存目录提交进仓库了,烦死了。