Files

7.3 KiB

doc_type, full_text
doc_type full_text
short sources/04_Modules.md

04_Modules 总结

核心主题

本文介绍 Python 中的Python模块:任何 .py 源文件都是一个模块;模块通过 import 加载和执行,并形成独立的命名空间。文档还说明了不同导入形式、模块加载缓存、模块搜索路径,以及如何把通用函数拆分到多个文件中复用。

模块与导入

  • 任意 Python 源文件都可以作为模块,例如 foo.py。
  • import foo 会加载并执行 foo.py 中的所有顶层语句。
  • 导入后,需要通过模块名前缀访问其中的函数或变量:
    • foo.grok(2)
    • foo.spam('Hello')
  • 模块名直接来自文件名:foo.py 对应模块名 foo。

模块是命名空间

模块是一组命名值的集合,也可以理解为一个命名空间。

  • 模块中的全局变量、函数和类构成该模块的命名空间。
  • 不同模块可以定义相同名称而不会冲突。
  • 例如:
    • foo.py 中的 x 是 foo.x
    • bar.py 中的 x 是 bar.x

重要结论:模块彼此隔离。

模块作为执行环境

模块不仅是命名空间,也是其中代码的封闭环境。

# foo.py
x = 42

def grok(a):
    print(x)

在这个例子中,grok() 使用的全局变量 x 绑定到它所在模块 foo.py 的全局作用域。每个源文件都是自己的“小宇宙”。

模块执行机制

导入模块时,Python 会从上到下执行模块中的所有语句,直到文件结束。

模块命名空间最终包含:

  • 导入完成后仍存在的全局变量
  • 函数定义
  • 类定义
  • 其他顶层赋值结果

因此,如果模块顶层包含打印、创建文件、运行计算等脚本语句,那么这些语句会在导入时立即运行。这一点与后续的Python主模块和 if __name__ == '__main__' 主题密切相关。

import as

可以在导入时给模块取一个本地别名:

import math as m

这只改变当前文件中引用模块的名字,不改变模块本身,也不改变模块加载机制。

常见用途:

  • 缩短长模块名
  • 避免命名冲突
  • 遵循惯例,如 import numpy as np

from module import name

可以从模块中导入特定名称到当前命名空间:

from math import sin, cos

这样可以直接调用:

cos(theta)
sin(theta)

而不必写:

math.cos(theta)
math.sin(theta)

但需要注意:from math import sin, cos 仍然会加载整个 math 模块,只是在加载完成后把指定名称复制到当前作用域。

导入形式不会改变模块本质

以下导入方式在模块加载层面本质相同:

import math
import math as m
from math import cos, sin

关键点:

  • 模块仍然作为独立环境存在。
  • 导入时仍会执行整个模块文件。
  • import as 只是改变当前文件中的引用名。
  • from ... import ... 只是把模块中的某些名称引入当前作用域。

模块只加载一次

Python 每个模块通常只加载并执行一次。重复导入不会重新执行模块文件,而是返回已加载模块的引用。

已加载模块保存在:

sys.modules

sys.modules 是一个字典,记录当前解释器中已经加载的所有模块。

这会带来一个常见陷阱:

  • 如果修改了模块源码后,在同一个 Python 解释器中再次 import,通常不会看到修改结果。
  • 因为 Python 会直接使用 sys.modules 中缓存的旧模块。
  • 最安全的做法是退出并重启解释器。

这与Python导入缓存相关。

模块搜索路径

Python 使用 sys.path 查找模块。

import sys
sys.path

sys.path 是一个路径列表,通常当前工作目录位于最前面。

如果模块不在当前目录或标准库路径中,可以手动添加路径:

import sys
sys.path.append('/project/foo/pyfiles')

也可以通过环境变量 PYTHONPATH 添加搜索路径:

env PYTHONPATH=/project/foo/pyfiles python3

不过,文档强调:一般不应频繁手动调整模块搜索路径。若出现导入问题,优先检查当前工作目录是否正确。

练习 3.11:模块导入

本练习要求在正确的工作目录中启动 Python 解释器,并导入之前写过的程序。

示例:

import bounce
import mortgage
import report

重点观察:导入模块会运行其中的顶层代码,因此会看到程序输出。

随后导入 fileparse 模块:

import fileparse
help(fileparse)
dir(fileparse)

并使用其中的 parse_csv() 函数读取数据:

portfolio = fileparse.parse_csv(
    'Data/portfolio.csv',
    select=['name','shares','price'],
    types=[str,int,float]
)

也可以使用:

from fileparse import parse_csv

这样就能直接调用:

portfolio = parse_csv(...)

该练习强调代码复用:把通用 CSV 解析逻辑放入 fileparse.py,供其他程序导入使用。

练习 3.12:使用库模块改造 report.py

本练习要求修改之前的 report.py,让输入文件处理逻辑使用 fileparse.parse_csv()。

需要改造的函数包括:

  • read_portfolio()
  • read_prices()

目标是:

  • 保持原有报表输出不变。
  • 去除重复的 CSV 解析代码。
  • 将通用解析逻辑集中在 fileparse.py 中。

这体现了模块化设计的基本思想:通用功能抽取成库模块,业务程序通过导入使用。

练习 3.13

此练习有意留空,跳过。

练习 3.14:更多库导入

本练习要求修改 pcost.py,使其使用 report.read_portfolio() 来读取投资组合数据。

原目标功能:

import pcost
pcost.portfolio_cost('Data/portfolio.csv')

返回:

44671.15

修改后,pcost.py 不再重复实现读取投资组合的逻辑,而是复用 report.py 中已有的 read_portfolio()。

最终程序结构

完成练习后,应形成三个相互协作的程序:

  1. fileparse.py

    • 包含通用函数 parse_csv()。
    • 负责通用 CSV 文件解析。
  2. report.py

    • 生成股票报表。
    • 包含 read_portfolio() 和 read_prices()。
    • 内部使用 fileparse.parse_csv()。
  3. pcost.py

    • 计算投资组合成本。
    • 使用 report.read_portfolio()。

这种结构展示了从脚本式程序逐渐走向Python模块化编程的过程。

关键结论

  • .py 文件就是模块。
  • import 会执行整个模块文件。
  • 模块形成独立命名空间,不同模块中的同名变量不会冲突。
  • 模块中的全局变量绑定到其所在文件。
  • import as 只是在当前文件中改名。
  • from module import name 只是把模块中的名称复制到当前命名空间。
  • 模块通常只加载一次,缓存于 sys.modules。
  • Python 通过 sys.path 查找模块。
  • 正确的工作目录对导入本地模块非常重要。
  • 通过模块可以把通用函数抽取出来,实现更好的代码复用和程序组织。