资讯动态

Python入门之模块与包

发布时间:2026/10/16 4:00:05 来源:尧图企业网站定制
前言初学 Python 时「模块」和「包」这两个词经常被混着用反正都是「一块代码」。但它们在语言层面是两个不同层级的东西模块是一个.py文件包是一个装着模块的目录。搞不清这一点后面遇到的怪现象就没法解释——为什么有的目录能导入、有的不能为什么删掉__init__.py之后行为变了为什么import的路径要一层一层写下来。还有两个很常见的误解。其一以为包必须永远有__init__.py——从 Python 3.3 起隐式命名空间包implicit namespace package允许没有__init__.py的目录也作为包导入但常规包仍然需要它两者的行为并不相同。其二以为「安装第三方库」和「写自己的包」是两回事——它们用的是同一套 import 机制。本文从最小例子出发把模块、包、子包、搜索路径串成一条线。一、从模块到包为什么需要目录单文件模块用着舒服但一个项目上百个文件时把所有东西平铺在根目录会失控。包解决的就是分类与命名空间问题。myproject/├── main.py└── shapes/├── __init__.py├── circle.py└── square.py这里的shapes就是一个包它是一个目录目录里有__init__.py还有两个模块文件。main.py里这样用# 适用于 Python 3.8from shapes import circlefrom shapes.square import area_of_squareprint(circle.area_of_circle(1.0))print(area_of_square(2.0))shapes.circle这种点号路径就是包的层级表达shapes是包circle是包里的模块。点号每多一层就多下一级目录。那__init__.py是干嘛的在常规包里它是包的初始化文件它的存在让 Python 把这个目录当作一个常规包regular package来对待。它里面的顶层代码在包第一次被导入时执行可以用来做初始化、建立包级别的变量。它可以定义__all__影响from 包 import *的行为。它可以让包对外暴露一套「门面」接口把内部结构的细节藏起来。一个最简的__init__.py可以是空文件。也可以什么都不做只留一个文档字符串。二、__init__.py与相对导入__init__.py里该放什么很多人把__init__.py当成「随便放点东西的杂物间」其实它有明确的职责边界。下面是两种常见且合理的用法。第一种暴露统一的对外接口。# 适用于 Python 3.8# 文件名 shapes/__init__.pyfrom .circle import area_of_circlefrom .square import area_of_square__all__ [area_of_circle, area_of_square]这样一来使用者可以写from shapes import area_of_circle而不必知道它其实在shapes.circle里。将来你把模块改名或拆分成两个文件只要__init__.py的对外名字不变调用方的代码也不用改。这是封装变化最省事的办法。第二种只在被显式使用时才导入重的东西。如果子模块会拉起数据库驱动、读大文件就不要在__init__.py里无条件导入它否则「只想用包里一个小工具」的人也被迫付代价。__all__的作用需要说清楚它只影响from 包 import *这一种写法写法__all__是否生效实际导入的名字from shapes import *生效__all__里列出的名字from shapes import *无__all__不适用所有不以下划线开头的顶层名字import shapes不适用只绑定shapes这个名字from shapes.circle import area_of_circle不适用显式点名的那个名字由于import *本身就不推荐使用__all__更大的价值其实是当作文档一眼就能看出这个包对外的公开接口是哪些。相对导入包内模块怎么写在包内部两个兄弟模块之间互相引用有两种写法。# 适用于 Python 3.8# 文件名 shapes/circle.py# 绝对导入从顶层包名开始写from shapes.square import area_of_square# 相对导入点号表示当前包from .square import area_of_square相对导入里的点号规则很简单一个点表示当前包两个点表示上一级包以此类推。from ..utils import helper就是从父包下的utils里取helper。相对导入有一条硬性限制只能在包内部使用。如果一个.py文件是被直接运行的python shapes/circle.py它的__name__是__main__、__package__是空字符串此时任何相对导入都会失败报出「attempted relative import with no known parent package」这类错误。解决办法是别直接运行包内的模块而是从包的外面用-m跑python -m shapes.circle这样解释器知道模块属于哪个包相对导入才成立。三、命名空间包没有__init__.py的包Python 3.3 起对应 PEP 420如果一个目录没有__init__.py但符合包名的目录结构它仍然可以被导入成为命名空间包namespace package。它和常规包的关键差别在于对比项常规包命名空间包是否需要__init__.py需要不需要__init__.py代码是否执行会执行没有这个文件无从执行__path__的类型普通列表特殊对象可跨多个目录合并主要用途绝大多数日常开发把同一命名空间拆分到多个独立发布的分发包命名空间包最典型的用途是「一个公司多个团队各自发布子包但共享同一个顶层名字」。这属于分发层面的技巧日常写业务代码时继续老老实实放__init__.py更省心因为常规包的行为更可预测。值得注意的是Python 2 没有命名空间包也不支持只写from . import x这种现代写法而且 Python 2.7 已于2020 年 1 月 1 日停止维护。今天没有任何理由再写 Python 2 的包结构。四、一个能跑起来的小包把前面的东西拼成一个完整例子。目录结构demo/├── run.py└── mypkg/├── __init__.py├── textutil.py└── mathutil.py三个包内文件# 适用于 Python 3.8# 文件名 mypkg/__init__.pymypkg一组小工具。from .textutil import shoutfrom .mathutil import average__all__ [shout, average]__version__ 0.1.0# 适用于 Python 3.8# 文件名 mypkg/textutil.pydef shout(text, times1):把文本转成大写并重复若干次用空格连接。if times 1:raise ValueError(times 必须大于等于 1)return .join([text.upper()] * times)# 适用于 Python 3.8# 文件名 mypkg/mathutil.pydef average(numbers):返回平均值空序列抛 ValueError。values list(numbers)if not values:raise ValueError(numbers 不能为空)return sum(values) / len(values)包外的入口脚本# 适用于 Python 3.8# 文件名 run.pyfrom mypkg import shout, averageimport mypkgprint(shout(hello, 3)) # HELLO HELLO HELLOprint(average([1, 2, 3, 4])) # 2.5print(mypkg.__version__) # 0.1.0在demo目录下用python run.py执行即可。这里run.py和mypkg是同级所以sys.path里的脚本目录正好能搜到mypkg。反过来如果你在demo外面运行python demo/run.pysys.path里加入的是demo目录依然能找到mypkg。但如果你把run.py放到demo/scripts/里再去运行mypkg就会报ModuleNotFoundError——因为mypkg不在sys.path的任何一项里。这时要么调整目录结构要么把包安装成可分发的形式要么在开发期用可编辑安装让解释器知道它在哪。常见坑点1. 以为没了__init__.py包就废了❌ 听到「3.3 起不需要__init__.py」就把项目里所有的__init__.py删掉结果from mypkg import x的语义变了原本在__init__.py里做的初始化也没人执行了。 ✅ 分清常规包和命名空间包日常项目保留__init__.py只在刻意做跨分发的命名空间共享时才去掉。2. 直接运行包内模块❌ 用python mypkg/textutil.py跑文件里面只要有一个from .mathutil import average就报相对导入错误。 ✅ 从包的外层目录用python -m mypkg.textutil运行。3.__init__.py里塞满副作用❌ 在__init__.py里直接连数据库、读配置文件导致只要有人import mypkg就触发这些动作测试和打包时到处出问题。 ✅ 把初始化封装成显式函数让使用者自己决定何时调用。4. 误以为__all__能限制一切访问❌ 以为写了__all__ [shout]就没人能访问mypkg.mathutil了。 ✅ 记住__all__只影响from 包 import *想表达「内部实现」应该靠下划线前缀命名和文档约定。5. 目录里忘了.py后缀❌ 把文件命名成textutil而不是textutil.py导入时找不到模块。 ✅ 模块文件必须带.py后缀。6. 包名和标准库重名❌ 建一个叫test或json的包结果跑测试或导 JSON 时被自己的包抢先匹配。 ✅ 包名加项目前缀如acme_test、acme_json。7. 把包当成「安装好的库」❌ 认为import mypkg能成功就代表包已经安装。 ✅ 只是当前工作目录或sys.path恰好能找到而已要真正安装并让任何目录都能导入需要走打包流程。8. 在包内用隐式相对导入那种旧思路❌ 按 Python 2 的习惯写import textutil指望它自动找同目录Python 3 只会去sys.path顶层找找不到就报错。 ✅ 包内一律显式写from .textutil import ...或完整的绝对路径。总结概念形态关键点模块单个.py文件导入即执行顶层代码只执行一次常规包含__init__.py的目录行为可预测日常首选命名空间包不含__init__.py的目录3.3 起支持用于跨分发包共享命名空间__init__.py包的初始化文件首次导入包时执行可定义__all__相对导入from .x import y只能在包内用直接运行单文件会失败记住一句话模块是文件包是目录点号是层级。把__init__.py当作包的对外门面而不是杂物间再配合python -m而不是直接运行包内文件绝大多数导入错误都会在源头消失。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑