一、为什么要「拆模块」
写到几十行、几百行之后,一个 .py 文件会越来越长。所有代码挤在一起,有三个很现实的麻烦:
- 找不到东西。想改「算平均分」那段,得从头往下翻半天;
- 不能复用。下个项目还要用同样的「算平均分」,只能复制粘贴,改一处要改两处;
- 没法分工。两个人同时改一个文件,很容易互相覆盖。
模块(module)解决的正是这件事:把相关的功能放进一个独立文件,需要时导入进来。包(package)则是把一堆相关的模块装进一个文件夹,再往上套一层。
其实你已经一直在用模块了——import math 里的 math 就是一个模块,只不过那是别人写好的。本篇讲的是自己拆、自己建、自己导。
| 名字 | 是什么 | 例子 |
|---|---|---|
| 模块 module | 一个 .py 文件 | math、你的 mytools.py |
| 包 package | 一个装着模块的文件夹 | numpy、你的 myshop/ |
| 函数 function | 文件里的一段可复用代码 | math.sqrt() |
第 11 篇《导入库的方法》讲的是「怎么把别人的东西拿来用」,本篇往前一步,讲的是「怎么把自己的东西拆好、放好、再用起来」。
课程思政|诚信与职业操守 · 分工,不是各扫门前雪
把功能拆进不同模块,好处之一是两个人可以各写各的、互不覆盖。但拆开只是开头——接口怎么定、变量叫什么,都要事先说好。协作的底线是不给别人添乱:你改的这一处,别人还要接着用。将来进团队、进项目组,先守约定、再谈效率,这份分寸感就是职业操守。
二、模块就是一个 .py 文件
这是最容易被想复杂的一点:模块没有任何特殊格式,就是一个普通的 .py 文件。
下面这个文件叫 mytools.py,内容里只有两个函数和一个变量:
# 文件:mytools.py
def add(a, b):
return a + b
def average(nums):
return sum(nums) / len(nums)
PI2 = 6.28
就这么简单。它现在既是「能直接运行的程序」,也是「能被别的文件导入的模块」。保存到你的项目文件夹里,模块就诞生了。
接下来在同一个文件夹里新建另一个文件,比如 main.py,写上:
# 示意:文件 main.py(和 mytools.py 放在同一个文件夹里才能跑)
import mytools
print(mytools.add(2, 3))
print(mytools.average([12, 15, 9])) # 三杯奶茶的价格
print(mytools.PI2)
运行 main.py,会看到:
5
12.0
6.28
.py。mytools.py 就写 import mytools,不要写成 import mytools.py。这是新手最常犯的错之一。为了让下面每一段代码你都能直接复制粘贴就跑,这里用一个临时代替:先用几行代码把 mytools.py 造在一个临时文件夹里,再导入它。真实项目里不需要这一堆准备代码——把文件放好、直接 import 就行。
import pathlib, sys, tempfile
work = pathlib.Path(tempfile.mkdtemp()) # 造一个临时文件夹
(work / 'mytools.py').write_text('''
def add(a, b):
return a + b
def average(nums):
return sum(nums) / len(nums)
PI2 = 6.28
''', encoding='utf-8')
sys.path.insert(0, str(work)) # 告诉 Python 去这个文件夹找模块
import mytools # 导入自己写的模块,写法和导入 math 一样
print(mytools.add(2, 3))
print(mytools.average([12, 15, 9])) # 三杯奶茶的价格
print(mytools.PI2)
输出:
5
12.0
6.28
注意第四行那几个单引号——它在造文件内容。真实的项目里你是在编辑器里手敲这个文件,用不着这行。
2.1 找不到模块会怎样
如果 mytools.py 没放在能被找到的地方(比如放错文件夹),导入就会失败:
import mytools
运行后会报错(逐字照抄解释器):
ModuleNotFoundError: No module named 'mytools'
ModuleNotFoundError 是导入环节最常见的一个错。它的意思很直白:「我按规矩找了一圈,没找到叫这个名字的模块」。第七节会讲它到底去哪儿找了。
三、导入的写法:四种,各有场合
第 11 篇《导入库的方法》里已经讲过四种写法。这里用标准库再快速过一遍,重点放在什么时候用哪种。为了让你能直接跑,下面都用 math 当例子——换成你自己写的模块,写法一模一样。
3.1 import 模块名:最稳,也最清楚
import math
print(math.sqrt(16))
print(math.pi)
输出:
4.0
3.141592653589793
用时前面要带 math.。啰嗦一点,但一眼就能看出这个函数是从哪来的——文件长了以后这个优点很重要。
3.2 from 模块名 import 名字:只拿要用的
from math import sqrt, pi
print(sqrt(16))
print(pi)
输出:
4.0
3.141592653589793
用的时候不用写前缀,代码更短。代价是看不出它来自哪个模块,而且名字可能和你自己定义的撞车。
3.3 import 模块名 as 别名:名字太长时的救星
import math as m
print(m.ceil(3.2))
print(m.floor(3.8))
输出:
4
3
3.4 from 模块名 import *:别用
from math import * 会把模块里所有公开名字一股脑倒进你的文件,包括你不认识的。后果是:你自己的变量被悄悄覆盖、dir() 列出来几百个名字、出错时根本查不出这个名字哪儿来的。养成习惯:一律不写星号导入。| 写法 | 什么时候用 | 要不要写前缀 |
|---|---|---|
import math | 默认就用这个,最不容易乱 | 要,math.sqrt() |
from math import sqrt | 只用一个函数,且名字不会撞 | 不要,sqrt() |
import math as m | 模块名很长(如 import numpy as np) | 要,但短,m.sqrt() |
from math import * | 任何情况下都不建议 | 不要,且会污染命名 |
3.5 自己写的模块也是这么导
把上面的 math 换成你的模块名,其他一个字都不用改:
import mytools→ 用mytools.add(2, 3);from mytools import add, average→ 直接用add(2, 3);import mytools as mt→ 用mt.add(2, 3)。
四、看看模块里有什么:dir() 与 help()
拿到一个不认识的模块(不管别人写的还是自己写的),先问两个问题:它有什么?每个东西怎么用? 两个内置工具就能回答。
4.1 dir(模块):列出它的全部名字
import math
print(dir(math)[:6]) # 只印前 6 个,不然太长
print('一共', len(dir(math)), '个名字')
输出:
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos']
一共 67 个名字
列表里那些带双下划线的(__name__、__doc__)是 Python 自动加的,先忽略;剩下没下划线的是真正给你用的函数和常量。
4.2 help(模块):看说明书
help(math) 会打印一整页说明,在 IDLE 或终端里翻着看最方便。它读的就是模块里的文档字符串——这也是为什么第五节要讲「给自己的模块写说明」。
import math
print(math.__doc__.strip().splitlines()[0])
输出:
This module provides access to the mathematical functions
4.3 三个自动带上的属性
import math, json
print('math 叫什么:', math.__name__)
print('math 有 __file__ 吗:', hasattr(math, '__file__'))
print('json 叫什么:', json.__name__)
print('json 从哪来:', json.__file__.split('/')[-1])
print('json 有说明吗:', bool(json.__doc__))
输出:
math 叫什么: math
math 有 __file__ 吗: False
json 叫什么: json
json 从哪来: __init__.py
json 有说明吗: True
注意 math 那一行是 False——它没有 __file__。原因是 math 属于内置模块,直接编进了 Python 解释器里,没有单独的 .py 文件;而 json 是用 Python 写的,实实在在地躺在一个文件里。
| 属性 | 含义 | 举例 |
|---|---|---|
模块.__name__ | 模块的名字 | 'json' |
模块.__file__ | 模块文件放在哪儿(内置模块没有) | …/json/__init__.py |
模块.__doc__ | 模块开头的说明文字 | help() 显示的就是它 |
dir(mytools) 会列出你的函数,mytools.__file__ 会告诉你那个文件到底在哪。「找不到自己写的模块」时,先印一下 __file__,十有八九是文件位置不对。五、__name__ 是什么、if __name__ == '__main__' 有什么用
上一节看到 math.__name__ 是字符串 'math'。那么正在运行的那个文件,它的 __name__ 是什么?
print('当前文件的 __name__ 是:', __name__)
import math
print('被导入的 math,__name__ 是:', math.__name__)
输出:
当前文件的 __name__ 是: __main__
被导入的 math,__name__ 是: math
结论:直接被运行的那个文件,__name__ 是 '__main__';被导入的模块,__name__ 是自己模块名。
这个区别有什么用?看下面这个文件:
# 文件:greet.py
def hello(name):
print('你好,' + name)
if __name__ == '__main__':
print('这一行只在本文件被直接运行时才出现')
hello('同学')
把它跑两遍,分别是「当模块导入」和「直接运行」,看看差别:
import pathlib, sys, tempfile, runpy
work = pathlib.Path(tempfile.mkdtemp())
f = work / 'greet.py'
f.write_text('''
def hello(name):
print('你好,' + name)
if __name__ == '__main__':
print('这一行只在本文件被直接运行时才出现')
hello('同学')
''', encoding='utf-8')
sys.path.insert(0, str(work))
print('--- 情况一:被别的文件 import 时 ---')
import greet
greet.hello('同学')
print('--- 情况二:自己直接运行时 ---')
runpy.run_path(str(f), run_name='__main__')
输出:
--- 情况一:被别的文件 import 时 ---
你好,同学
--- 情况二:自己直接运行时 ---
这一行只在本文件被直接运行时才出现
你好,同学
差别就在这里:导入时那句 print 没出现,因为它被 if 挡住了。
| 运行方式 | 此时 __name__ | if 里面会执行吗 |
|---|---|---|
python greet.py(直接运行) | '__main__' | 会 |
import greet(被别人导入) | 'greet' | 不会 |
if __name__ == '__main__':,把「测试用的打印」和「演示用的调用」写在里面。好处是:别人导入你的模块时,不会有莫名其妙的输出冒出来。AI 助手小贴士|两种 import 的差别在「谁进了当前文件的名字空间」。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:import my_utils 和 from my_utils import fmt_price 有什么区别?各举一个会发生命名冲突的例子
课程思政|工匠精神与严谨求实 · 别打扰用你代码的人
别人 import 你的模块,是为了用里面的函数,不是想看你在自测时随手打印的东西。把测试代码放进 if __name__ == '__main__': 里,导入时它就安静地待着。写代码时多想一步使用者的感受,这个小小的判断里,藏着的正是替别人着想的工匠心。
六、包:把模块装进文件夹
模块多了怎么办?按用途分类,放进文件夹——这个文件夹就是包(package)。
一个包的样子长这样:
myshop/
├── __init__.py ← 这个文件的「有无」决定它是不是包
├── goods.py ← 模块:饮品
└── orders.py ← 模块:订单
注意那个 __init__.py:它是包的门牌。有个约定要记住——
| Python 版本 | __init__.py 是必须的吗 | 说明 |
|---|---|---|
| Python 3(现代用法) | 不是必须 | 没有它也能当包用(叫「命名空间包」) |
| 老版本 / 老教程 | 必须 | 很多教程还这么写,看到别奇怪 |
| 建议 | 还是写上 | 内容可以是空的,也可以放包的说明;有它更明确 |
用起来是这样的:
# 示意:文件 main.py(需先按上面的结构建好 myshop 包)
from myshop import goods # 方式一:从包里导入某个模块
import myshop.orders # 方式二:导入包里的模块,用全名调
print(goods.price('珍珠奶茶'))
print(myshop.orders.count())
下面这段把「建包 → 导入 → 用」整个流程真跑一遍:
import pathlib, sys, tempfile
work = pathlib.Path(tempfile.mkdtemp())
pkg = work / 'myshop'
pkg.mkdir()
(pkg / '__init__.py').write_text('# 包的门牌,内容可以为空\n', encoding='utf-8')
(pkg / 'goods.py').write_text(
'def price(name):\n'
' return {"珍珠奶茶": 12, "柠檬茶": 8}[name]\n',
encoding='utf-8')
(pkg / 'orders.py').write_text(
'def count():\n'
' return 3\n',
encoding='utf-8')
sys.path.insert(0, str(work))
from myshop import goods
import myshop.orders
print(goods.price('珍珠奶茶'))
print(myshop.orders.count())
输出:
12
3
6.1 绝对导入与相对导入
在包里面的模块之间互相导入时,有两种写法:
| 写法 | 长什么样 | 什么时候用 |
|---|---|---|
| 绝对导入 | from myshop import goods | 推荐,从顶层包名写起,清楚 |
| 相对导入 | from . import goods | 只在包内部用,. 表示当前包 |
| 相对导入(上一级) | from .. import other | 少用,层级深了容易绕晕 |
python orders.py 运行包里的某个文件,那句 from . import goods 会报 ImportError: attempted relative import with no known parent package。起步阶段配一条省心规则:包内部的一律写绝对导入,不碰相对导入。
七、Python 去哪儿找模块:sys.path
写 import mytools 时,Python 会按一个固定顺序去一串文件夹里翻。这串文件夹列在 sys.path 里:
import sys
print('搜索清单第 1 项:', repr(sys.path[0]))
print('清单里一共几处:', len(sys.path))
输出:
搜索清单第 1 项: ''
清单里一共几处: 6
sys.path[0] 那一项是空字符串 '',表示「当前正在运行的那个脚本所在的文件夹」。这就是为什么「把两个文件放在同一个文件夹」就能直接导入——第一个被翻的就是那里。
'' 只在交互模式(或 python -c)里出现;把这两行存成 find.py 再运行,第 1 项打印的就是 find.py 所在文件夹的路径。「清单里一共几处」也因电脑和装的库而异,不用对数。完整顺序大致是:
| 顺序 | 找哪里 | 说明 |
|---|---|---|
| 1 | 脚本所在文件夹 | 最常见的情况,也是你放自己的模块最省事的位置 |
| 2 | 标准库目录 | 装 Python 时自带的,math、os 都在这儿 |
| 3 | 第三方库目录 | 用 pip install 装进来的库,如 numpy |
7.1 找不到时怎么办
先别急着改代码,按这个顺序排查:
- 确认文件名没写错:
import mytool找不到mytools.py,就是差一个字母; - 确认在同一个文件夹:脚本旁边有没有那个
.py; - 确认没写
.py:import mytools.py是错的写法; - 确认文件名不叫
math.py这类标准库名:同名会「遮住」标准库,非常难查; - 实在不在同一层,才考虑
sys.path.append()临时加路径(写法见下面)。
临时加一个搜索位置,可以这样(只对本次运行有效):
import sys
before = len(sys.path)
sys.path.append('/tmp')
print('加之前有几处:', before)
print('加之后有几处:', len(sys.path))
print('最后一项是:', sys.path[-1])
输出:
加之前有几处: 6
加之后有几处: 7
最后一项是: /tmp
sys.path 是排查工具,不是首选方案——很多新手学会了 sys.path.append 之后到处乱加,反而搞出更难查的问题。八、虚拟环境:给项目一个独立的小房间
pip install 装的库,默认落在整个电脑共用的那一处。这会带来两个真实麻烦:
- A 项目要
pandas 1.x,B 项目要pandas 2.x——全局只能留一个版本,必有一个跑不起来; - 装了几十个库之后,你根本记不清哪个项目用了哪些,换台电脑就装不回同样的环境。
虚拟环境(virtual environment)就是给每个项目单独开一个小房间,房间里的库只属于这个项目。
8.1 三步走
# 第一步:在项目文件夹里创建虚拟环境(文件夹名通常叫 venv)
python -m venv venv
# 第二步:激活它(Windows)
venv\Scripts\activate
# 第二步:激活它(macOS / Linux)
source venv/bin/activate
# 第三步:这时再装库,就只装在这个项目里
pip install requests
激活成功的标志很清楚:命令行提示符前面多了一对括号,写着环境名,像 (venv)。
8.2 那三个高频命令
pip list # 看看这个环境里已经装了什么
pip freeze > requirements.txt # 把当前清单导出成一个文件
pip install -r requirements.txt # 换台电脑时,按清单一次装回来
requirements.txt 是项目交接的关键:别人拿到你的项目,一条命令就能装出一样的环境。
| 命令 | 作用 | 什么时候用 |
|---|---|---|
python -m venv venv | 新建虚拟环境 | 每个新项目开工时 |
venv\Scripts\activate | 激活(Windows) | 每次打开终端要开工时 |
source venv/bin/activate | 激活(macOS / Linux) | 每次打开终端要开工时 |
deactivate | 退出虚拟环境 | 切到别的项目时 |
pip install 包名 | 装库 | 需要新库时 |
pip freeze > requirements.txt | 导出清单 | 项目要交给别人 / 备份时 |
pip install -r requirements.txt | 按清单装 | 拿到别人的项目时 |
venv/)不要拷给别人、也不要用 git 提交。它体积大且和本机绑定,换台电脑就失效。要交付的是 requirements.txt,别人自己按清单重建。8.3 让 TRAE 帮你打理虚拟环境
8.1 和 8.2 的那些命令不需要背——TRAE 的侧边 AI 对话框可以替你敲。按 Ctrl + U 打开(Mac 上是 Cmd + U),这个对话框的完整介绍见《4-Trae》。
新建并激活:刚建好项目文件夹,在对话框里说一句——
「在当前项目文件夹里新建一个名叫 venv 的虚拟环境,激活它,再装上 requests。」
AI 会把这句话拆成 8.1 的那几步,在底部终端里逐条执行:python -m venv venv、激活命令、pip install requests。完成后可以再补一句「跑一下 pip list」,看到 requests 出现在清单里,就说明库确实装进了这个项目自己的小房间。
更新已有环境:项目做了一半,环境要动,也照样一句话——
「按 requirements.txt 把虚拟环境的依赖更新一遍」「把 pandas 升级到最新版」,对应的正是 8.2 里的 pip install -r requirements.txt 和升级命令。换电脑拿到别人的项目时,一句「先建虚拟环境,再按 requirements.txt 装齐依赖」就能把整套环境复原出来。
想让 AI 做得又快又准,说话时记住三点:
- 说清楚位置和名字:在哪个文件夹建、环境叫什么名(用默认的 venv 就不用特意说);
- 一句话说一件事:新建、激活、装库、升级、按清单重装,分开说比揉在一起更不容易出错;
- 拿不准先看现状:让它先跑
pip list汇报当前装了什么,再决定下一步。
课程思政|数据安全与法治素养 · 各归各的房间
虚拟环境给每个项目单独开一间房,库只装在自己屋里,不跟别的项目混在一起。边界清楚,才不会互相污染。把依赖写进 requirements.txt,交接时别人一条命令就能装出一样的环境——这既是工程规矩,也是一种数据素养:谁的数据、放在哪儿、谁有权动,心里都要有一道清楚的线。
九、写模块的几个好习惯
9.1 一个模块只干一类事
scores.py 就放和成绩有关的函数,files.py 就放和文件有关的。名字起得越具体,以后越好找。
9.2 给模块和函数写说明
在文件最上面写一段说明,它会自动成为 help(你的模块) 显示的内容。函数里第一行也可以写:
# 文件:scores.py
'''成绩相关的工具函数。'''
def average(nums):
'''算平均分:把一组分数的和除以个数。'''
return sum(nums) / len(nums)
9.3 用 if __name__ == '__main__': 隔开「自测」
第五节讲过。把「随手试一下」的代码放进这个分支,导入你模块的人就不会被莫名其妙的输出打扰。
9.4 别用标准库的名字
csv.py、random.py、string.py、json.py、datetime.py 这类名字,会遮住同名的标准库。表现是:import csv 之后 csv.reader 不见了,报 AttributeError,而且换个文件夹又好了——极难排查。避开办法很简单:自己的文件名加个前缀或用途词,比如
my_csv_tools.py、data_io.py。9.5 别在两个模块里互相导入
# a.py 里写了 from b import func_b
# b.py 里又写了 from a import func_a ← 这就是「循环导入」
# 结果是导入报错,或者某个名字变成 None,问题很难定位
真需要共享的东西,抽到第三个模块(比如 common.py)里,两边都去导入它。
AI 助手小贴士|拆模块的目录结构有最佳实践,让 AI 给个起点。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:我要把课程项目拆成几个 .py 文件,请给一个目录结构建议:工具函数放哪、主流程放哪、什么时候需要 __init__.py
十、常见坑
坑 1:写成了 import mytools.py
import pathlib, sys, tempfile
work = pathlib.Path(tempfile.mkdtemp())
(work / 'mytools.py').write_text('def add(a, b): return a + b\n', encoding='utf-8')
sys.path.insert(0, str(work)) # 文件明明就在这儿,还是导不进来
import mytools.py
运行后会报错(逐字照抄解释器):
ModuleNotFoundError: No module named 'mytools.py'; 'mytools' is not a package
报错信息里那句话很值得读一遍:'mytools' is not a package——Python 把 mytools.py 理解成了「包 mytools 下面的模块 py」,于是去找一个根本不存在的包。
处理办法:导入写文件名去掉后缀,import mytools。
坑 2:自己的文件名和标准库撞名
假设你手边有个自己的文件叫 csv.py。Python 找模块时先翻你的文件夹,于是 import csv 拿到的是你的那个,标准库的 csv 被挡住了:
import pathlib, sys, tempfile
work = pathlib.Path(tempfile.mkdtemp())
(work / 'csv.py').write_text('MARK = 1\n', encoding='utf-8')
sys.path.insert(0, str(work))
import csv
print('导入到的 csv 是:', csv.__file__.split('/')[-1])
print('它有 reader 吗:', hasattr(csv, 'reader'))
输出:
导入到的 csv 是: csv.py
它有 reader 吗: False
接着用它,就会冒出这种「标准库明明有、我这里却没有」的报错:
import pathlib, sys, tempfile
work = pathlib.Path(tempfile.mkdtemp())
(work / 'csv.py').write_text('MARK = 1\n', encoding='utf-8')
sys.path.insert(0, str(work))
import csv
print(csv.reader(['a,b']))
运行后会报错(逐字照抄解释器):
AttributeError: module 'csv' has no attribute 'reader'
处理办法:给自己的文件换个名字(my_scores.py、data_io.py),别用 csv、random、string、json、datetime 这些标准库名。
math 这类内置模块是直接编进解释器的,同名文件挡不住它。但像 csv、random、string 这些从文件加载的模块就会被挡住。不用去记哪些挡得住——统一避开标准库名就对了。坑 3:模块不在同一个文件夹
import mytools
运行后会报错(逐字照抄解释器):
ModuleNotFoundError: No module named 'mytools'
处理办法:先确认 .py 和脚本在同一个文件夹;确实要跨层,再用第六节的办法处理路径。
坑 4:导入整个模块时,模块里的顶层代码会执行一遍
import pathlib, sys, tempfile
work = pathlib.Path(tempfile.mkdtemp())
(work / 'noisy.py').write_text('print("我被导入了!")\n', encoding='utf-8')
sys.path.insert(0, str(work))
print('导入之前')
import noisy
print('导入之后')
输出:
导入之前
我被导入了!
导入之后
处理办法:模块顶层只放「定义」(def / class / 常量),把会执行、会打印的代码放进 if __name__ == '__main__': 里。
坑 5:改了模块代码,但运行结果没变
Python 导入一个模块后会把它缓存起来,同一次运行里再导入不会重新读文件。在 IDLE 里改完 mytools.py 却看不到变化,就是这个原因。处理办法:重新运行一次程序(多数编辑器里是重新点运行),而不是在同一个会话里反复 import。
坑 6:相对导入只能在包内部用
from . import something
运行后会报错(逐字照抄解释器):
ImportError: attempted relative import with no known parent package
处理办法:别直接运行包里的单个文件;或者在包内部统一改成绝对导入(第六节 6.1)。
十一、报错手册
报错原文都是从本机解释器逐字抄下来的,对照着找就行。
| 报错原文(最后一行) | 什么情况 | 怎么改 |
|---|---|---|
ModuleNotFoundError: No module named 'mytools' | 找不到这个模块:文件名错、不在同一文件夹、或没装 | 核对文件名与位置;第三方库先 pip install |
ModuleNotFoundError: No module named 'mytools.py'; 'mytools' is not a package | 写成 import mytools.py 了 | 去掉 .py,写 import mytools |
ImportError: attempted relative import with no known parent package | 在包外面(或直接运行包内文件)用了相对导入 | 改成绝对导入,或从包的外层运行 |
ImportError: cannot import name 'add' from 'mytools' | 模块找到了,但里面没有这个名字 | 检查名字拼写;确认函数真的定义在模块里 |
AttributeError: module 'csv' has no attribute 'reader' | 标准库被同名的自己文件遮住了 | 给自己的文件改名,别用标准库名 |
SyntaxError: invalid syntax(出现在 import 行) | 漏了逗号、括号,或路径写法不对 | 对照本节前面的正确写法逐字核 |
下面这几条是「不报错,但结果不对」的,比报错更值得当心:
| 现象 | 原因 | 怎么改 |
|---|---|---|
| 导入时冒出一堆打印 | 模块顶层有会执行的代码 | 挪进 if __name__ == '__main__': |
| 改了模块,运行结果还是老样子 | 模块被缓存了 | 重新运行整个程序 |
from mytools import * 之后名字乱撞 | 星号导入倒进了太多名字 | 改成 import mytools 或点名导入 |
| 自己定义的变量把标准库函数覆盖了 | import * 或重合命名 | 去掉星号导入,变量别起成 list、sum |
十二、常见问题
Q:模块和包到底怎么区分?
模块是一个 .py 文件;包是一个装着模块的文件夹。用起来都是 import,区别只在「导入的名词前面有没有文件夹那一层」,比如 import goods 是模块,from myshop import goods 里的 myshop 是包。
Q:自己的模块要不要加 __init__.py?
现代 Python 不加也能当包用。但建议加上(可以是空文件),一是更明确,二是很多工具和老教程按这个约定来。
Q:if __name__ == '__main__': 一定要写吗?
不是「一定」,但写上是好习惯。判断标准很简单:这个文件会不会被别人导入? 会,就写;如果它只是个一次性的小脚本,不写也无所谓。
Q:虚拟环境非用不可吗?
只装一两个库、只做练习时可以不用。但只要开始「同时做两个项目」或者「要交给别人」,就该用——否则版本冲突迟早找上门。
Q:为什么我 import 自己的模块总是报错,用别人的库就没事?
因为第三方库装在了 Python 的搜索目录里,而你的文件在哪儿,Python 并不知道。记住那条最省事的规则:把模块和用它的脚本放进同一个文件夹。
Q:一个项目里文件怎么组织比较好?
起步阶段的够用版本:项目文件夹里放「入口脚本」(如 main.py)+ 几个功能模块(scores.py、files.py)+ 一个放数据的文件夹 + 一个 venv/。功能变多了,再把同类模块收进一个包。
AI 助手小贴士|模块、包、库三个词总被混着用,让 AI 辨析清楚;pip 的报错也能直接贴给它(虚拟环境的打理见第八节 8.3)。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:初学者常混淆 Python 的模块、包、库这三个词。请用文件夹类比讲清它们的关系,再说明 pip、import、requirements.txt 各负责哪一环
十三、随堂自测(12 题)
点击选项即可判分,答错的题会立刻显示解析。
1自己写了一个 mytools.py,正确的导入写法是?
- A.
import mytools.py as mytools - B.
import mytools.py - C.
import mytools - D.
from mytools.py import add
.py。写成 import mytools.py 会报 ModuleNotFoundError: No module named 'mytools.py',因为 Python 把 mytools.py 当成「包 mytools 下面的 py」,而不是文件名。2自己的模块写好了,但一导入就报 ModuleNotFoundError,最先该检查的是?
- A. Python 是不是装错了
- B. 文件是不是和脚本放在同一个文件夹、名字有没有拼错
- C. 电脑要不要重启
- D. 是不是该用
from而不是import
.py → 是否和标准库撞名。3关于 if __name__ == '__main__':,下面说法正确的是?
- A. 这个文件被直接运行时,
__name__的值是'__main__' - B. 这个文件被 import 时,
if里面的代码也会执行 - C.
__name__只能在主文件里用 - D. 写不写对程序没有任何影响
__name__ 是 '__main__';被导入时是模块自己的名字(比如 'greet')。所以 if 里的代码只有在直接运行时才执行——这正是用它隔开「自测代码」的原理。4下面哪种导入写法被建议不要用?
- A.
import math - B.
from math import sqrt - C.
import math as m - D.
from math import *
dir() 变得无法阅读。前三种写法各有场合:import 模块名 最不容易乱,from ... import 短但要小心重名,as 适合名字很长的库。5文件夹 myshop/ 里有一个 goods.py,想用里面的 price() 函数,写法正确的是?
- A.
import myshop/goods - B.
from myshop import goods,然后goods.price() - C.
from myshop.goods import *,这是唯一正确写法 - D.
import myshop-goods
from myshop import goods 之后写 goods.price();也可以 import myshop.goods,之后写 myshop.goods.price()。斜杠、连字符都不是 Python 的写法。6包里的模块想导入同包的另一个模块,下面哪种写法被推荐?
- A. 绝对导入
from myshop import goods - B. 相对导入
from . import goods,且只能这么写 - C. 写文件路径
from myshop/goods import price - D. 用
include语句
from . import goods 虽然合法,但它有个硬规矩:只能在该模块被当作包的一部分导入时使用——直接运行包里的单个文件就会报 ImportError: attempted relative import with no known parent package。7dir(math) 能告诉你什么?
- A. math 模块里一共有多少个函数被调用过
- B. math 模块装在哪台电脑上
- C. math 模块里可用的名字有哪些
- D. math 模块每个函数怎么用
dir() 列出模块里的全部名字(函数、常量、类型),带双下划线的可以先忽略。想看「每个东西怎么用」要用 help(),它读的是模块里的文档字符串。8在包内部写 from . import goods,如果直接运行这个包内的文件,会怎样?
- A. 完全正常,和导入时一样
- B. 只会警告一下,继续运行
- C. 自动改成绝对导入
- D. 报
ImportError: attempted relative import with no known parent package
. 指的是谁。直接运行包内的单个文件时没有这个上下文,就报错。处理办法:在包内部统一改成绝对导入,或者不要直接运行包内的文件。9下面哪个文件名最容易被建议改掉?
- A.
math.py - B.
my_scores.py - C.
score_utils.py - D.
class_report.py
math.py 和标准库同名,会把它「遮住」——之后 import math 拿到的是你的文件,math.sqrt() 就消失了,报 AttributeError。同类要避开的还有 random.py、json.py、csv.py、string.py、datetime.py。10虚拟环境(venv)主要解决什么问题?
- A. 让 Python 跑得更快
- B. 让不同项目用各自的库版本,互不干扰
- C. 自动帮你写代码注释
- D. 把代码打包成 exe
pip freeze > requirements.txt 导出的清单,不是 venv/ 文件夹本身。11改完 mytools.py 里的代码,重新导入却没变化,最可能的原因是?
- A. 必须把模块放进包里才能生效
- B. Python 坏了,要重装
- C. 文件名写错了
- D. 模块已经被缓存,需要重新运行整个程序
sys.modules 里,同一次运行中再 import 不会重读文件。正确做法是重新运行整个程序,而不是在同一个会话里反复写 import。12一个文件既能被直接运行、又能被别人导入,下面哪个做法最合理?
- A. 把测试用的打印和演示调用直接写在文件末尾
- B. 禁止别人导入这个文件
- C. 把「定义」放在顶层,把「演示/测试」放进
if __name__ == '__main__':里 - D. 把演示代码写进
__init__.py
def / class / 常量,别人导入时不会有输出冒出来;演示和测试放进 if __name__ == '__main__':,只有自己直接运行时才执行。这是模块「既能当工具、又能当脚本」的标准写法。