模块
随着程序越写越大,如果所有代码都放在一个 .py 文件里,会越来越难维护。
Python 使用模块和包来组织代码:
- 一个
.py文件就是一个模块(module) - 一个包含多个模块的目录可以组成包(package)
- 模块可以被其他模块导入和复用
模块的基本概念
什么是模块
一个 Python 文件就是一个模块。
例如:
abc.py
它就是一个名为 abc 的模块。
再例如:
hello.py
它就是一个名为 hello 的模块。
在其他代码里可以通过 import 导入模块:
import hello
模块的好处:
- 提高代码可维护性
- 方便代码复用
- 避免把所有函数和变量堆在一个文件里
- 不同模块中可以存在同名函数或变量
模块命名规范
创建自己的模块时,建议遵守:
- 文件名使用小写字母
- 多个单词用下划线连接,例如
user_utils.py - 不要使用中文、空格和特殊字符
- 不要和 Python 内置模块同名,例如
sys.py、time.py、random.py
错误示例:
sys.py
random.py
my module.py
学生管理.py
推荐示例:
user.py
user_utils.py
student_manager.py
如果你把自己的文件命名为 random.py,再写:
import random
Python 可能会先导入你自己的 random.py,而不是标准库里的 random 模块,从而造成非常隐蔽的问题。
包
为什么需要包
如果不同人都写了一个 utils.py,模块名就可能冲突。
为了解决模块名冲突,Python 使用包来组织模块。
例如:
mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py
此时:
abc.py的完整模块名是mycompany.abcxyz.py的完整模块名是mycompany.xyzmycompany是一个包
导入方式:
import mycompany.abc
或者:
from mycompany import abc
__init__.py
传统 Python 包目录中通常会有一个 __init__.py 文件:
mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py
__init__.py 可以是空文件,也可以写 Python 代码。
它的作用:
- 标记当前目录是一个普通 Python 包
- 包被导入时,
__init__.py会先执行 - 可以在里面定义包级别变量或导入常用对象
例如:
# mycompany/__init__.py
__version__ = '1.0.0'
使用:
import mycompany
print(mycompany.__version__)
补充说明:现代 Python 支持“命名空间包”,某些目录没有 __init__.py 也可能被导入。但初学阶段建议创建普通包时保留 __init__.py,更清晰,也更符合大多数教程和项目结构。
多级包
包可以有多级目录:
mycompany
├─ __init__.py
├─ abc.py
├─ utils.py
└─ web
├─ __init__.py
├─ utils.py
└─ www.py
各模块完整名称:
mycompany.abc
mycompany.utils
mycompany.web
mycompany.web.utils
mycompany.web.www
注意:
mycompany.web
本身也是一个模块,它对应的文件是:
mycompany/web/__init__.py
使用模块
模块文件模板
下面是一个简单的 hello.py 模块:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""A test module."""
__author__ = 'Michael Liao'
import sys
def test():
args = sys.argv
if len(args) == 1:
print('Hello, world!')
elif len(args) == 2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__ == '__main__':
test()
说明:
#!/usr/bin/env python3:在 Unix/Linux/macOS 中可以让脚本直接运行# -*- coding: utf-8 -*-:声明文件编码,现代 Python 3 默认就是 UTF-8,但写上也没问题"""A test module.""":模块文档字符串,可通过hello.__doc__查看__author__:模块作者信息import sys:导入标准库模块sysif __name__ == '__main__'::只在当前文件被直接运行时执行代码
import
导入模块:
import sys
导入后,可以通过模块名访问模块里的变量、函数和类:
import sys
print(sys.argv)
print(sys.path)
这种写法的优点是来源清楚:
sys.argv
一眼就能看出 argv 来自 sys 模块。
from ... import ...
也可以从模块中导入某个对象:
from sys import argv
print(argv)
导入多个对象:
from math import sqrt, pi
print(sqrt(9))
print(pi)
这种写法更短,但如果导入太多名字,容易造成命名冲突。
import ... as ...
可以给模块或对象起别名:
import numpy as np
这在第三方库中很常见。
也可以给函数起别名:
from math import sqrt as square_root
print(square_root(16))
不推荐使用 from module import *
下面这种写法会导入模块里的很多公开名字:
from math import *
不推荐初学者使用,因为你很难看出当前文件中的变量或函数来自哪里,也容易覆盖已有名字。
更推荐:
import math
print(math.sqrt(9))
或者:
from math import sqrt
print(sqrt(9))
sys.argv
sys.argv 是命令行参数列表。
例如有文件 hello.py:
import sys
def test():
args = sys.argv
print(args)
if __name__ == '__main__':
test()
在命令行运行:
python hello.py
可能输出:
['hello.py']
运行:
python hello.py Michael
可能输出:
['hello.py', 'Michael']
也就是说:
sys.argv[0]通常是脚本文件名sys.argv[1]开始才是用户传入的参数
改成问候程序:
import sys
def test():
args = sys.argv
if len(args) == 1:
print('Hello, world!')
elif len(args) == 2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__ == '__main__':
test()
运行效果:
python hello.py
# Hello, world!
python hello.py Michael
# Hello, Michael!
__name__ == '__main__'
这是模块中非常重要的写法。
if __name__ == '__main__':
test()
含义是:
- 如果当前文件被直接运行,
__name__的值是'__main__' - 如果当前文件被其他文件导入,
__name__的值是模块名
例如 hello.py:
def test():
print('hello')
if __name__ == '__main__':
test()
直接运行:
python hello.py
会执行 test()。
如果在另一个文件中导入:
import hello
不会自动执行 test()。
这样做的好处是:
- 模块可以被直接运行,用于测试
- 模块也可以被导入复用,不会自动执行测试代码
作用域
公开变量和私有变量
在模块中,我们可能会定义很多函数和变量。有些希望给外部使用,有些只希望模块内部使用。
Python 主要通过命名约定表达作用域意图。
公开名字:
PI = 3.14
def area(radius):
return PI * radius * radius
这些名字可以被外部直接使用。
特殊变量:
__author__ = 'Michael Liao'
__name__
__doc__
这类前后双下划线的名字通常有特殊含义。我们自己的普通变量不要随便写成 __xxx__。
非公开名字:
_private_name = 'internal'
def _helper():
pass
一个下划线开头表示:这是模块内部使用的名字,不建议外部直接访问。
注意:Python 并不会真正禁止你访问 _helper()。它只是一个约定。
封装内部逻辑
例如:
def _private_1(name):
return 'Hello, %s' % name
def _private_2(name):
return 'Hi, %s' % name
def greeting(name):
if len(name) > 3:
return _private_1(name)
return _private_2(name)
外部只需要调用:
greeting('Bob')
不需要关心 _private_1() 和 _private_2() 的内部细节。
这就是封装:
- 外部只暴露必要接口
- 内部实现可以自由调整
- 调用方不需要知道内部细节
__all__
如果模块中定义了 __all__,它会影响:
from module import *
例如:
__all__ = ['greeting']
def greeting(name):
return 'Hello, %s' % name
def _helper():
return 'internal'
当别人写:
from module import *
只会导入 __all__ 中列出的公开名字。
初学阶段可以先知道这个机制,不必急着大量使用。
安装第三方模块
Python 标准库自带很多模块,比如:
sysostimedatetimemathrandom
除了标准库,还可以安装第三方模块。
第三方模块通常通过 pip 安装。
pip 基本用法
安装模块:
python -m pip install Pillow
查看已安装模块:
python -m pip list
升级模块:
python -m pip install --upgrade Pillow
卸载模块:
python -m pip uninstall Pillow
为什么推荐写 python -m pip?
因为它能确保使用的是当前这个 python 对应的 pip,减少多版本 Python 环境中装错位置的问题。
示例:Pillow
Pillow 是常用的图像处理库。
安装:
python -m pip install Pillow
使用:
from PIL import Image
img = Image.open('test.jpg')
print(img.size)
如果导入时报错:
ModuleNotFoundError: No module named 'PIL'
通常说明:
- Pillow 没有安装
- 安装到了另一个 Python 环境
- 当前虚拟环境没有激活
requirements.txt
项目中通常会用 requirements.txt 记录依赖:
Pillow
requests
安装依赖:
python -m pip install -r requirements.txt
导出当前环境依赖:
python -m pip freeze > requirements.txt
注意:pip freeze 会导出当前环境里几乎所有包。学习阶段可以手动维护一个简洁的 requirements.txt。
虚拟环境
不同项目可能依赖不同版本的第三方库。为了避免互相影响,通常会给每个项目创建虚拟环境。
创建虚拟环境:
python -m venv .venv
Windows PowerShell 激活:
.\.venv\Scripts\Activate.ps1
激活后安装依赖:
python -m pip install requests
退出虚拟环境:
deactivate
虚拟环境的好处:
- 不污染系统 Python
- 不同项目依赖相互隔离
- 更容易复现项目环境
模块搜索路径
当执行:
import mymodule
Python 会按照一定路径搜索模块。
搜索路径保存在:
import sys
print(sys.path)
常见搜索位置包括:
- 当前脚本所在目录
- 标准库目录
- 第三方库目录
site-packages - 环境变量
PYTHONPATH指定的目录
如果找不到模块,会报错:
ModuleNotFoundError: No module named 'mymodule'
临时添加搜索路径
可以在代码运行时修改 sys.path:
import sys
sys.path.append(r'E:\Study\Python\my_scripts')
这种方式只在当前程序运行期间有效,程序结束后失效。
使用 PYTHONPATH
也可以设置环境变量 PYTHONPATH,让 Python 自动把指定目录加入搜索路径。
不过初学阶段更推荐:
- 把文件放到合理的项目目录结构中
- 在项目根目录运行程序
- 使用包和模块的正常导入方式
不要在项目里到处手动修改 sys.path,否则项目结构会越来越难理解。
常见导入方式对比
import module
import math
print(math.sqrt(9))
优点:来源清楚,不容易冲突。
from module import name
from math import sqrt
print(sqrt(9))
优点:调用更短。
import module as alias
import numpy as np
优点:适合常用第三方库的约定别名。
from module import *
from math import *
不推荐。名字来源不清楚,容易冲突。
常见误区
- 把自己的文件命名为标准库模块名,例如
sys.py、random.py - 导入模块时,以为模块中的所有代码都会“只导入函数”,其实顶层代码会执行
- 忘记使用
if __name__ == '__main__':,导致模块一导入就运行测试代码 - 在多个地方随意修改
sys.path - 安装第三方库时使用了错误的 Python 环境
- 以为下划线开头的变量真的不能访问,其实它只是约定为非公开
本章综合小结
- 一个
.py文件就是一个模块 - 包用于组织多个模块,避免模块名冲突
- 普通包通常包含
__init__.py import module可以导入模块from module import name可以导入模块中的指定对象__name__ == '__main__'用于区分“直接运行”和“被导入”- 下划线开头的名字表示模块内部使用
- 第三方模块通常用
python -m pip install 包名安装 - 虚拟环境可以隔离不同项目的依赖
sys.path决定 Python 从哪些目录查找模块
练习题
下面练习按难度从低到高排列。
基础题
- 新建文件
hello.py,定义函数say_hello(name),返回'Hello, name!'。 - 新建文件
main.py,导入hello模块并调用hello.say_hello('Alice')。 - 在
hello.py中加入:
if __name__ == '__main__':
print(say_hello('World'))
分别直接运行 hello.py 和在 main.py 中导入它,观察区别。
- 编写模块
calc.py,包含add(x, y)、sub(x, y)、mul(x, y)、div(x, y)四个函数。 - 在另一个文件中分别用下面几种方式导入并调用
calc.add():
import calc
from calc import add
import calc as c
包练习
- 创建如下目录结构:
tools
├─ __init__.py
├─ string_utils.py
└─ number_utils.py
- 在
string_utils.py中定义trim_and_title(s),去掉首尾空格并把首字母大写。 - 在
number_utils.py中定义is_even(n),判断是否为偶数。 - 在项目根目录新建
main.py,导入并使用:
from tools.string_utils import trim_and_title
from tools.number_utils import is_even
- 在
tools/__init__.py中定义__version__ = '1.0.0',并在main.py中打印它。
作用域练习
- 在模块中定义
_format_name(name)作为内部函数,再定义公开函数greeting(name)调用它。 - 尝试从外部访问
_format_name(),思考为什么能访问但不推荐。 - 给模块添加
__all__ = ['greeting'],观察from module import *的变化。
sys 和命令行参数练习
- 编写
args_demo.py,打印sys.argv。 - 编写
greet.py:- 没有参数时输出
Hello, world! - 有一个参数时输出
Hello, 参数! - 参数过多时输出
Too many arguments!
- 没有参数时输出
- 编写
calc_cli.py,支持命令:
python calc_cli.py add 3 5
python calc_cli.py mul 4 6
输出对应计算结果。
pip 和环境练习
- 使用下面命令查看当前 Python 对应的 pip 版本:
python -m pip --version
- 使用下面命令查看已安装包:
python -m pip list
- 创建虚拟环境
.venv并激活。 - 在虚拟环境中安装
requests,然后运行:
import requests
print(requests.__version__)
小项目练手
项目:命令行工具包
目标:把多个功能拆成模块和包,练习模块导入、包结构、__name__ == '__main__'、命令行参数和作用域。
项目结构:
mini_tools
├─ __init__.py
├─ text.py
├─ number.py
└─ cli.py
main.py
功能要求
mini_tools/text.py提供文本工具:normalize_name(name):去掉首尾空格,首字母大写is_palindrome(text):判断字符串是否回文
mini_tools/number.py提供数字工具:is_even(n):判断偶数factorial(n):计算阶乘
mini_tools/cli.py处理命令行参数。main.py作为程序入口。
推荐代码
mini_tools/text.py:
def normalize_name(name):
name = name.strip()
if not name:
return ''
return name[0].upper() + name[1:].lower()
def is_palindrome(text):
text = str(text)
return text == text[::-1]
mini_tools/number.py:
def is_even(n):
return n % 2 == 0
def factorial(n):
if n < 0:
raise ValueError('n must be >= 0')
result = 1
for x in range(1, n + 1):
result = result * x
return result
mini_tools/cli.py:
from mini_tools.number import factorial, is_even
from mini_tools.text import is_palindrome, normalize_name
def run(args):
if len(args) < 2:
print('Usage:')
print(' python main.py name alice')
print(' python main.py palindrome level')
print(' python main.py even 10')
print(' python main.py factorial 5')
return
command = args[0]
value = args[1]
if command == 'name':
print(normalize_name(value))
elif command == 'palindrome':
print(is_palindrome(value))
elif command == 'even':
print(is_even(int(value)))
elif command == 'factorial':
print(factorial(int(value)))
else:
print('Unknown command:', command)
main.py:
import sys
from mini_tools.cli import run
def main():
run(sys.argv[1:])
if __name__ == '__main__':
main()
运行示例
python main.py name alice
# Alice
python main.py palindrome level
# True
python main.py even 10
# True
python main.py factorial 5
# 120
进阶要求
- 在
mini_tools/__init__.py中添加__version__ - 给
factorial()添加更多参数检查 - 给
cli.py的错误输入添加提示 - 新增
mini_tools/file.py,实现读取文本文件行数 - 添加
README.md说明模块结构和运行方式
项目复盘
完成后重点回顾:
- 哪些代码应该放在模块里?
- 哪个文件是程序入口?
- 为什么
main.py里要写if __name__ == '__main__':? - 哪些函数是公开接口,哪些可以用下划线表示内部函数?
- 如果某个模块导入失败,应该检查哪些搜索路径和文件名?
浙公网安备 33010602011771号