1 模块的本质 —— .py 文件就是模块
是什么?为什么学?
模块(module)就是一个 .py 文件,文件里写的函数、变量、类都可以被其他文件导入使用。它是 Python 组织代码的最小单位:复用(写一次到处用)、隔离(每个文件只管一件事)、协作(多人各写各的文件)。到昨天为止,所有代码都是「一个文件从头写到尾」;真实项目动辄几千上万行,全堆在一个文件里没法维护——模块就是把代码按功能拆成一个个文件,需要时导入使用。
底层原理
import math 背后做了三件事:① 找到 math.py 文件(按 sys.path 里的目录顺序查找);② 执行 整个文件(模块顶层的代码会真实运行一遍);③ 把模块对象绑定到名字 math 上。之后 math.sqrt 就是「从 math 这个模块对象里取 sqrt 属性」。每个模块只被完整执行一次,之后都从 sys.modules 缓存里直接取——所以导入多次不会重复执行。
生活类比
模块像零件仓库里的货架:每个货架(.py 文件)上整整齐齐摆着一类零件(函数、变量),货架上有标签(模块名)。你想用「平方根」这个零件,就去 math 货架取:math.sqrt。零件放仓库里,谁需要谁来取,不用每个车间(文件)都自己造一遍。
注意事项总结
1 模块是一个对象,用「模块名.名字」取里面的东西:
import math
print(type(math)) # 模块的类型是 module
print(math.sqrt(81)) # 从 math 里取 sqrt 函数来用
print(math.pi) # 从 math 里取变量 pi
运行结果:
<class 'module'>
9.0
3.141592653589793
模块里不仅有函数,还有 pi 这样的常量变量——它们统一叫模块的「名字」(name)。拿到不熟悉的模块时,可以用 dir(模块) 列出全部名字、用 hasattr(模块, "名字") 验证是否存在——这两个是「探索模块」的万能钥匙。
2 每个模块只执行一次——导入多次不会重复加载:
import sys
import math
print("第一次导入后:", "math" in sys.modules)
import math # 再导入一次,不会重新执行 math 文件
print("math 还是同一个对象:", sys.modules["math"] is math)
运行结果:
第一次导入后: True
math 还是同一个对象: True
sys.modules 是「已导入模块的缓存字典」。任何地方再 import math,Python 直接从缓存取,不会重新执行——模块导入是「单例」的。
易错点
- 错误写法:
import mathh(拼写错误) → 问题:报ModuleNotFoundError: No module named 'mathh'→ 正确写法:检查模块名拼写。 - 错误写法:只
import math却直接写sqrt(16)→ 问题:报NameError,sqrt 不在当前命名空间 → 正确写法:写math.sqrt(16)(带模块名前缀)。 - 错误写法:自己建了个叫
math.py的文件 → 问题:你的文件会「顶掉」标准库 math,行为全乱 → 正确写法:自定义文件不要用标准库模块名命名。
记忆口诀:模块就是 .py,import 三连:找到、执行、绑名字;只跑一次进缓存,模块名前缀别忘写。
2 import 的两种姿势 —— 整模块导入 vs 精准导入
是什么?为什么学?
import math 导入整个模块(要用 math. 前缀);from math import sqrt 只把某个名字「复制」进当前文件(直接用)。加上 as 别名可以给长名字起短名。三种姿势各有适用场景。
底层原理
import math 是把模块对象绑定到 math 这个名字;from math import sqrt 则是「从 math 对象里取出 sqrt 属性,绑定到当前模块的 sqrt 名字」——相当于一次「复制引用」。所以之后直接用 sqrt 就行,但 math 这个名字本身没有绑定,写 math.sqrt 会 NameError。import math as m 只是把模块对象绑定到名字 m 上(as = alias,别名)。要注意:from 导入如果和你已有的变量同名,会覆盖你的变量——这是它最大的坑。
生活类比
两种姿势像超市购物:import math 是把整个超市搬回家,家里多了「math 超市」,拿东西要写「去 math 超市拿 sqrt」;from math import sqrt 是只买走 sqrt 这一个商品,回家直接用,但家里并没有「math 超市」这个名字。as 则像给买回来的东西贴个自己的标签。
注意事项总结
1 from 导入会覆盖同名变量(重要坑!):
sqrt = 100 # 1. 自己先定义了一个变量 sqrt
from math import sqrt # 2. from 导入把 sqrt 覆盖成了 math 的函数
print(sqrt) # 现在 sqrt 是函数,不再是 100
print(sqrt(16)) # 可以当函数用
运行结果:
<built-in function sqrt>
4.0
from math import sqrt 把 sqrt 这个名字重新绑定到 math.sqrt 函数上,原来的 100 被覆盖丢失。这解释了为什么团队规范推荐多用 import math 形式——前缀能避免这种「名字被悄悄覆盖」的意外。
2 as 别名——给名字起短名:
import random as r # 模块起别名
from math import sqrt as s # 函数起别名
print(s(144)) # 用别名调用
print(r.randint(1, 3)) # 1~3 随机整数(每次运行可能不同)
运行结果:
12.0
2(每次运行结果可能不同)
长模块名(如 import numpy as np)或重名冲突时特别好用。
易错点
- 错误写法:
from math import sqrt后又写math.sqrt(9)→ 问题:报NameError: name 'math' is not defined→ 正确写法:用 from 导入就用裸名字;想写 math. 前缀就用 import math。 - 错误写法:
from math import *→ 问题:把 math 所有名字塞进来,可能覆盖你的变量,团队规范禁止 → 正确写法:明确导入需要的名字:from math import sqrt, pi。
记忆口诀:import 整模块,前缀来访问;from 精准拿,裸名直接用;as 起别名,覆盖要提防。
3 标准库实战 —— math / random / os / sys 四大金刚
是什么?为什么学?
标准库是 Python 自带的「百宝箱」,装了几百个现成模块。今天先认识最常用的四个:math(数学计算)、random(随机数)、os(操作系统)、sys(系统参数)。学会用标准库,是「不重复造轮子」的第一步——你要的功能,大概率别人已经写好了。
底层原理
标准库模块都是 C 或 Python 写的、随 Python 一起安装的现成代码。它们与普通模块的导入机制完全一样:按 sys.path 查找、执行、绑定名字。math 里的函数(如 sqrt)用 C 实现,比 Python 手写快几十上百倍;random 用「梅森旋转」算法生成伪随机数(从种子出发,看起来随机但可复现);os 封装了操作系统的 API;sys 直接暴露解释器运行状态(版本、参数、路径)。
生活类比
标准库像宜家的样板间:你不需要从零设计家具,样板间里全是装好的方案——math 是计算器柜台,random 是抽奖转盘柜台,os 是物业办公室(管目录、管路径),sys 是前台服务台(告诉你本机信息)。需要什么功能,直接去对应柜台取。
注意事项总结
1 math——数学计算:
import math
print(math.sqrt(144)) # 平方根:12.0
print(math.fabs(-7)) # 绝对值:7.0
print(math.pow(2, 10)) # 2 的 10 次方:1024.0
print(math.factorial(5)) # 阶乘:120
2 random——随机数:
import random
print(random.randint(1, 6)) # 1~6 的随机整数
print(random.choice(["石头", "剪刀", "布"])) # 从列表随机选一个
print(round(random.random(), 4)) # 0~1 之间的随机小数(保留 4 位)
nums = [1, 2, 3, 4, 5]
random.shuffle(nums) # 原地打乱列表顺序
运行结果(每次运行不同,示例):
5
剪刀
0.6374
[3, 5, 1, 4, 2]
3 os——操作系统:
import os
print(os.path.join("data", "notes", "hello.txt")) # 拼接路径
print(os.path.exists("hello.txt")) # 文件是否存在
print(os.getcwd()) # 当前工作目录
运行结果(Windows 示例;getcwd 取决于运行目录):
data\notes\hello.txt
False
C:\Users\...\Temp
os.path.join 按当前系统的习惯拼接路径(Windows 用 \、Linux/macOS 用 /),跨平台不报错——文件相关的程序几乎都要用到 os。
4 sys——系统参数:
import sys
print(sys.version) # Python 版本号
print(sys.argv) # 命令行参数列表(运行脚本时传入)
运行 python test.py a b 时,sys.argv 是 ['test.py', 'a', 'b']——下标 0 是脚本本身,参数从下标 1 开始。
易错点
- 错误写法:
random.random()想要 16 的整数 → 问题:得到的是 01 的小数 → 正确写法:整数随机用random.randint(1, 6)。 - 错误写法:
math.sqrt("16")传字符串 → 问题:报TypeError,数学函数只接受数字 → 正确写法:先转数字:math.sqrt(int("16"))。
记忆口诀:math 算数,random 随机,os 管系统,sys 查自己;标准库常查文档,不造轮子省力气。
4 自定义模块与 name 魔法 —— 写自己的 .py 文件
是什么?为什么学?
自己写的 .py 文件就是自定义模块,放在同一目录即可被导入。但直接运行和被导入时,文件的行为应该不同:被导入时不该执行「测试代码」。if __name__ == "__main__": 就是区分「直接运行 / 被导入」的标准写法——每个 Python 文件的标准结尾。
底层原理
Python 执行任何文件时都会给它设置一个内置变量 __name__:直接运行时值是字符串 "__main__";被 import 导入时值是模块名(如 "my_tools")。所以 if __name__ == "__main__": 的意思是「只有当我被直接运行时才执行这段代码」。被导入时条件为假,测试代码自然跳过——同一个文件,两种身份,一套判断搞定。
生活类比
__name__ 像员工工牌上的「值班身份」:白天上班(被导入)时,胸牌写的是「部门-岗位」(模块名),只干本职工作;下班回家(直接运行)时,身份是「主人」(__main__),可以做自我检查(运行测试代码)。
注意事项总结
1 自定义模块 my_tools.py(新建这个文件):
def add(a, b):
"""两数相加"""
return a + b
def is_even(n):
"""判断 n 是否为偶数"""
return n % 2 == 0
PI = 3.14159
if __name__ == "__main__":
# 只有直接运行 my_tools.py 时才执行下面的自测
print("自测 add(1, 2):", add(1, 2))
print("自测 is_even(4):", is_even(4))
2 use_tools.py(与 my_tools.py 放同一目录)——导入并使用:
import my_tools
from my_tools import is_even, PI
print(my_tools.add(3, 5)) # 用模块名前缀访问
print(is_even(10)) # 直接用导入的名字
print(PI) # 导入的常量
运行结果:
8
True
3.14159
import my_tools 时,Python 会执行 my_tools.py 文件——但 __name__ 此时是 "my_tools" 而不是 "__main__",所以自测代码不会执行。两个文件必须放在同一个目录下。
易错点
- 错误写法:测试代码直接写在文件顶层 → 问题:被 import 时测试代码也执行,屏幕上乱打东西 → 正确写法:测试代码全部放进
if __name__ == "__main__":。 - 错误写法:
if __name__ == "main":(少了双下划线) → 问题:条件永远为假,直接运行时自测不执行 → 正确写法:写全:if __name__ == "__main__":。 - 错误写法:自定义模块没放在当前目录 → 问题:报
ModuleNotFoundError→ 正确写法:被导入文件与使用文件放同一目录。
记忆口诀:直接运行 __main__,被导模块名;测试放进口判断,一个文件两种身份。
5 包 —— 把模块装进文件夹
是什么?为什么学?
包(package)是带 __init__.py 的文件夹,用来组织多个相关模块。模块一多,全平铺在目录里会乱;包让你用「包名.模块名」分层管理,就像文件夹套文件夹。真实项目(Django、Flask 都是几十上百个模块)全靠包来组织,今天先建立概念。
底层原理
Python 把「含 __init__.py 的文件夹」当作包:导入 mypkg.tools 时,先执行 mypkg/__init__.py(可放包级初始化代码,也可为空),再执行 mypkg/tools.py。__init__.py 是包的「身份证」——有了它,文件夹才能被当成包导入。Python 3 还支持「命名空间包」(没有 init.py 也能导),但规范做法仍然是显式创建 __init__.py。点号 . 在导入语句里就是「进入下一层」的意思。
生活类比
包像办公室的文件柜:模块是柜子里的文件夹,包是带标签的柜子(标签就是 __init__.py)。找文件时先找到柜子(包名),再找文件夹(模块名):「财务柜.报销单」= mypkg.tools。
注意事项总结
1 标准库里的「包」——xml 就是一个包:
import xml
print(type(xml)) # xml 是包,本质也是模块
print("未导入时有没有 etree:", hasattr(xml, "etree")) # 子模块不会自动加载
import xml.etree.ElementTree as ET # 包名.模块名.类名,分层导入
root = ET.fromstring("<name>Python</name>")
print(root.tag, root.text)
运行结果:
<class 'module'>
未导入时有没有 etree: False
name Python
import xml 只加载包本身,子模块 etree 不会自动加载(所以 hasattr 是 False);import xml.etree.ElementTree as ET 用点号一层层进到具体模块。
2 建包的目录结构:
mypkg/ # 包:一个文件夹
__init__.py # 包的标志文件(可以是空文件)
tools.py # 模块 1
data.py # 模块 2
import mypkg.tools # 用 包名.模块名 导入
from mypkg import data # 也可以这样
from mypkg.tools import add # 或直接导入模块里的函数
易错点
- 错误写法:忘了建
__init__.py→ 问题:老版本 Python 无法导入该文件夹(3.3+ 有兼容但不符合规范) → 正确写法:每个包文件夹里都放一个__init__.py(可空)。 - 错误写法:
import mypkg.tools后直接写tools.add(1, 2)→ 问题:报NameError,tools 没绑定到当前命名空间 → 正确写法:用全名mypkg.tools.add(1, 2),或from mypkg import tools。
记忆口诀:包是文件夹,带证 __init__;点号进层级,包名点模块;子模块不自动,导入才加载。
6 动手实践:随机密码生成器
把今天学的 random、自定义模块、函数组合起来:做一个密码生成器,可以指定长度和是否包含数字、大小写、特殊符号。
# 随机密码生成器
import random
import string
def generate_password(length=12, use_digits=True, use_special=True):
"""生成长度为 length 的随机密码
参数:
length: 密码长度
use_digits: 是否包含数字
use_special: 是否包含特殊符号
"""
chars = string.ascii_letters # 所有大小写字母
if use_digits:
chars = chars + string.digits # 加上数字
if use_special:
chars = chars + string.punctuation # 加上特殊符号
password = ""
for i in range(length):
password = password + random.choice(chars)
return password
if __name__ == "__main__":
print("默认密码(12位,含数字和符号):", generate_password())
print("纯字母密码(8位):", generate_password(8, False, False))
print("含数字不含符号(16位):", generate_password(16, True, False))
print("很长的密码(20位):", generate_password(20))
运行结果(每次不同,示例):
默认密码(12位,含数字和符号): G7&kL#q2@xWp
纯字母密码(8位): QwErTyUi
含数字不含符号(16位): A1b2C3d4E5f6G7h8
很长的密码(20位): !xZ9&vB3@kL5#nP7*qW2
完成标准:运行后能看到 4 种不同规则的密码;修改 generate_password 的默认参数,再生成「只含小写字母、长度 6」的密码。想一想:string 是标准库,string.ascii_letters、string.digits、string.punctuation 分别是什么,用 print 看一看。
今日总结
模块是什么:.py 文件就是模块;import 三件事——找到、执行、绑定名字,且只执行一次(sys.modules 缓存)
import 模块 用 模块.名字 访问;from 模块 import 名字 直接使用;as 可起别名;from ... import * 要避免
from ... import ... 会覆盖同名变量,这是它最大的坑,多用 import 模块 更安全
标准库四大金刚:math 算数学、random 造随机、os 管系统、sys 查参数
自定义模块放同一目录即可导入;测试代码放 if __name__ == "__main__": 里,直接运行才执行、被导入不执行
包 = 带 __init__.py 的文件夹,用点号分层导入;子模块不会自动加载
完成随机密码生成器,并尝试改参数生成不同规则的密码
| 报错 | 原因 | 修复 |
|---|---|---|
ModuleNotFoundError: No module named 'xxx' | 模块不存在、没安装,或自定义模块不在同一目录 | 检查名字拼写;自定义模块放在当前目录;第三方库用 pip 安装 |
AttributeError: module 'xxx' has no attribute 'yyy' | 模块里没有这个函数/属性 | 检查函数名拼写;用 dir(模块) 看模块里有什么 |
ImportError: cannot import name 'xxx' | from 导入的名字在模块里不存在 | 确认模块里确实定义了该名字 |
NameError: name 'math' is not defined | 用了 math.xxx 但只 from math import sqrt | 用 import math 才能写 math.xxx,或直接导入你要的函数 |
RecursionError 或行为异常(导入时打印了测试输出) | 自定义模块里测试代码没放在 if __name__ == "__main__": 里 | 把测试代码放进 if __name__ == "__main__": |