🐍 Day 9: 模块与包 — Python 代码组织的灵魂

16 阅读4分钟

📖 知识点简介

当你写了几百行代码后会发现,全塞在一个文件里就是灾难。Python 用 模块(Module)包(Package) 这两大武器帮你组织代码。学完今天的内容,你会彻底搞懂 import 的底层逻辑、__init__.py 的意义、以及如何避免循环导入这种经典噩梦。


1️⃣ 模块:就是一个 .py 文件

模块 = 一个 .py 文件。你用 import 引入它,就能用它里面的变量、函数和类。

示例

# my_utils.py
def greet(name):
    return f"Hello, {name}!"

PI = 3.14159

class Calculator:
    @staticmethod
    def add(a, b):
        return a + b
# main.py — 导入并使用
import my_utils

print(my_utils.greet("Alice"))      # Hello, Alice!
print(my_utils.PI)                   # 3.14159
calc = my_utils.Calculator()
print(calc.add(1, 2))               # 3

导入风格对比

方式写法使用优缺点
import moduleimport my_utilsmy_utils.greet()命名清晰,避免冲突
from module import namefrom my_utils import greetgreet()简洁,但可能污染命名空间
from module import *from my_utils import *全部暴露🚫 不推荐!你不知道导入了什么
import module as aliasimport my_utils as mumu.greet()适合长名字模块

🚨 避坑:__name__ 与模块入口

每个 Python 模块都有 __name__ 属性:

  • 被直接运行时 → __name__ = "__main__"
  • 被导入时 → __name__ = 模块名
# calc.py
def add(a, b): return a + b

# 只有直接运行时才执行这行
if __name__ == "__main__":
    print(add(3, 5))  # 测试代码放这里

为什么重要? 如果不加这个判断,别人 import 你的模块时,测试代码会跟着跑一遍!


2️⃣ 包:就是一个带 __init__.py 的文件夹

包(Package)本质是一个包含 __init__.py 文件的目录。__init__.py 可以是空文件,它的存在告诉 Python:「这个目录是一个包」。

目录结构示例

project/
├── main.py
└── mypackage/
    ├── __init__.py
    ├── math_ops.py
    └── string_ops.py
# mypackage/__init__.py
print("包已加载!")

# mypackage/math_ops.py
def add(a, b): return a + b
def multiply(a, b): return a * b

# mypackage/string_ops.py
def upper(text): return text.upper()

# main.py 中导入
import mypackage.math_ops
print(mypackage.math_ops.add(3, 4))  # 7

__init__.py 的妙用

不只是标记包的存在,还能做很多事:

# mypackage/__init__.py
# 1. 简化导入路径
from mypackage.math_ops import add, multiply
from mypackage.string_ops import upper

# 2. 定义 __all__ 控制 from package import *
__all__ = ["add", "multiply", "upper"]

# 3. 包级别初始化:读取配置、建立数据库连接

这样用户在 main.py 里就可以直接写:

import mypackage
mypackage.add(3, 4)       # 不用写 mypackage.math_ops.add

3️⃣ Python 的 import 机制(必知底层原理)

当你写 import foo 时,Python 按顺序搜索这些位置:

  1. 内置模块(sys.modules)— 已加载的模块缓存
  2. 当前脚本所在目录(第一个找到的)
  3. PYTHONPATH 环境变量指定的路径
  4. 标准库路径(Python 安装目录下的 lib)
  5. site-packages(第三方包安装位置)
import sys
# 查看模块搜索路径
for path in sys.path:
    print(path)

模块只加载一次!

import mymodule  # 首次:执行并缓存
import mymodule  # 第二次:直接从缓存读取,不会再次执行!

Python 用 sys.modules 字典缓存所有已加载的模块:

import sys
print(sys.modules.keys())  # 看看有哪些模块

4️⃣ 绝对导入 vs 相对导入

绝对导入(推荐)

从项目根目录或 sys.path 已知位置开始引用:

# 任何时候都推荐这样写
from package.subpackage import module
from package import module

相对导入(🚨 有坑)

# 在一个包内部使用
from . import sibling_module         # 当前包的兄弟模块
from .. import parent_package        # 父包
from ..sibling import some_function  # 兄弟包里的内容

关键规则:

导入方式唯一可使用场景
绝对导入任何文件
相对导入仅在包的内部文件(用 . 开头)
直接运行的文件不能用相对导入!会报错 ImportError

🚨 经典坑:相对导入 + 直接运行

# mypackage/foo.py
from . import bar  # 写得很开心

# 当你 python mypackage/foo.py 直接运行 → ❌ 报错!
# 正确使用:在包外部通过 import 引用它

5️⃣ 实战:打造可维护的包结构

project/
├── main.py
├── config.py
├── requirements.txt
├── README.md
└── myapp/
    ├── __init__.py          # 应用入口
    ├── models/
    │   ├── __init__.py
    │   ├── user.py
    │   └── order.py
    ├── services/
    │   ├── __init__.py
    │   ├── auth.py
    │   └── payment.py
    └── utils/
        ├── __init__.py
        ├── logger.py
        └── validators.py
# myapp/__init__.py
from myapp.services.auth import login
from myapp.services.payment import process_payment

__version__ = "1.0.0"

# myapp/models/user.py
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

# myapp/services/auth.py
from myapp.models.user import User  # 绝对导入

def login(email: str, password: str) -> User | None:
    """用户登录逻辑"""
    ...

# main.py — 使用简洁接口
from myapp import login, __version__

print(f"myapp v{__version__}")
user = login("alice@example.com", "123456")

⚡ 避坑 & 要点总结

#要点说明
1️⃣__init__.py 不是必须的?(Python 3.3+)有它可以做初始化,没有也可做命名空间包
2️⃣相对导入用 dot. = 当前包,.. = 父包,... = 爷爷包
3️⃣循环导入是设计问题把公共依赖提取到第三个模块,不要用"延迟导入"糊弄
4️⃣__all__ 控制通配导入__init__.py 定义,from pkg import * 时生效
5️⃣模块缓存用 sys.modulesimportlib.reload(module) 可强制重载
6️⃣脚本入口加 __name__ 检查这是 Python 社区共识,没有例外
# 调试小技巧:看模块从哪里来的
import requests
print(requests.__file__)
# /Users/you/.venv/lib/python3.12/site-packages/requests/__init__.py