跳到主要内容

6. 模块:把代码装进文件里

对应官方文档:6. Modules 一句话导读:这一章解决「程序一长怎么拆」——把定义装进 .py 文件、用 import 复用、用包分层,学完能从写单个脚本升级到组织一个小项目。

退出解释器再进来,之前定义的函数和变量全没了。想写长一点的程序就得把代码存进文件里跑,官方叫脚本;程序再长大,就该拆成好几个文件互相引用。村长按 6.1 到 6.4 的编号走,6.1 和 6.4 底下再分小节。凡是「自己造一个模块文件」的例子,统一用 tempfile 建临时目录,复制粘贴就能跑,不依赖你硬盘上真有那些文件。

6.1 深入模块​

一个模块就是装着 Python 定义和语句的 .py 文件,文件名去掉后缀就是模块名,模块里 __name__ 这个全局变量存着自己的名字。模块有自己独立的命名空间:作者可以放心用全局变量,不怕和调用方撞名;调用方也能用 模块名.名字 去摸模块里的东西。官方举的例子是 fibo.py:

>>> import fibo
>>> fibo.fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
>>> fibo.__name__
'fibo'

这里先把最容易混的两兄弟讲清楚:

写法在当前命名空间里绑定了什么怎么调用
import fibo只绑定模块对象 fibofibo.fib(500)
from fibo import fib只绑定函数 fib,不引入 fibofib(500)
import fibo as f绑定模块对象,但改名叫 ff.fib(500)
from fibo import fib as f绑定函数,但改名叫 ff(500)
from fibo import *导入所有不以下划线开头的名字fib(500)

from ... import * 会把一堆来路不明的名字灌进来,可能悄悄覆盖你已有的定义,官方明确说正式代码里不推荐,只适合交互模式偷懒。而 import 与 from 更深的差别在于绑定:from m import f 只是把 f 这个对象贴到当前命名空间,之后模块里若把 m.f 重新指向别的东西,你手里那份不会跟着变;import m 每次都通过模块对象去取,永远拿到最新的。

还有个官方特别标注的机制:出于效率,每个模块在一次解释器会话里只导入一次。第一次 import 时执行模块顶层语句并缓存进 sys.modules,之后再导入同一个名字只是把同一个对象取出来。

import sys, os, tempfile
source = ("print(' >> 模块顶层语句只执行一次')\n"
"VALUE = 42\n"
"def double(x):\n return x * 2\n")
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, 'demo_mod.py'), 'w', encoding='utf-8') as f:
f.write(source)
sys.path.insert(0, d)
try:
import demo_mod # 第一次导入:执行顶层语句
import demo_mod as alias # 第二次导入:命中缓存,不再执行
print(demo_mod.VALUE, demo_mod.double(3)) # -> 42 6
print('同一个对象:', alias is demo_mod, '| 已缓存:', 'demo_mod' in sys.modules)
ns = {'__name__': '__main__'}
exec('from demo_mod import double', ns)
print('from 只带来:', sorted(k for k in ns if not k.startswith('__')), ns['double'](5))
finally:
sys.path.remove(d)
sys.modules.pop('demo_mod', None)

村长划重点:改了模块代码却「没生效」,八成是撞上了这份缓存——要么重启解释器,要么 import importlib; importlib.reload(模块名)。这也是为什么开发时改了 .py 要重新启动程序,别指望旧进程自动刷新。

6.1.1 把模块当脚本执行​

用 python fibo.py 50 直接跑一个模块时,代码照常执行,唯一的区别是 __name__ 被设成了 __main__。所以文件末尾常挂一段 if __name__ == '__main__': 把命令行解析包进去,让同一个文件既能被导入、又能当命令行工具:

import sys
def main(args):
print('脚本模式:程序入口在这里,参数 =', args)
print('当前 __name__ =', __name__)
if __name__ == '__main__':
main(sys.argv[1:] or ['50'])

把上面存成文件直接运行,会打印 __main__ 并调用 main();换成被 import 时,__name__ 是模块名,main() 就不会自动跑。这个模式太常用了,村长叫它「文件的双面人生」:被导入时是安静的工具箱,被直接跑时是带界面的小程序,还顺便能塞一段自测代码。

$ python fibo.py 50
0 1 1 2 3 5 8 13 21 34

6.1.2 模块搜索路径​

import spam 时解释器先翻内置模块(名字列在 sys.builtin_module_names),没找到再去 sys.path 这一串目录里找 spam.py。sys.path 初始化自三个地方:

  • 输入脚本所在目录(没指定文件时就是当前目录);
  • 环境变量 PYTHONPATH 里列的目录;
  • 安装相关的默认路径,通常包含 site-packages。
import sys
print('sys.path[0]:', repr(sys.path[0]))
print('搜索路径共', len(sys.path), '项,含 site-packages:', any('site-packages' in p for p in sys.path))
print('内置模块共', len(sys.builtin_module_names), '个,前 5 个:', sys.builtin_module_names[:5])

程序启动后还能改 sys.path,比如 sys.path.append('/my/lib') 塞进自己的库目录。但有个副作用必须知道:脚本所在目录排在标准库之前,所以把自己文件命名成 json.py、string.py、random.py,同名标准库就会被它遮蔽,报错报得莫名其妙。

村长划重点:别拿标准库模块名给自己的文件起名。排查「明明 import 了却报 AttributeError」时,先看目录里有没有 json.py 这种李鬼,再 print(json.__file__) 确认到底加载了谁。

6.1.3 「编译」的 Python 文件​

为了加快加载,Python 会把每个模块编译后的字节码缓存到 __pycache__ 目录,文件名形如 module.cpython-313.pyc,其中 313 是解释器版本。注意这只是字节码缓存,不是 C 那种编译成机器码,Python 依旧解释执行。

import json, os, importlib.util
print('源码位置:', json.__file__)
print('字节码缓存:', json.__cached__)
print('由源码名推导:', os.path.basename(importlib.util.cache_from_source('demo.py')))

官方给了几条让人安心的细节:源码没改就直接用缓存,改了会自动重编;缓存和 Python 版本绑定,换版本互不干扰;从 .pyc 加载并不比从 .py 加载更快,快的只是「不用重新编译」;用 -B 或 PYTHONDONTWRITEBYTECODE 可以干脆不写缓存。

6.2 标准模块​

Python 自带一整套标准库,另有一批模块直接编进解释器里,要么为了效率、要么为了封装系统调用(比如只有 Windows 才有的 winreg)。最值得先认识的是 sys,它存在于每一个解释器里;os 则负责路径、目录、环境变量、进程这些和操作系统打交道的活。

import sys, os
print('平台:', sys.platform, '| 版本:', sys.version.split()[0])
print('当前工作目录:', os.getcwd())
print('sys 里带 path 的名字:', [n for n in dir(sys) if 'path' in n.lower()])
print('命令行参数:', sys.argv)

sys.ps1 / sys.ps2 定义交互模式的 >>> 和 ... 提示符(只在交互模式下存在),sys.path 就是上一节的搜索路径,能当普通列表改。想知道某个模块到底有什么,紧接着的 dir() 就是干这个的。

6.3 dir() 函数​

内置函数 dir() 用来查「一个模块(或对象)定义了哪些名字」,返回排好序的字符串列表。不带参数时列出当前作用域的名字;带参数时列出那个对象公开的属性和方法。

import sys
print('dir(sys) 里带 path 的名字:', [n for n in dir(sys) if 'path' in n.lower()])
def inner():
local_var = 1
print('函数内 dir():', sorted(n for n in dir() if not n.startswith('_')))
inner() # -> 函数内 dir(): ['local_var']
print('dir(str) 前几个公开方法:', [n for n in dir(str) if not n.startswith('_')][:6])

你可以这样理解:dir() 是模块递给你的「目录页」,配合 help() 或 __doc__ 就能不翻文档地摸索一个陌生模块。官方也提醒,它默认不列出内置函数和变量;想看内置名字,去查 builtins 模块。

6.4 包​

包就是「用目录组织起来的一堆模块」,靠带点的名字 A.B 定位:目录 A 里的 B.py 就是 A.B。目录里放一个 __init__.py(可以为空)告诉 Python「这是一个包」,它本身也是包被导入时最先执行的代码。下面临时造一个 sound 包,演示三种导入写法:

import tempfile, os, sys
FILES = {
'sound/__init__.py': '',
'sound/effects/__init__.py': '',
'sound/effects/echo.py': 'def echofilter(x):\n return f"echo: {x}"\n',
}
with tempfile.TemporaryDirectory() as d:
for rel, src in FILES.items():
p = os.path.join(d, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, 'w', encoding='utf-8').write(src)
sys.path.insert(0, d)
try:
import sound.effects.echo as echo
print('① 完整路径:', echo.echofilter('hi'))
from sound.effects import echo as echo2
print('② from 包 import 子模块:', echo2.echofilter('hi'))
from sound.effects.echo import echofilter
print('③ 直接拿函数:', echofilter('hi'))
finally:
sys.path.remove(d)
[sys.modules.pop(k, None) for k in list(sys.modules) if k.startswith('sound')]

三种写法依次是:import 全路径(用的时候得写全名)、from 包 import 子模块(子模块名进当前命名空间)、from 子模块 import 函数(函数直接可用)。as 重命名对这三种都生效。

6.4.1 从包里导入 * 与 all​

from sound.effects import * 会导入哪些子模块?如果包的 __init__.py 里定义了 __all__,就以这个列表为准;否则只导入包命名空间里不以下划线开头的名字——注意并不会自动把磁盘上所有子模块捞进来,所以没 __all__ 时 import * 常常什么都没拿到。__all__ 就是包作者亲手写的「公开清单」。

import tempfile, os, sys
FILES = {
'sound/__init__.py': '__all__ = ["echo", "surround"]\n',
'sound/echo.py': 'def hi():\n return "echo"\n',
'sound/surround.py': 'def hi():\n return "surround"\n',
'sound/_private.py': 'def hi():\n return "private"\n',
}
with tempfile.TemporaryDirectory() as d:
for rel, src in FILES.items():
p = os.path.join(d, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, 'w', encoding='utf-8').write(src)
sys.path.insert(0, d)
try:
ns = {'__name__': '__main__'}
exec('from sound import *', ns)
print('import * 拿到的名字:', sorted(k for k in ns if not k.startswith('__')))
# -> ['echo', 'surround'],列在 __all__ 里的子模块会被一并导入
finally:
sys.path.remove(d)
[sys.modules.pop(k, None) for k in list(sys.modules) if k.startswith('sound')]

_private 以下划线开头,既不在 __all__ 里、也不会被 import * 带出来,这正是「约定俗成的私有」在包层面的体现。

6.4.2 包内引用与相对导入​

包里的模块互相引用时,写死全名当然可以,但包一改名就得全改。更舒服的是相对导入:一个点 . 表示当前包,两个点 .. 表示上一级包,from . import 兄弟 / from .. import 叔叔 都基于当前模块的 __package__ 解析。

import tempfile, os, sys, importlib
FILES = {
'pkg/__init__.py': '',
'pkg/helper.py': 'def tag(x):\n return f"<{x}>"\n',
'pkg/filters/__init__.py': '',
'pkg/filters/surround.py': 'def apply(x):\n return f" [{x}] "\n',
'pkg/filters/echo.py': ('from . import surround\n' # 同级 pkg.filters.surround
'from .. import helper\n' # 上一级 pkg.helper
'def run(x):\n'
' return helper.tag(surround.apply(x))\n'),
}
with tempfile.TemporaryDirectory() as d:
for rel, src in FILES.items():
p = os.path.join(d, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, 'w', encoding='utf-8').write(src)
sys.path.insert(0, d)
try:
mod = importlib.import_module('pkg.filters.echo')
print('相对导入结果:', mod.run('hi')) # -> < [hi] >
finally:
sys.path.remove(d)
[sys.modules.pop(k, None) for k in list(sys.modules) if k.startswith('pkg')]

村长划重点:相对导入依赖「我是哪个包的成员」这个身份信息。把包内文件用 python pkg/filters/echo.py 直接当脚本跑时,__name__ 是 __main__、__package__ 为空,相对导入立刻抛 ImportError: attempted relative import with no known parent package。想跑包里的小程序,请用 python -m pkg.filters.echo。

本章小结​

  • 模块就是一个 .py 文件,有独立命名空间和 __name__;import m 绑定模块对象,from m import f 只绑定 f,as 可改名,import * 正式代码里别用。
  • 模块在 sys.modules 里只执行一次,改代码要重启解释器或 importlib.reload();if __name__ == '__main__': 让文件既能被导入又能当脚本跑。
  • 导入按 sys.path 顺序找:脚本目录、PYTHONPATH、标准库与 site-packages;别用标准库名给自己的文件命名,否则会遮蔽。
  • __pycache__ 里的 .pyc 只是字节码缓存,Python 仍是解释执行;dir() 用来查模块或对象定义了哪些名字。
  • 包是带 __init__.py 的目录,__all__ 决定 import * 的公开清单,相对导入用 . / .. 定位,但直接把包内文件当脚本跑会失效,要用 python -m。