page contents

详解Python文件: .py、.ipynb、.pyi、.pyc、​.pyd !

昨天晚上十一点多吧,我在公司楼下抽烟(别学啊…就是加班烦),我们组小李突然甩我一句:“哥,为啥我这项目目录里又有 .pyc 又有 .pyd,还有人给我塞了个 .pyi,我都快怀疑人生了。”我说你先别急,先把烟掐了…咱把这几种文件当成“同一个人不同证件照”,你就通了。

attachments-2026-02-rHul6rxe698d2db23c38a.png昨天晚上十一点多吧,我在公司楼下抽烟(别学啊…就是加班烦),我们组小李突然甩我一句:“哥,为啥我这项目目录里又有 .pyc 又有 .pyd,还有人给我塞了个 .pyi,我都快怀疑人生了。”我说你先别急,先把烟掐了…咱把这几种文件当成“同一个人不同证件照”,你就通了。

先说最常见的 .py,这个就不用装了吧,就是你写的源码。你写 print("hello"),解释器就一行一行读、一行一行跑。它最大特点是“人能读懂,机器也能跑”,但机器跑的时候其实会偷偷做一件事:把你这堆文本先编译成字节码,方便下次启动快一点点。这个“偷偷干活”的产物,就是 .pyc 的来源。

我之前有次线上急救,容器里代码没变但启动突然变慢,最后发现镜像里把 __pycache__ 清掉了…咳,扯远了。反正 .py 是原材料,后面那些都是加工品。

你要真想亲眼看 .pyc 怎么出来的,不用等它“偷偷生成”,你可以自己手动来一把:

# demo.py 先随便写点东西
# def add(a, b): return a + b

import py_compile
import marshal
import struct

py_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 json
from pathlib import Path

nb = 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.py
def add(a, b):
    return a + b

然后你配一个同名的 .pyi,把类型写清楚(注意:这不是给解释器看的,是给“检查器”看的):

# math_utils.pyi
from typing import overload

@overload
def add(a: int, b: int) -> int: ...
@overload
def add(a: float, b: float) -> float: ...
def add(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 m

print("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 报错”,我手机都震半天了,走了走了。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1799 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 1799 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章