六:模块与包——组织你的代码

一、回顾与本篇目标

上一篇我们学了函数,你学会了把一段逻辑封装起来,起个名字,随叫随到。但这只解决了“代码复用”的一半问题——在同一个文件里复用。

如果另一个项目也需要用你写的工具函数怎么办?复制粘贴?那迟早会出问题——改了这头的代码忘了改那头,两个版本渐行渐远。真正的解决办法是模块:把函数、类、变量放在独立的文件里,哪个项目需要就导入哪个。

本篇的目标:

  1. 理解 Python 中模块和包的概念
  2. 学会 import 的四种导入方式
  3. 掌握 if __name__ == '__main__' 的作用
  4. 学会用 pip 安装第三方库
  5. 学会用 venv 创建虚拟环境

二、什么是模块

在 Python 中,一个 .py 文件就是一个模块。文件名(去掉 .py)就是模块名。这件事极其简单——你不需要像 JavaScript 那样写 module.exportsexport。文件里定义的所有函数、变量、类,默认都可以被其他文件导入。

假设你有一个文件 math_utils.py

# math_utils.py —— 数学工具模块

PI = 3.14159

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def circle_area(radius):
    return PI * radius ** 2

在另一个文件 main.py(和 math_utils.py 在同一文件夹)里,你可以导入并使用它:

# main.py

import math_utils

print(math_utils.PI)                        # 3.14159
print(math_utils.add(10, 20))              # 30
print(math_utils.circle_area(5))           # 78.53975

这就是模块最基本的用法。不需要任何声明、不需要任何配置文件。Python 的设计哲学在这里体现得淋漓尽致:简单到几乎没有学习成本

三、import 的四种方式

Python 提供了多种导入方式,适用于不同场景。

方式一:导入整个模块

import math_utils

# 使用时需要带模块名前缀
print(math_utils.add(3, 5))

这是最推荐的方式。优点:清晰——一眼就能看出 add 函数来自哪个模块;不会和当前文件里的同名函数冲突。

方式二:导入模块中的特定函数

from math_utils import add, circle_area

# 使用时不需要模块名前缀
print(add(3, 5))
print(circle_area(5))

优点:简洁。缺点:如果当前文件也有一个叫 add 的函数,就会覆盖掉导入的那个。

方式三:导入模块中的所有内容(不推荐)

from math_utils import *

print(add(3, 5))
print(PI)

这会把模块里所有不以 _ 开头的名字全部导入当前命名空间。强烈不推荐,因为它会让你搞不清哪些名字是从外部来的,容易造成命名冲突。

方式四:导入时给模块或函数起别名

import math_utils as mu

print(mu.add(3, 5))

# 也可以给函数起别名
from math_utils import circle_area as ca

print(ca(5))

当模块名很长时,起别名很有用。比如 import numpy as np 是数据科学领域的标准写法。

和 JavaScript 的对比

操作 JavaScript (ES6) JavaScript (CommonJS) Python
导出 export function foo() module.exports = {foo} 默认所有顶层名称都可导入
导入整个模块 import * as utils from './utils' const utils = require('./utils') import utils
导入特定内容 import { foo } from './utils' const { foo } = require('./utils') from utils import foo
起别名 import { foo as f } const f = require('./utils').foo from utils import foo as f

四、Python 查找模块的路径

当你写 import math_utils 时,Python 怎么知道去哪找这个文件?它会按顺序搜索以下路径:

  1. 当前文件所在的目录
  2. PYTHONPATH 环境变量中的目录
  3. Python 标准库的目录
  4. 第三方库安装的目录(site-packages)

你可以查看当前 Python 的搜索路径:

import sys
for path in sys.path:
    print(path)

如果导入失败(ModuleNotFoundError),先检查文件名对不对,再检查文件是否在搜索路径里。

五、if __name__ == '__main__':让文件有两种运行模式

这是 Python 中最经典的两行代码,几乎每个 Python 文件底部都能看到:

if __name__ == '__main__':
    # 这里的代码只在直接运行这个文件时执行
    # 被其他文件 import 时不会执行
    pass

它解决什么问题?

假设 math_utils.py 底部有一些测试代码:

# math_utils.py

def add(a, b):
    return a + b

# 测试代码
print(add(10, 20))

当你写 import math_utils 时,整个 math_utils.py 文件会被执行一遍——包括那行 print。这意味着每次导入这个模块,都会在控制台打印一次测试结果,这显然不是你想要的行为。

但你又希望在直接运行 math_utils.py 时能执行这些测试。这就需要区分两种运行模式。

Python 通过 __name__ 变量来区分:

  • 当你直接运行一个 Python 文件时(如 python math_utils.py),该文件的 __name__ 变量被设置为 '__main__'
  • 当这个文件被作为模块导入时(import math_utils),__name__ 被设置为模块名 'math_utils'

所以,if __name__ == '__main__' 的意思是:只有当这个文件被直接运行时,才执行下面的代码。

# math_utils.py —— 正确的写法

PI = 3.14159

def add(a, b):
    return a + b

def circle_area(radius):
    return PI * radius ** 2

# 测试代码放在这里
if __name__ == '__main__':
    print('=== 测试 math_utils 模块 ===')
    print(f'add(10, 20) = {add(10, 20)}')
    print(f'circle_area(5) = {circle_area(5)}')

现在:

  • 直接运行 python math_utils.py:会输出测试结果。
  • 在其他文件中导入 import math_utils:不会输出任何东西,只导入函数。

这是一个非常优雅的设计。每个 Python 文件都可以同时是“可执行的脚本”和“可导入的模块”。

六、包:把多个模块组织成一个文件夹

当模块越来越多,你会想把相关的模块放在同一个文件夹里。这个文件夹就是一个

一个包在文件系统中就是一个包含 __init__.py 文件的文件夹。这个 __init__.py 可以是空的,它只是告诉 Python:“把这个文件夹当作一个包来对待”。

my_project/
├── main.py
└── utils/
    ├── __init__.py          ← 让 utils 成为一个包
    ├── math_utils.py        ← 数学工具
    └── string_utils.py      ← 字符串工具

使用包中的模块:

# main.py

# 方式一:导入包中的模块
import utils.math_utils
print(utils.math_utils.add(3, 5))

# 方式二:从包中导入模块
from utils import math_utils
print(math_utils.add(3, 5))

# 方式三:从包中的模块导入函数
from utils.math_utils import add
print(add(3, 5))

和 JavaScript 中从 node_modules 导入第三方模块类似,只不过 Python 的包就是普通的文件夹,不需要注册到 package.json

七、pip:安装第三方库

Python 自带的标准库非常强大(mathrandomjsondatetimeos 等),但真正的生态力量在第三方库——全世界开发者贡献的开源模块。

pip 是 Python 的包管理工具,相当于 Node.js 的 npm。它让你用一行命令安装任何第三方库。

检查 pip 是否已安装

pip --version

如果显示版本号,说明 pip 已就绪。pip 通常随 Python 一起安装。

安装第三方库

pip install 库名

例如安装 requests(最常用的 HTTP 请求库):

pip install requests

安装后,就可以在代码中导入使用了:

import requests

response = requests.get('https://www.example.com')
print(response.status_code)
print(response.text[:200])

pip 常用命令

命令 作用 npm 对应命令
pip install 库名 安装一个库 npm install 库名
pip install 库名==版本号 安装指定版本 npm install 库名@版本号
pip install --upgrade 库名 升级一个库 npm update 库名
pip uninstall 库名 卸载一个库 npm uninstall 库名
pip list 列出所有已安装的库 npm list -g --depth=0
pip freeze > requirements.txt 导出依赖列表 npm shrinkwrappackage.json
pip install -r requirements.txt 从依赖列表安装 npm install

Python 项目的依赖通常记录在 requirements.txt 文件中,相当于 Node.js 的 package.json

# requirements.txt
requests==2.31.0
flask==3.0.0
pymysql==1.1.0

别人拿到这个文件后,执行 pip install -r requirements.txt 就能安装完全相同的依赖版本。

八、虚拟环境:让每个项目有独立的 Python 环境

这是一个非常重要的概念。假设你有两个项目:

  • 项目 A 需要 requests 版本 2.28.0
  • 项目 B 需要 requests 版本 2.31.0

如果你把两个项目的依赖都装在系统全局的 Python 里,版本就会冲突。虚拟环境解决了这个问题:为每个项目创建一个隔离的 Python 环境,各装各的依赖,互不影响。

Python 内置了 venv 模块来创建虚拟环境。

创建虚拟环境

# 在项目根目录执行
python -m venv venv

这会在当前文件夹创建一个名为 venv 的子文件夹(你也可以起别的名字,但 venv 是惯例),里面有一份独立的 Python 解释器副本和独立的 pip。

项目文件夹结构:

my_project/
├── venv/              ← 虚拟环境(不要手动修改)
├── main.py
├── utils/
└── requirements.txt

激活虚拟环境

Windows(命令提示符或 PowerShell):

venv\Scripts\activate

Mac / Linux:

source venv/bin/activate

激活成功后,终端提示符前面会出现 (venv),表示当前在虚拟环境中:

(venv) C:\my_project>

现在你执行的 pythonpip 都是虚拟环境里的版本。用 pip install 安装的库只会装在这个虚拟环境里,不影响系统全局 Python。

退出虚拟环境

deactivate

终端提示符前面的 (venv) 消失,回到了全局环境。

为什么要用虚拟环境(和 Node.js 的对比)

Node.js 默认就是把依赖装在项目本地的 node_modules 里,天然隔离。Python 的 pip 默认是全局安装,所以需要虚拟环境来手动隔离。这是 Python 和 Node.js 在包管理上最大的设计差异。理解了这一点,你就不会奇怪为什么 Python 项目总要多一个激活虚拟环境的步骤。

最佳实践:每新建一个 Python 项目,第一步就是创建虚拟环境、激活它、然后在里面安装依赖。别忘了把 venv 文件夹加到 .gitignore 中——这和 node_modules 不应该提交到 Git 是一个道理。

九、常用标准库一览

Python 自带的标准库非常丰富,很多常用功能不需要安装第三方库就能完成。下面列出最常用的几个:

模块 作用 示例
math 数学函数 math.sqrt(16)math.pi
random 生成随机数 random.randint(1, 10)random.choice(list)
json 处理 JSON 数据 json.loads(str)json.dumps(obj)
datetime 处理日期和时间 datetime.datetime.now()
os 操作系统相关操作 os.path.join()os.listdir()
sys Python 解释器相关 sys.argv(命令行参数)、sys.path
re 正则表达式 re.search(pattern, text)
collections 高级数据结构 CounterdefaultdictOrderedDict

十、综合演示:搭建一个完整的项目结构

下面用本篇学到的模块、包、虚拟环境知识,搭建一个标准的小项目。

# 项目结构
# number_tools/
# ├── venv/
# ├── main.py
# ├── requirements.txt
# └── my_utils/
#     ├── __init__.py
#     ├── stats.py
#     └── converter.py

my_utils/__init__.py(空文件,让 my_utils 成为包)

my_utils/stats.py

# 统计工具模块

def mean(numbers):
    """计算平均值"""
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)

def median(numbers):
    """计算中位数"""
    sorted_nums = sorted(numbers)
    n = len(sorted_nums)
    if n == 0:
        return 0
    mid = n // 2
    if n % 2 == 0:
        return (sorted_nums[mid - 1] + sorted_nums[mid]) / 2
    else:
        return sorted_nums[mid]

def mode(numbers):
    """找出众数(出现次数最多的数)"""
    if len(numbers) == 0:
        return None
    from collections import Counter
    counter = Counter(numbers)
    return counter.most_common(1)[0][0]

my_utils/converter.py

# 单位转换模块

def celsius_to_fahrenheit(celsius):
    """摄氏度转华氏度"""
    return celsius * 9 / 5 + 32

def fahrenheit_to_celsius(fahrenheit):
    """华氏度转摄氏度"""
    return (fahrenheit - 32) * 5 / 9

def km_to_miles(km):
    """公里转英里"""
    return km * 0.621371

def miles_to_km(miles):
    """英里转公里"""
    return miles / 0.621371

main.py

# 主程序

from my_utils.stats import mean, median
from my_utils import converter

def main():
    # 测试统计工具
    scores = [85, 92, 78, 95, 88, 85, 70]
    print('===== 成绩统计 =====')
    print(f'原始数据:{scores}')
    print(f'平均分:{mean(scores):.1f}')
    print(f'中位数:{median(scores)}')

    # 测试单位转换
    print('\n===== 单位转换 =====')
    celsius = 30
    print(f'{celsius}°C = {converter.celsius_to_fahrenheit(celsius):.1f}°F')

    km = 10
    print(f'{km} 公里 = {converter.km_to_miles(km):.2f} 英里')

if __name__ == '__main__':
    main()

requirements.txt(目前没有第三方依赖,可以记录标准库以外的内容):

# 本项目暂无第三方依赖
# 后续如果需要,用 pip freeze > requirements.txt 生成

这个项目结构清晰、模块职责明确、可复用性高。这就是 Python 社区推荐的标准项目组织方式。

十一、本篇动手练习

练习 1:创建自己的工具包

把前几篇练习中写的函数(计算器、温度转换、字符串处理等)整理成一个包 my_toolkit,包含两个模块:math_tools.pystring_tools.py。然后写一个 main.py 导入并使用它们。

练习 2:使用第三方库

新建一个项目,创建虚拟环境并激活。安装 requests 库,写一个脚本获取任意一个公开 API 的数据(比如 https://api.github.com),并打印返回的状态码和部分内容。

练习 3:练习 if __name__ == '__main__'

创建一个模块 greetings.py,里面定义一个 greet(name) 函数。模块底部用 if __name__ == '__main__' 包裹一段测试代码。然后分别测试:直接运行这个文件,和在另一个文件中导入它,观察测试代码是否执行。

十二、本篇小结

这一篇你学会了 Python 的模块和包系统:

  • 模块:一个 .py 文件就是一个模块。文件名即模块名。不需要 export,所有顶层定义默认可导入。
  • import 的四种方式:导入整个模块(推荐)、导入特定函数、导入全部(不推荐)、起别名。
  • if __name__ == '__main__':让一个文件既可以作为模块被导入,也可以作为脚本直接运行。直接运行时 __name__'__main__',被导入时是模块名。
  • :包含 __init__.py 的文件夹。用于组织多个相关模块。
  • pip:Python 的包管理工具。安装、升级、卸载第三方库。requirements.txt 记录项目依赖。
  • 虚拟环境 venv:为每个项目创建独立的 Python 环境,防止依赖冲突。和 Node.js 的 node_modules 隔离思想相同,但需要手动激活。

模块化是编程的基石。学会合理拆分模块、组织包结构、管理项目依赖,你就能写出结构清晰、易于维护的项目。

下一篇预告

下一篇——《文件读写——处理文本和 JSON》:学习用 Python 读写本地文件。包括文本文件的读写、with 语句自动关闭文件、JSON 文件的读写、CSV 文件的处理。这是 Python 自动化脚本和数据处理的必备技能。

Python 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容